REST API의 동작 이해하기

클라이언트가 보내는 HTTP 요청 메시지의 첫 줄에는 시작 라인인 요청 라인이 있고 그 아래에는 헤더와 본문이 있고,

응답 메시지의 첫 줄에도 시작 라인인 상태 라인이 있고 그 아래 헤더와 본문이 있다.

응답으로 오는 상태 코드 요청이 성공했을 때는 200, 데이터 생성을 완료했을 때는 201을 반환한다.

요청한 정보를 찾을 수 없을 시에는 404, 서버 오류는 500을 반환한다.

위의 이미지와 같이 JSON은 키와 값의 쌍으로 된 속성으로 데이터를 표현하는데,

JSON의 값으로 또 다른 JSON 데이터나 배열을 넣을 수도 있다.

마지막으로 REST와 API의 의미를 알아보자면,

 먼저 REST는 HTTP URL로 서버의 자원을 명시하고 HTTP 메서드로 해당 자원에 대해 CRUD 하는 것을 말한다.

API란 클라이언트가 서버의 자원을 요청할 수 있도록 서버에서 제공하는 인터페이스이다.

REST API를 잘 구현하면 클라이언트가 기기에 구애받지 않고 서버의 자원을 이용하고,

클라이언트의 요청에 체계적으로 대응할 수 있어 프로그램의 재사용성과 확장성이 좋아진다.


REST API의 구현과정

REST API를 구현하려면 REST API의 주소인 URL을 설계해야 한다.

게시판의 Article 데이터를 CRUD 하기 위해 REST API의 주소를 아래와 같이 설계해준다.

주소 설계가 끝났다면 URL 요청을 받아 그 결과를 JSON으로 반환해 줄 컨트롤러도 만들어야 한다.

게시판을 만들 때는 일반 컨트롤러를 사용했지만,

REST API로 요청과 응답을 주고받을 때는 REST 컨트롤러를 사용해야 한다.


REST API 구현하기

프로젝트 탐색기의 com.example.firstproject에서 api 새 패키지 만들고 RestController인 FirstApiController 만들기.

/api/hello URL 요청 접수 시 문자열 반환하는 메서드 만들고 잘 동작하는 지 확인.

package com.example.firstproject.api;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FirstApiController {

    @GetMapping("/api/hello")
    public String hello() {
        return "hello world!";
    }
}

이제 순서대로 REST API로 GET, POST, PATCH, DELETE를 하나하나 구현해 볼 것이다.

먼저 RestController인 ArticleApiController 만들기.

GET은 전체 게시글과 특정 id의 단일 게시글 조회하기 두 가지인데 코드와 실행 결과는 아래와 같다.

    // GET - 전체 데이터
    @GetMapping("/api/articles")
    public List<Article> index() {
        return articleRepository.findAll();
    }

    // GET - 단일 데이터
    @GetMapping("/api/articles/{id}")
    public Article show(@PathVariable Long id) {
        return articleRepository.findById(id).orElse(null);
    }

두 번째로는 POST인데 이전과 달리 REST API에서 데이터를 생성할 시에는,

JSON 데이터를 받아와야 하므로 dto 매개변수 앞에 @RequestBody라는 어노테이션을 추가해 주어야 한다.

    // POST
    @PostMapping("/api/articles")
    public Article create(@RequestBody ArticleForm dto) {
        Article article = dto.toEntity();
        return articleRepository.save(article);
    }

이번에는 Talend API Tester로 응답코드 200 확인 후 h2 DB로도 확인해 보았다.

마지막으로는 데이터 수정과 삭제인 PATCH와 DELETE인데 두 경우 모두 잘못된 요청에 대한 처리를 해 주어야 한다.

수정의 경우 없는 데이터의 수정 요청이나, 수정 요청의 번호와 요청 본문의 번호가 잘못된 경우에 대한 처리를 해 주어야 한다.

삭제의 경우도 대상 엔티티게 없어서 요청 자체가 잘못된 경우를 처리해 주어야 한다.

수정 과정에서 생각해야 할 것이 하나 더 있는데 일부 데이터만 수정할 경우에도 생각을 해 주어야 한다.

코드로는 Article 엔티티에 아래와 같이 작성해주어 null이 아닐 경우 즉 갱신할 값이 있다면 this로 갱신을 해 준다.

    public void patch(Article article) {
        if(article.title != null) {
            this.title = article.title;
        }

        if (article.content != null) {
            this.content = article.content;
        }
    }

전체 PATCH와 DELETE 구현 컨트롤러 코드는 아래와 같다.

    // PATCH
    @PatchMapping("/api/articles/{id}")
    public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto) {
        // 1. dto -> 엔티티 변환하기
        Article article = dto.toEntity();   // dto를 엔티티로 변환
        log.info("id: {}, article: {}", id, article.toString()); // 로그 찍기

        // 2. 타깃 조회하기
        Article target = articleRepository.findById(id).orElse(null);

        // 3. 잘못된 요청 처리하기
        if (target == null || id != article.getId()) {  // 잘못된 요청인지 판별
            log.info("잘못된 요청! id: {}, article: {}", id, article.toString());    // 로그 찍기
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);    // ResponseEntity 반환
        }

        // 4. 업데이트 및 정상 응답(200) 하기
        target.patch(article);
        Article updated = articleRepository.save(target);  // article 엔티티 DB에 저장
        return ResponseEntity.status(HttpStatus.OK).body(updated);  // 정상 응답
    }

    // DELETE
    @DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Article> delete(@PathVariable Long id) {
        // 1. 대상 찾기
        Article target = articleRepository.findById(id).orElse(null);

        // 2. 잘못된 요청 처리하기
        if (target == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }

        // 3. 대상 삭제하기
        articleRepository.delete(target);
        return ResponseEntity.status(HttpStatus.OK).build();
    }