원티드 백엔드 챌린지 11월 2회차

원티드 백엔드 챌린지 11월 2회차

생성일
Nov 7, 2024 10:08 AM
최종 편집 일시
Last updated November 7, 2024
태그
JAVA

Request 처리 프로세스

notion image
  1. 가장 먼저 요청 메세지를 Dispatcher-Servlet이 받는다.
  1. Handler Mapping
    1. Request Message의 Header에 저장된 부가적인 정보를 이용해서 요청 처리를 위임할 Controller를 찾는다.
  1. Request를 위임할 HandlerAdapter를 찾아서 전달한다.
    1. Controller 구현 방식이 다양하기 때문에 이들을 모두 수용하기 위해서 Adapter 인터페이스를 통해 어댑터 패턴을 적용함으로써 컨트롤러에 구현 방식에 상관 없이 요청을 위임할 수 있다.
  1. HandlerAdpater가 Handler(= Controller)로 요청을 위임한다.
    1. HandlerAdpter에서 Handler로 요청을 위임하는 과정에서 Controller의 Method 인자에 선언된 객체(@RequestBody, @RequestParam)를 인스턴스화 한다.
    2. 이 과정은 ArgumentResolver가 HttpMessageConverter를 사용해서 Request Message의 데이터를 적절한 데이터 타입으로 변환 및 생성한다.
    3. ArgumentResolver 상세 프로세스

      1. Handler를 호출하여 파라미터 타입 정보를 전달 받는다.
      1. ArgumentResolver를 호출하여 Controller의 파라미터 객체 생성을 요청한다.
        1. supportsParameter: 파라미터의 처리 가능 여부를 확인하고 실행 여부를 리턴한다.
        2. resolveArgument: 유입된 Parameter를 가공하는 역할을 수행한다.
        3. parameter를 가공할 때, HTTPMessageConverter를 이용해서 Request Message를 알맞은 데이터 타입으로 변환하고 생성한 후 반환한다.

      HTTPMessageConverter 상세 프로세스

      1. HTTPMessageConverter를 이용해서 Request Message를 알맞은 데이터 타입으로 변환하고 생성한 후 반환한다.
        1. 기본적으로 제공되는 Converter가 다양하게 존재하고 JSON을 역직렬화할 수 있는 라이브러리를 추가하면 자동으로 Converter가 추가 등록된다.
        2. MessageConverter 구현체 중 MappingJacksonHttpMessageConverter를 사용하여 리플렉션을 통해 @RequestBody가 붙은 객체를 가져와 Jackson의 ObjectMapper를 써서 직렬화, 역직렬화를 하고 객체 타입으로 Casting 된다.
  1. 위 과정들이 완료되면 리플렉션을 이용해 Controller를 호출하면서 파싱된 Request Message를 담고 있는 객체를 파라미터로 함께 전달한다.

유효성 검사 @Valid

  • Request Message의 값 여부 및 Format을 정의한 대로 검사해주는 어노테이션
  • 실행 순서
    • ArgumentResolver@resolverArgument에서 실행되고 검사한다.
    • Filter of Request → DispatcherServlet → HandlerMapping → HandlerAdapter → HandlerInterceptor → ArgumentResolver(HttpMessageConverter → validateIfApplicable) → Handler(Controller)
    • Interceptor가 먼저 호출되고 ArgumentResolver가 실행된다는 점은 주의해야 한다.

@Valid 사용법

  • 검증에서 오류가 발생하면 MethodArgumentNotValidException 예외가 발생한다.

고정된 Response Message Format

Response의 경우 오청의 결과가 성공이든 실패이든 항상 동일한 Message 형식으로 반환해야 한다.
그렇지 않고 Response Message의 형식이 계속 변경된다면 프론트엔드 개발자 입장에서 불편할 수 있다.

공통 Response Message - Envelope Pattern

  • Response Message 형식을 공통화하기 위해서 사용되는 방법 중 가장 많이 사용되는 방법이 2가지 정도 있다.
  • 첫째로, ApiResponse<T> 객체를 제네릭 타입으로 정의 후, Handler Method의 Return 타입으로 사용하는 방법. 이 경우 Controller의 Method를 정의할 때마다 항상 return 타입을 ApiResponse<T>로 감싸야 한다. (귀찮아짐. ResponseEntity<>를 쓰는 것과 똑같다.)
  • 둘째로, ApiResponse<T>를 정의하고 AOP를 통해서 Handler가 Return하는 값을 중간에 가로채서 정의한 타입으로 감싸는 방법이 있다.

