1. REST API 동작.

  • 클라이언트가 보내는 HTTP 요청 메시지첫 줄에는 시작 라인인 요청 라인(request line)이 있고, 그 밑으로 헤더(header), 본문(body)가 있음.
  • 응답 메시지첫 줄에는 시작 라인인 상태 라인(status line)이 있고, 그 밑에 헤더(header), 본문(body)가 있음.
    • 응답으로 오는 상태 코드에는 100 ~ 500번대까지 5개의 그룹으로 나눠져있음.
  • REST API의 응답 표준으로 사용하는 JSON키(key)값(value)의 쌍으로 된 속성으로 데이터를 표현함.
    • JSON의 값으로 또 다른 JSON 데이터배열을 넣을 수도 있음.

1-1. REST, API

  • REST
    • HTTP URL로 서버의 자원(resource)을 명시하고, HTTP 메서드(POST, GET, PATCH/PUT, DELETE)로 해당 자원에 대해 CRUD(생성, 조회, 수정, 삭제)하는 것.
  • API
    • 클라이언트서버의 자원을 요청할 수 있도록 서버에서 제공하는 인터페이스(Interface)임.
  • REST APIREST 기반으로 API를 구현한 것.
    • REST API를 잘 구현하면 클라이언트가 기기에 구애받지 않고 서버의 자원을 이용할 수 있을 뿐만 아니라, 서버클라이언트의 요청에 체계적으로 대응할 수 있어서 서버 프로그램의 재사용성확장성이 향상됨.

2. REST API 구현 과정.

  • REST API를 구현하려면 REST API의 주소, 즉 URL을 설계해야됨.
    • 조회.
      • GET 메서드.
      • /api/articles : 전체 데이터 조회.
      • /api/articles/{id} : 단일 데이터 조회.
    • 생성.
      • POST 메서드.
      • /api/articles : 생성.
    • 수정.
      • PATCH 메서드.
      • /api/articles/{id} : 내용 수정.
    • 삭제.
      • DELETE 메서드.
      • /api/articles/{id} : 삭제.
  • URL 설계가 끝났으면 URL 요청을 받아 그 결과를 JSON으로 변환해 줄 컨트롤러(controller)가 필요함.
  • 단순 게시판을 만들 때는 일반 컨트롤러(controller)를 사용했지만, REST API요청, 응답을 주고 받을 때는 REST 컨트롤러(rest controller)를 사용해야함.
  • 응답을 보낼 때 적절한 상태 코드를 반환해주기 위해 ResponseEntity클래스도 활용함.


2-1. @Controller Vs @RESTController

  • @RESTController
    • JSON or 텍스트 같은 데이터를 반환함.
  • @Controller
    • 뷰(view) 페이지를 반환함.

좌 : RESTController, 우 : Controller


2-2. REST API - GET(전부 조회).

  • 모든 데이터 조회.
@RestController 
public class ArticleApiController {
    @Autowired
    private ArticleRepository articleRepository;

    @GetMapping("/api/articles")
    public List<Article> index() {      
        return articleRepository.findAll();
    }
}
  • @RestController
    • REST 컨트롤러 선언.
  • @Autowired
    • 의존성 주입.(Dependency Injection = DI)
  • @GetMapping("/api/articles")
    • "/api/articles" 주소로 오는 URL 요청을 받음.
  • List<Article>
    • 모든 데이터를 조회하므로 반환타입을 List<Article>로 설정.
  • return articleRepository.findAll();
    • repositoryfindAll메서드를 통해 DB에 저장된 모든 데이터를 가져옴.


2-3. REST API - GET(단일 조회).

  • 단일 데이터 조회.
    @GetMapping("/api/articles/{id}")
    public Article show(@PathVariable Long id) {
        return articleRepository.findById(id).orElse(null);
    }
  • @GetMapping("/api/articles/{id}")
    • "/api/articles/{id}"주소로 오는 URL 요청을 받음.
    • 게시글의 id마다 URL 요청이 변경되므로 {id}로 변수처리.
  • public Article show(@PathVariable Long id)
    • 단일 데이터를 반환하므로 Article 타입.
    • id로 검색하기 위해서 메서드의 매개변수로 값을 받아옴.
      • URL에 있는 {id}와 매개변수 id를 매핑 시켜줌.
      • @PathVariable를 이용해서.
  • return articleRepository.findById(id).orElse(null);
    • DB에서 findById를 이용해서 id엔티티를 가져오거나, 데이터가 없을 경우 null을 반환.


2-4. REST API - POST(생성)

  • 데이터 생성 요청.
    @PostMapping("/api/articles")
    public Article create(ArticleForm dto) {
        Article article = dto.toEntity();
        return articleRepository.save(article);
    }
  • @PostMapping("/api/articles")
    • "/api/articles" 주소로 오는 URL 요청을 받음.
  • public Article create(ArticleForm form)
    • 반환타입 Article
    • DTO를 매개변수로 해서 값을 받아옴.
  • Article article = form.toEntity();
    • DTO -> Entity 변환.
  • return articleRepository.save(article);
    • save()메서드를 이용해서 DB에 저장하고 반환함.

  • But 이렇게 코드를 작성하고 POST로 데이터를 보내면 title, contentnull이 나옴.
    • @Controller에서 데이터를 생성할 때는 메서드에 매개변수로 dto를 받아오기만 하면 됐지만
      REST API에서 데이터를 생성할 때는 JSON 데이터를 받아 와야 해서 매개변수dto를 썼다고 해서 받아올 수 있는 게 아님.
    • 이럴 때 매개변수 앞에 @RequestBody 어노테이션을 추가 해주면 됨.
      • 요청 시 본문(body)에 실어 보내는 데이터를 create()메서드의 매개변수로 받아올 수 있음.


2-5. REST API - PATCH(수정).

  • 데이터 수정 요쳥.
@PatchMapping("/api/articles/{id}")
public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto) {
    Article article = dto.toEntity();
    Article target = articleRepository.findById(id).orElse(null);
    if (target == null || id != article.getId()) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
    }
    Article updated = articleRepository.save(article);
    return ResponseEntity.status(HttpStatus.OK).body(updated);
}
  • @PatchMapping("/api/articles/{id}")
    • @PatchMapping으로 "/api/articles/{id}"이 주소로 오는 URL 요청을 받음.
  • @PathVariable Long id, @RequestBody ArticleForm dto
    • 매개변수로 요청 URL에 있는 {id}변수를 받아오기 위해서 @PathVariable를 사용했고, 요청 메시지의 Body 데이터를 받아오기 위해서 @RequestBody를 사용.
  • if (target == null || id != article.getId())
    • 존재하지 않는 article을 수정 요청한 경우, 수정 요청 id와 요청 본문의 id가 다를 경우.
    • 클라이언트의 요청 오류. (예외처리)
    • return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
      • if문이 참일 경우 잘못된 요청이므로 ResponseEntity상태(status)에는 BAD_REQUEST(400), Body에는 null을 실어서 반환함.
  • ResponseEntity<Article>
    • 반환타입을 ResponseEntity<Article>이렇게 설정해서 ResponseEntityArticle을 담아서 반환 함.
  • return ResponseEntity.status(HttpStatus.OK).body(updated);
    • 수정하려는 데이터를 DB에 갱신한 후 updated 변수에 저장해서 ResponseEntity에 담아서 응답으로 보냄.

2-5-1. 문제점.

  • 4번 글의 title 자체를 지우고 내용만 수정할 경우.


  • title의 기존 값 자체가 없어져서 null값이 되어버렸음.
  • 이러한 문제점을 개선해야할 필요성이 있음.
    • 수정 요청이 들어온 값만 수정을 하고, 값이 들어오지 않은 부분은 기존 값을 유지하는 식으로.
    • 즉, 기존 데이터에 수정 요청이 들어온 데이터를 붙여주면 일부 데이터만 수정할 수 있음.
    @PatchMapping("/api/articles/{id}")
    public ResponseEntity<Article> update(@PathVariable Long id, @RequestBody ArticleForm dto) {
        Article article = dto.toEntity();
        log.info("id = {}, article = {}", id, article.toString());
        Article target = articleRepository.findById(id).orElse(null);
        if (target == null || id != article.getId()) {
            log.info("잘못된 요청임 id = {}, article = {}", id, article.toString());
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        target.patch(article);								// 수정 된
        Article updated = articleRepository.save(target);	// 부분
        return ResponseEntity.status(HttpStatus.OK).body(updated);
    }
public void patch(Article article) {	// 새로 만든 메서드.
        if (article.title != null) {
            this.title = article.title;
        }
        if (article.content != null) {
            this.content = article.content;
        }
    }
  • patch() 메서드를 호출해서 수정할 내용이 있을 때만 동작하도록 구현.



2-6. REST API - DELETE.(삭제)

  • 데이터 삭제 요청.
    @DeleteMapping("/api/articles/{id}")
    public ResponseEntity<Article> delete(@PathVariable Long id) {
        Article target = articleRepository.findById(id).orElse(null);
        if (target == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        articleRepository.delete(target);
        return ResponseEntity.status(HttpStatus.OK).build();
    }
  • @DeleteMapping("/api/articles/{id}")
    • @DeleteMapping으로 /api/articles/{id} URL 요청을 받음.
  • if (target == null)
    • 예외처리.
  • articleRepository.delete(target);
    • 찾은 엔터티를 삭제함.
  • return ResponseEntity.status(HttpStatus.OK).build();
    • 응답으로 상태 코드(200)을 보냄.
    • 마지막에 .Body(null)를 쓰거나 .build()써도 결과는 같음.
      • ResponseEntitybuild() 메서드는 HTTP 응답의 Body가 없는 ResponseEntity 객체를 생성함.

3. ResponseEntity.

  • ResponseEntity
    • REST Controller의 반환형, 즉 REST API의 응답을 위해 사용하는 클래스.
    • 요청에 대한 응답을 할 때 해당 클래스에 HTTP 상태 코드, 헤더, 본문을 실어서 보낼 수 있음.
import java.net.URI;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;

public class ResponseEntity<T> extends HttpEntity<T> {
    private final Object status;

    public ResponseEntity(HttpStatusCode status) {
        this((Object)null, (MultiValueMap)null, (HttpStatusCode)status);
    }

    public ResponseEntity(@Nullable T body, HttpStatusCode status) {
        this(body, (MultiValueMap)null, (HttpStatusCode)status);
    }

    public ResponseEntity(MultiValueMap<String, String> headers, HttpStatusCode status) {
        this((Object)null, headers, (HttpStatusCode)status);
    }

    public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, HttpStatusCode status) {
        this(body, headers, (Object)status);
    }

    public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, int rawStatus) {
        this(body, headers, (Object)rawStatus);
    }

    private ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, Object status) {
        super(body, headers);
        Assert.notNull(status, "HttpStatusCode must not be null");
        this.status = status;
    }
    // 이하 코드 생략.
}

4. HttpStatus.

  • HttpStatus
    • HTTP 상태 코드를 관리하는 클래스.
    • 다양한 Enum 타입과 관련한 메서드를 가지고 있음.
      • Enum 타입은 열거형으로, 여러 상수로 이루어진 고정 집합을 가짐.
      열거형이름.상수 	(형식)
import org.springframework.lang.Nullable;

public enum HttpStatus implements HttpStatusCode {
    CONTINUE(100, HttpStatus.Series.INFORMATIONAL, "Continue"),
    SWITCHING_PROTOCOLS(101, HttpStatus.Series.INFORMATIONAL, "Switching Protocols"),
    PROCESSING(102, HttpStatus.Series.INFORMATIONAL, "Processing"),
    EARLY_HINTS(103, HttpStatus.Series.INFORMATIONAL, "Early Hints"),
    /** @deprecated */
    @Deprecated(
        since = "6.0.5"
    )
    CHECKPOINT(103, HttpStatus.Series.INFORMATIONAL, "Checkpoint"),
    OK(200, HttpStatus.Series.SUCCESSFUL, "OK"),
    CREATED(201, HttpStatus.Series.SUCCESSFUL, "Created"),
    ACCEPTED(202, HttpStatus.Series.SUCCESSFUL, "Accepted"),
    NON_AUTHORITATIVE_INFORMATION(203, HttpStatus.Series.SUCCESSFUL, "Non-Authoritative Information"),
    
    // 이하 코드 생략.

4-1. 참고

profile
Every cloud has a silver lining.

0개의 댓글