REST API

이동영·2025년 6월 21일

웹개발

목록 보기
14/36

REST API와 JSON의 등장 배경

웹 서비스를 사용하는 클라이언트에는 웹 브라우저만 있는 게 아니다. 스마트폰, 스마트워치, 태블릿, CCTV, 각종 센서 등이 모두 클라이언트이다. IT 기기의 발전에 따라 지금도 수많은 클라이언트가 만들어지고 있다.
서버는 이러한 모든 클라이언트의 요청에 응답해야 한다. 웹 브라우저뿐만 아니라 어떤 기기가 와도 기기에 맞는 뷰 페이지를 응답해야 한다.
그런데 이런 기기들은 앞으로 끝없이 나올텐데 그때마다 서버가 일일이 대응하기란 쉽지 않다. 그래서 이를 위해 REST API를 사용한다.

REST API

REST API(Representational State Transfer API)는 서버의 자원을 클라이언트에 구애받지 않고 사용할 수 있게 하는 설계 방식이다. REST API 방식에서는 HTTP 요청에 대한 응답으로 서버의 자원을 반환한다. 서버에서 보내는 응답이 특정 기기에 종속되지 않도록 모든 기기에서 통용될 수 있는 데이터를 반환한다.

JSON

REST API의 동작에서 서버는 클라이언트의 요청에 대한 응답으로 화면(view)가 아닌 데이터(data)를 전송한다. 이때 사용하는 응답 데이터가 JSON(JavaScript Object Notation)이다. 과거에는 응답데이터로 XML을 많이 사용했지만, 최근에는 JSON으로 통일되는 추세이다.
XML과 JSON의 데이터 형식을 간단히 비교하면 다음과 같다.

// XML : Extensible Markup Language
<article-form>
	<id>1</id>
    <title>가가가가</title>
    <content>1111</content>
</article-form>
// JSON : JavaScript Object Notation
{
	"id" : 1,
    "title" : "가가가가",
    "content" : "1111"
}

JSON 데이터는 키(key)와 값(value)으로 구성된 정렬되지 않은 속성(property)의 집합이다. 키는 문자열이므로 항상 큰따옴표("")로 감싸고, 값은 문자열인 경우에만 큰 따옴표("")로 감싼다.

API

API(Application Programming Interface)란 애플리케이션을 간편히 사용할 수 있게 하는, 미리 정해진 일종의 약속으로, 사용자와 프로그램 간 상호 작용을 돕는다. 자판기를 예로 들면, 자판기 버튼이 하나의 API가 된다. 콜라 버튼을 누르면 콜라가, 사이다 버튼을 누르면 사이다가 나오는 것은 각 버튼에 따라 반환될 음료를 미리 약속했기 때문이다. 이와 마찬가지로 '윈도우 API'는 윈도우 개발을 위해 미리 정해진 약속, '자바 API'는 자바 개발을 위해 미리 만들어진 약속이라고 보면 된다.

Talend API Tester

Talend API Tester 프로그램을 이용하면, HTTP 요청을 보내고 돌아온 응답을 쉽게 확인할 수 있다. 이는 크롬 웹 스토어에서 설치가 가능하다.

HTTP 상태 코드

HTTP 상태 코드는 클라이언트가 보낸 요청이 성공했는지, 실패했는지 알려 주는 코드이다. 코드는 100번대부터 500번대까지 5개 그룹으로 나뉘어 있다.

  • 1XX(정보) : 요청이 수신돼 처리 중
  • 2XX(성공) : 요청이 정상적으로 처리됨
  • 3XX(리다이렉션 메시지) : 요청을 완료하려면 추가 행동이 필요함
  • 4XX(클라이언트 요청 오류) : 클라이언트의 요청이 잘못돼 서바가 요청을 수행할 수 없음
  • 5XX(서버 응답 오류) : 서버 내부에 에러가 발생해 클라이언트 요청에 대해 적절히 수행하지 못함

HTTP 메시지

Talend API Tester 맨 아래에서 HTTP 메시지를 확인할 수 있다. HTTP 메시지는 시작 라인(start line), 헤더(header), 빈 라인(blank line), 본문(body)으로 구성된다.

  • 시작 라인 : HTTP 요청 또는 응답 내용이 있다. 시작 라인은 항상 한 줄로 끝난다.
  • 헤더 : HTTP 전송에 필요한 부가 정보(metadata)가 있다.
  • 빈 라인 : 헤더의 끝을 알리는 빈 줄로, 헤더가 모두 전송되었음을 알린다.
  • 본문 : 실제 전송하는 데이터가 있다.

REST API 구현하기

맛보기