ResponseBodyAdvice

각 Controller의 Method의 Return 값을 공통적으로 가공하고 싶은 요소가 있을 때 사용한다.
제약조건으로는 Controller의 Method가 @ResponseBody || ResponseEntity로 리텅할 때 적용할 수 있다.

구조

  • supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>>
    • 특정 응답에 공통 처리 여부를 판단하는 메서드
    • returnType과 converterType을 매개변수로 받아서 사용자가 원하는대로 정의할 수 있다.
  • beforeBodyWrite(Object body, ....)
    • Controller의 반환 데이터를 공통 처리하는 로직을 담당하는 메서드

ResponseBodyAdvice의 문제점

ResponseBodyAdvice를 적용하여 중복을 제거하고 불필요한 제약 조건을 제거했다.
하지만 Controller Method의 리턴 타입이 primitive인 경우에 에러가 발생한다.
  • HttpMessageConverter#canRead 메서드가 실행되면서 메서드의 리턴 타입에 맞는 적절한 컨버터를 선택한다.
  • @ResponseBodyAdvice가 Return 값을 가로채서 공통 로직을 수행한다. (return new ApiResponse<>(responseMessage); )
  • ReturnValueHandler에서 HttpResponse Body에 문자열 데이터를 write하는 과정에서 StringHttpMessageConverter#addDefaultHeaders(responseMessage)가 실행된다.
  • 이 때 리턴 타입이 원시타입에서 ApiResponse<>로 바뀌어있기 때문에 캐스팅 에러가 발생한다.

해결 방법

@Component @Slf4j @Order(Ordered.HIGHEST_PRECEDENCE) public class CommonHttpMessageConverter extends AbstractHttpMessageConverter<ApiResponse<Object>> { private final ObjectMapper objectMapper; @Override public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) { return clazz.equals(ApiResponse.class) && this.canRead(mediaType); } @Override public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) { return (clazz.equals(ApiResponse.class) || clazz.isPrimitive()) && this.canWrite(mediaType); } // ApiResponse<T>용과 원시 타입용 Converter를 선언하고 우선순위(@Order)를 최상단으로 설정 @Override protected ApiResponse<Object> readInternal(Class<? extends ApiResponse<Object>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { throw new UnsupportedOperationException("This converter can only support writing operation."); } @Override protected void writeInternal(ApiResponse<Object> resultMessage, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { String responseMessage = this.objectMapper.writeValueAsString(resultMessage); StreamUtils.copy(responseMessage.getBytes(StandardCharsets.UTF_8), outputMessage.getBody()); } }

Error Handling 설계

공통 처리를 하는 이유
Response Message 공통 처리와 마찬가지로 Exception도 공통으로 처리할 수 있도록 하는 것이 코드의 가독성과 유지보수에 좋다. 이 또한 AOP를 사용하는 것이 좋다.
Exception 종류
Exception의 종류는 기본적으로 Spring Boot에서 제공하는 것으로 최대한 사용하고 기본적으로 제공하는 Exception으로 의미를 정확하게 전달하기 어렵다면 Custom해서 표현하는 것을 추천한다.
Custom 하는 경우는 지양하는 것이 좋긴 하다.
  • Exception의 네이밍을 잘못한 경우, 의미 전달이 왜곡될 수 있다.
  • 누군가 생성한 커스텀 익셉션이 작성자의 의도와 다르게 사용될 수도 있다.

구현

@RestConrollerAdvice를 사용하여 공통으로 처리될 수 있게 한다.
@ExceptionHandler를 통해 어떠한 에러는 특수성을 가지고 처리할 지 설정할 수 있다.
@ConfigurationProperties("error-trace") @RestControllerAdvice public class GlobalException extends ResponseEntityExceptionHandler { private boolean stackTrace; @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public final ErrorResponse handleBadReqExceptions(Exception ex, WebRequest request) { List<StackTraceElement> stackTraces = null; if (stackTrace) { stackTraces = Arrays.asList(ex.getStackTrace()); } return new ErrorResponse(stackTraces, ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } }