TIL - 0606

REST API, 그리고 스프링에서 REST API 디자인해보기

  • REST한 API(REST API)
    • 간단하게 말하자면 API 디자인 가이드라인(원칙)
    • API란? 원격으로 다른 시스템의 메소드 콜을 할 수 있도록 하는 것
    • 어떤 클라이언트라도 일관된 방법으로 콜 할 수 있게, 서버에 변동이 생겨도 클라이언트는 아무런 영향 받지 않도록 - 하위 호환성, 일관성을 띄는 API 디자인 가이드라인(원칙)
  • REST 왜 만들었을까?
    • 현재의 웹 환경을 파괴하지않고도 HTTP를 발전시킬 수는 없을까 라는 생각 아래에 만들어짐
  • REST한 API가 되려면 지켜야하는 디자인 원칙 : 각각의 디자인 원칙은 하나하나가 디자인 제약조건의 집합임(여러 제약조건이 모여 만들어진 집합)
    • client - server
    • stateless : 무상태성 유지(클라이언트 컨텍스트 저장하면 안됨)
    • cache : 응답 캐싱
    • code on demand : 서버가 보낸 코드를 클라이언트에서 실행시킬 수 있어야함(js)
    • layered system : 서버의 아키텍쳐 디자인(여러 대의 서버 로드밸런싱, 프록시 서버 등)에 대해서는 클라이언트가 알 필요가 없음 - 요청값(URI)만 있으면 됨
    • uniform interface : 일관성 있는 인터페이스(디자인)
  • 가장 지키지않고 지키기 번거로운 uniform interface : 어떤 제약조건들이 있길래? 나머지 제약조건들은 HTTP를 지키면서 API 디자인을 하면 지켜짐
    • 자원에는 식별자(@Id)가 있어야함 : URI를 통해 자원을 식별할 수 있어야함
    • 메세지에는 자원에 대한 어떤 처리(GET/POST/PUT/DELETE)를 할 것인지 명시되어있어야함
    • 메세지는 스스로를 설명할 수 있어야함 : 자원이 의미하는 바는 무엇인지 어디(명세)를 참조해야하는지, 호스트가 누구인지, 다음 가야할 링크가 뭔지, 데이터 타입이 무엇인지 등 어떤 클라이언트가 요청을 하더라도 메세지만 응답받으면 처리할 수 있도록 응답 메세지를 작성해야함
    • 모든 어플리케이션 상태 전이는 하이퍼링크를 통해서 이뤄짐(HATEOAS) : 클라이언트에 링크가 제시되어야함, 응답 결과 어디로 가야하는지(location)
  • 알아본 바로는 클라이언트가 무엇이라도 일관성(클라이언트가 무엇이라도, 서버에 변경이 생겨도) 있게 통신할 수 있게하는 것 그러려면 메세지에 필요한 정보를 다 넣어서 클라이언트가 결과 처리를 할 수 있도록 하는 것이 REST한 API 디자인이 아닐까 함 : 등장 배경이 그러하듯..!

  • @RestController : Spring에서 제공하는 REST API용 컨트롤러 어노테이션
    • @Controller + @ResponseBody를 합친 것 : 이전에는 두개를 써야했는데 하나로 퉁침 - 각 메소드(컨트롤러 액션)마다 @ResponseBody를 써야했으므로 매우 비효율적
    • @RestController를 적용하면? 기존 Controller flow(컨트롤러 처리 -> 뷰 리졸버 -> 뷰 응답)이 아닌 컨트롤러 처리 -> 메세지 컨버터 -> 응답 메세지 작성 후 응답
    • 메세지 컨버터 : 디폴트로 Jackson 라이브러리를 사용함 - 스프링부트는 web 스타터 모듈 하위에 json 모듈에 Jackson 디펜던시가 포함되어있음
    • 컨트롤러 리턴에서 객체를 리턴하면 메세지 컨버터가 요청 타입(xml, json)에 따라 변환해서 응답 바디에 변환 결과를 부과함

@RestController 
@RequestMapping("/questions")
public class QusetionController {

    @Resource
    private QnaService qnaService;
    
    @PostMapping
    public ResponseEntity<Void> create(@Valid @RequestBody QuestionDto questionDto) {
        Qusetion addedQuestion = qnaService.add(questionDto);
        
        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(URI.create("/questions/" + addedQuestion.getId()));
        return new ResponseEntity<>(headers, HttpStatus.CREATED);
    }
}
  • ResponseEntity<T> : T(타입파라미터)를 json으로 변환함(메세지 컨버팅), 헤더에 추가적인 내용을 부과할 때 ResponseEntity를 리턴타입으로 사용함
    • 다음 가야할 곳을 클라이언트에 알리기위해 헤더에 location을 부과함(HATEOAS)
    • 그냥 오브젝트를 리턴타입으로 사용하면 헤더 변경없이 바디에 해당 오브젝트를 컨버팅해서 응답메세지 완성

스프링 API(또는 J2EE) 커스텀 파라미터(HandlerMethodArgumentResolver)

  • HandlerMethodArgumentResolver : 메소드의 파라미터를 커스텀 하는 방법
    • 예외처리를 할 수 있어서 좋음
  • @Valid : 엔티티 validation을 위한 커스텀 파라미터, 어노테이션이 설정된 엔티티의 조건에 부합하지않을 때 MethodArgumentNotValidException를 발생시킴
  • @RequestBody : 요청 메세지의 바디를 특정 타입(클래스)오브젝트와 바인딩함 - REST API를 구현할 때 사용(요청 바디에 json 데이터 - 메세지 컨버터에 의해 변환)
  • 두 커스텀 파라미터 적용해보기(+ 예외처리 ControllerAdvice)
    • 예외가 발생했을 때에도 클라이언트에 응답을 json(오브젝트 리턴하면 메세지 컨버터가 알아서 컨버팅해줌)으로
/* controller */
@PostMapping
public ResponseEntity<Void> create(@Valid @RequestBody UserDto user) {
    User savedUser = userService.add(user);

    HttpHeaders headers = new HttpHeaders();
    headers.setLocation(URI.create("/api/users/" + savedUser.getId()));
    return new ResponseEntity<>(headers, HttpStatus.CREATED);
}

/* ControllerAdvice */
@RestControllerAdvice
public class ValidationExceptionControllerAdvice {
    private static final Logger log = LoggerFactory.getLogger(ValidationExceptionControllerAdvice.class);

    @Resource(name = "messageSourceAccessor")
    private MessageSourceAccessor msa;

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ValidationErrorsResponse handleValidationException(MethodArgumentNotValidException exception) {
        List<ObjectError> errors = exception.getBindingResult().getAllErrors();
        ValidationErrorsResponse response = new ValidationErrorsResponse();
        for (ObjectError objectError : errors) {
            log.debug("object error : {}", objectError);
            FieldError fieldError = (FieldError) objectError;
            response.addValidationError(new ValidationError(fieldError.getField(), getErrorMessage(fieldError)));
        }
        return response;
    }
}

해야할, 알아봐야할 것들

  • HTTP 인증프로토콜(2) 다이제스트 인증에 대해 알아보기
    • 스프링부트 스타터 디펜던시 알아보기
  • 레이어 구조 정리하기
  • JPA Persistence Context 한번 더 보기