우선 제대로 해보기 전에 맛보기부터 하겠다.
패키지 com.example.firstproject.api를 생성하고 그 안에 FirstApiController 클래스 생성하고 코드를 작성한다.

package com.example.firstproject.api;

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

@RestController		// REST API용 컨틀롤러
public class FirstApiController {

    @GetMapping("/api/hello")       // URL 요청 접수
    public String hello() {         // hello world! 문자열 반환
        return "hello world!";
    }
}

이렇게 해서 Api Tester로 실행해보면

이렇게 데이터만 나온다.
만약 REST API로 하지 않은 경우에는

이런식으로 뷰 페이지가 반환이 된다.

GET 구현

이번에는 패키지 com.example.firstproject.api 내에 ArticleApiController를 만든다.

@Slf4j
@RestController		// REST 컨트롤러 선언
public class ArticleApiController {
    @Autowired      // 게시글 리파지터리 주입
    private ArticleRepository articleRepository;
    
    // GET
    @GetMapping("/api/articles")        // URL 요청 접수
    public List<Article> index() {      // index() 메서드 정의
        return articleRepository.findAll();
    }
    
    @GetMapping("/api/articles/{id}")
    public Article show(@PathVariable Long id) {        // URL의 id를 매개변수로 받아오기
        return articleRepository.findById(id).orElse(null);
    }
}
  • 모든 게시글 조회
  1. 우선은 @RestController로 해당 클래스가 REST 컨트롤러임을 선언한다.
  2. @GetMapping으로 "/api/articles" 주소로 오는 URL 요청을 받는다.
  3. 메서드 수행 결과로 Article 묶음을 반환하므로, 반환형이 List인 index()라는 메서드를 정의한다. return문에는 articleRepository의 findAll() 메서드를 사용해 DB에 저장된 모든 Article을 가져와 반환한다.
  4. articleRepository를 클래스 내부에 선언한다. 이는 스프링 부트가 제공하므로 @Autowired 어노테이션을 붙여 의존성을 주입한다.
  • 단일 게시글 조회
  1. @GetMapping의 URL을 "/api/articles/{id}" 주소로 한다.
  2. 메서드 수행 결과로 단일 Article을 반환하므로 메서드의 반환형을 Article로 정의한다. 메서드 이름은 show()로 한다. return문에는 DB에서 id로 검색해 얻은 엔티티를 가져오도록 하고, 만약 해당 엔티티가 없으면 null을 반환하도록 한다.
  3. DB에서 id로 검색하려면 show() 메서드의 매개변수로 id를 받아 와야 한다. 이때 id는 요청 URL에서 가지고 오므로 매개변수 앞에 @PathVariable을 붙인다.

이제 GET 요청이 잘 되는지 확인해보면

전체 게시글 반환도 잘 되고

단일 게시글 반환도 됨을 확인할 수 있다.

POST 구현

    @PostMapping("/api/articles")       // URL 요청 접수
    public Article create(@RequestBody ArticleForm dto) {       // create() 메서드 정의
        Article article = dto.toEntity();
        return articleRepository.save(article);
    }
  1. @PostMapping으로 "/api/articles" 주소로 오는 URL 요청을 받는다.
  2. 반환형이 Article인 create()라는 메서드를 정의하고 수정할 데이터를 dto 매개변수로 받아온다. 이렇게 받아온 dto는 DB에서 활용할 수 있도록 엔티티로 변환해 article 변수에 넣고, articleRepository를 통해 DB에 저장한 후 반환한다.
  3. dto 매개변수 앞에 @RequestBody라는 어노테이션을 추가한다. 이렇게 해야 요청 시 본문(BODY)에 실어 보내는 데이터를 create() 메서드의 매개변수로 받아 올 수 있다. 그렇지 않으면 전부 null 값으로 나온다.

코드 작성 후, 서버를 재시작하고 POST 요청을 보내보면 성공 응답이 돌아오며, GET요청으로 추가가 되었음을 확인할 수 있다.

PATCH 구현

    // PATCH
    @PatchMapping("/api/articles/{id}")     // URL 요청 접수
    public ResponseEntity<Article> update(@PathVariable Long id,    // update() 메서드 정의
                                         @RequestBody ArticleForm dto) {
        // 1. DTO -> 엔티티 변환
        Article article = dto.toEntity();
        log.info("id : {}, article : {}", id, article.toString());      // 로그 찍기
        // 2. 타깃 조회
        Article target = articleRepository.findById(id).orElse(null);
        // 3. 잘못된 요청 처리
        if (target == null || id != article.getId()) {
            // 400, 잘못된 요청 응답!
            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);
        return ResponseEntity.status(HttpStatus.OK).body(updated);      // 정상 응답
    }
  1. @PatchMapping으로 "/api/articles/{id}" 주소로 오는 URL 요청을 받는다.
  2. 반환형이 Article인 update()라는 메서드를 정의하고 매개변수로 요청 URL의 id와 요청 메시지의 본문 데이터를 받아 온다.
    )) 1. 수정용 엔티티 생성하기
  3. 클라이언트에서 받은 수정 데이터가 담긴 dto를 DB에서 활용할 수 있도록 엔티티로 변환해 article 변수에 저장한다.
  4. 중간에 실행이 잘 되는지 확인하기 위해 id와 article의 내용을 로그로 찍어 본다. 코드에서 id는 첫 번째 중괄호({})에 들어가고, article.toString()의 결과는 두 번째 중괄호에 들어간다.
  5. 로그 기능을 이용했으므로 클래스 위에 @Slf4j 어노테이션을 추가한다.
    )) 2. DB에 대상 엔티티가 있는지 조회하기
  6. articleRepository.findById(id)를 통해서 DB에서 해당 id를 가진 엔티티를 가져오되 없다면 null을 반환한다. 이렇게 반환한 값은 target이라는 이름의 변수에 저장한다.
    )) 3. 대상 엔티티가 없거나 수정하려는 id가 잘못됐을 경우 처리하기
  7. 대상 엔티티가 없거나(target == null) 수정 요청 id와 본문 id가 다를 경우(id != article.getId) 잘못된 요청이므로 조건문을 실행한다.
  8. 잘못된 요청임을 확인할 수 있도록 id와 article의 내용을 로그로 찍는다.
  9. 클라이언트 요청 오류이므로 상태 코드 400을 반환해야 한다. 그런데 update() 메서드의 반환형을 단순히 Article로 하면 안 된다. Article을 ResponseEntity에 담아서 반환해야만 반환하는 데이터에 상태 코드를 실어 보낼 수 있다.
  10. if 뭉의 실행 결과 ResponseEntity의 상태(status)에는 400 또는 HttpStatus.BAD_REQUEST를, 본문(body)에는 반환할 데이터가 없으므로 null을 실어 반환한다.
    )) 4. 대상 엔티티가 있으면 수정 내용으로 업데이트하고 정상 응답(200) 보내기
  11. 일부 수정할 경우에도 데이터를 저장할 수 있도록 아직 만들지 않았지만 patch()라는 메서드가 있다고 가정해, target에 patch() 메서드로 aritcle(수정할 내용만)을 붙인다.
  12. article 엔티티에 담긴 수정용 데이터를 담은 target을 DB에 저장한 후 updated라는 이름의 변수에 저장한다.
  13. 수정된 데이터는 ResponseEntity에 담아서 보낸다. 이때 상태(status)에는 정상 응답이므로 200 또는 HttpStatus.OK를 싣고, 본문(body)에는 반환할 데이터인 updated를 싣는다.

Article 엔티티 코드에 patch()메서드를 추가한다.

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

patch() 메서드는 수정할 내용이 있는 경우에만 동작하면 된다. 따라서 if 문으로 article(수정 엔티티)의 title이 null이 아니면(article.title != null), 즉 갱신할 값이 있다면 this(target)의 title을 갱신해 준다. 같은 방법으로 content도 갱신해 준다.

이제 PATCH 요청으로 수정이 잘 되는지 확인한다.


부분 수정을 해도 잘 되는 것을 확인했다.

DELETE 구현

    @DeleteMapping("/api/articles/{id}")        // URL 요청 접수
    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();
    }
  1. @DeleteMapping으로 "/api/articles/{id}" URL 요청을 받는다.
  2. 반환형으로 ResponseEntity에 Article을 실어 보내는 delete()라는 메서드를 정의하고 URL의 id를 매개변수로 받아 온다.
  3. DB에 삭제할 대상 엔티티가 있는지 조회하고 없으면 null을 반환한다. 반환 받은 값은 target이라는 변수에 저장한다.
  4. 잘못된 요청을 처리하는 코드를 작성한다. 즉, target이 null이면 ResponseEntity의 상태(status)에는 BAD_REQUEST, 본문(body)에는 null을 실어 보낸다.
  5. 잘못된 요청이 아니라면 찾은 대상 엔티티를 삭제한다. 그리고 ResponseEntity의 상태(status)에는 HttpStatus.OK, 본문(body)에는 null을 실어 보낸다.
    그런데 return문에 body(null) 대신 build()를 작성해도 된다. ResponseEntity의 build() 메서드는 HTTP 응답의 body가 없는 ResponseEntity 객체를 생성한다. 따라서 build() 메서드로 생성된 객체는 body(null)의 결과와 같다.



이제 DELETE 요청을 보내서 삭제가 잘 되는지 확인해본다.

0개의 댓글