[Spring] 공통 응답 객체와 Exception Handler

Dev_owl ·2023년 10월 29일

1. @ResponseStatus

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Some parameters are invalid
  • 컨트롤러 메서드의 응답 상태를 지정하기
  • Spring은 표시된 메서드가 성공적으로 완료될 때만 해당 애너테이션을 사용합니다.

1-A. 오류 처리기 사용

  • @ResponseStatus를 사용하여 예외를 Http 응답 상태로 변환하는 세가지 방법이 있습니다.
    1. @ExceptionHandler 사용

      → 밑에서 후술

    2. @ControllerAdvice 사용

      → 밑에서 후술

    3. Exception 클래스 표시

      @ResponseStatus(code = HttpStatus.BAD_REQUEST)
      class CustomException extends RuntimeException {}

1-B. 자주 쓰는 HttpStatus

  • Enum class 이다.
  • 1xx(정보) : 요청을 받았으며 프로세스를 계속 진행합니다.
  • 2xx(성공) : 요청을 성공적으로 받았으며 인식했고 수용하였습니다.
  • 3xx(리다이렉션) : 요청 완료를 위해 추가 작업 조치가 필요합니다.
  • 4xx(클라이언트 오류) : 요청의 문법이 잘못되었거나 요청을 처리할 수 없습니다.
  • 5xx(서버 오류) : 서버가 명백히 유효한 요청에 대한 충족을 실패했습니다
EnumConstantDescription쓰는 상황
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#OK200 OK.요청이 성공적으로 처리됨
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#CREATED201 Created.요청이 성공적으로 처리되어 새로운 리소스가 생성됨
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#ACCEPTED202 Accepted.요청을 수신했지만 행동할 수 없음 (다른 프로세스에서 처리 또는 서버가 요청을 다루고 있음 또는 배치 프로세스 중)
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#BAD_REQUEST400 Bad Request.서버가 이해할 수 없는 요청
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#UNAUTHORIZED401 Unauthorized.표준에서는 미승인을 의미하지만 사실상 비인증을 의미함
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#FORBIDDEN403 Forbidden.클라이언트가 리소스에 접근할 권리가 없음
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#NOT_FOUND404 Not Found.요청받은 리소스를 찾을 수 없음
/ 리소스를 숨기기 위해 403 대신 내보낼 수 있음
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#CONFLICT409 Conflict.현재 서버의 상태와 충돌됨
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#INTERNAL_SERVER_ERROR500 Internal Server Error.서버 에러인 경우
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.html#BAD_GATEWAY502 Bad Gateway.서버끼리의 통신 중 유효하지 않은 응답을 받은 경우

2. @ExceptionHandler


//커스텀 예외를 매핑하여 내보낸다.
**@ExceptionHandler(CustomException.class)**
public CommonResponse handlerCustomException(CustomException e){

    return CommonResponse.builder()
            .returnCode(e.getReturnCode())
            .returnMessage(e.getReturnMessage())
            .build();
  • @Controller, @RestController가 적용된 Bean 내에서 발생하는 예외를 처리
  • 반환 타입, 인자 타입은 상관 없다.

3. @ControllerAdvice

  • 모든 @Controller에서 발생할 수 있는 예외를 잡아 처리 ↔ @ExceptionHandler는 하나의 클래스에서 발생하는 예외만 처리
    • 별도의 속성 없이 사용하면 모든 패키지 전역에 있는 컨트롤러를 담당

3-A. @RestControllerAdvice와 @ControllerAdvice의 차이

  • @RestControllerAdvice를 자세하게 들여다보자
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    **@ControllerAdvice
    //위의 애너테이션은 @ControllerAdivce와 같다.**
    @ResponseBody
    public @interface RestControllerAdvice
    • 기본적으로 @ControllerAdviced와 동일한 애너테이션을 공유한다.
    • @ResponseBody만 추가되어있다.
    • 즉, 예외를 잡아 핸들링할 수 있는 기능을 수행하면서 @ResponseBody를 통해 객체를 리턴할 수 있다.

3-B. 처리할 예외의 범위를 패키지 단위로 제한하는 방법

@RestControllerAdvice("com.example.demo.login.controller")

3-C. 하나의 @로 여러개의 예외를 잡는 방법

@ExceptionHandler({FileSystemException.class, RemoteException.class})
  • 예외를 잡을 때는 포괄적으로 잡는 것이 아닌 구체적으로 명시하자

4. 자주 사용하는 Exception

4-1. MethodArgumentNotValidException

  • @Valid 애너테이션으로 데이터를 검증하고, 해당 데이터에 에러가 있을 경우 예외메시지를 JSON으로 처리
  1. 검증할 필드에 Annotation을 추가합니다.

    @Id
    @Column(name = "COMP_CD", length = 100, nullable = false)
    @Comment(value = "회사 코드")
    @NotNull(message = "회사코드를 입력하세요")
    private String compCd;
    @Column(name = "COMP_NM", length = 50, nullable = false)
    @Comment(value = "회사명")
    **@NotNull(message = "회사명을 입력하세요")
    @Max(value = 50, message = "회사명을 50자리 이하로 입력하세요")
    private String compNm;**
  2. 컨트롤러에 @Valid를 추가합니다.

    @RequestMapping(method = {RequestMethod.PUT}, produces = APPLICATION_JSON)
    public Company save(@Valid @RequestBody Company request) {
        companyService.saveCompany(request);
        return request;
    }
  3. MethdoArgumentException Handler를 추가

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object processValidationError(MethodArgumentNotValidException ex) {
        return ApiResponse.error(ApiStatus.SYSTEM_ERROR, ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
    }

4-2. HttpStatusCodeException

  • Http 요처을 보내거나 Http 응답을 처리할 때 발생할 수 있는 예외 상황을 다루는데 사용
  • RestTemplate를 사용하여 상호작용하는 과정에서 발생함
    • 상태코드가 4—또는 5— 범위에 속할 때 발생한다.
  • 메서드
    getStatusCode()Http 응답의 상태 코드를 반환한다.
    getStatusText()Http 응답의 상태 텍스트를 반환한다.
    getResponseBodyAsString()Http 응답의 본문을 문자열로 반환한다.
    getHeaders()Http 응답의 헤더 정보를 반환한다.

5. Common Response

@Getter
@Setter
@Builder
@JsonInclude(Include.NON_NULL)
//null인 객체는 필드자체를 생략해버림
public class CommonResponse<T> {
    private String returnCode;
    private String returnMessage;

    //Dto 담을려는 용도로 제너릭스로 선언함
    private T info;

    public CommonResponse(CodeEnum codeEnum){
        setReturnCode(codeEnum.getCode());
        setReturnMessage(codeEnum.getMessage());
    }

    public CommonResponse(T info){
        setReturnCode(CodeEnum.SUCCESS.getCode());
        setReturnMessage(CodeEnum.SUCCESS.getMessage());
        setInfo(info);
    }
    public CommonResponse(CodeEnum codeEnum, T info){
        setReturnCode(codeEnum.getCode());
        setReturnMessage(codeEnum.getMessage());
        setInfo(info);
    }

}

6. flab 공개 포트폴리오 분석

  • Exception과 HttpStatus를 어떻게 매핑했는지 살펴보도록하자

1.

@RestControllerAdvice
public class ErrorController {

  /**
   * 패키지 명을 제외한 클래스 이름을 반환한다.
   * 
   * @param e 에러
   * @return
   */
  **private static String getSimpleName(Exception e) {
    return e.getClass().getSimpleName();
  }**
  
  @ResponseStatus(**HttpStatus.CONFLICT**) // 반환할 상태코드 설정한다.
  @ExceptionHandler(**DuplicateIdException.class**) // 처리할 에러를 설정한다.
  public ErrorMsg handleDuplicateIdException(DuplicateIdException e) {
    // Exception 객체의 현지화 메시지와 클래스 이름을 반환한다.
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(InvalidMenuGroupCountException.class)
  public ErrorMsg handleInvalidMenuGroupCountException(InvalidMenuGroupCountException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  //404 
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(InvalidMenuGroupIdException.class)
  public ErrorMsg handleInvalidMenuGroupIdException(InvalidMenuGroupIdException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  
  @ExceptionHandler(**HttpStatusCodeException.class**) 
  public ResponseEntity handleHttpStatusCodeException(**HttpStatusCodeException e**) {
    return ResponseEntity.status(e.getStatusCode()).build();
  }
  
  @ResponseStatus(**HttpStatus.BAD_REQUEST**)
  @ExceptionHandler(**IllegalArgumentException.class**)
  public ErrorMsg handleIllegalArgumentException(IllegalArgumentException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(value = {CanNotOpenShopException.class, CanNotCloseShopException.class})
  public ErrorMsg handleCannotShopException(RuntimeException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }

  
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(IssuedCouponExistException.class)
  public ErrorMsg handleIssuedCouponExistException(IssuedCouponExistException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  
  @ResponseStatus(**HttpStatus.CONFLICT**)
  @ExceptionHandler(DuplicateItemException.class)
  public ErrorMsg handleDuplicatedItemException(DuplicateItemException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  
  @ResponseStatus(**HttpStatus.INTERNAL_SERVER_ERROR**)
  @ExceptionHandler(MockPayException.class)
  public ErrorMsg handleMockPayException(MockPayException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
  
  @ResponseStatus(HttpStatus.UNAUTHORIZED)
  @ExceptionHandler(IdDeletedException.class)
  public ErrorMsg handleIdDeletedException(IdDeletedException e) {
    return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e));
  }
}

2.

  • 출처

https://github.com/f-lab-edu/daangn-market-used-trading

@RestControllerAdvice
public class ExceptionAdvice {

    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity<HttpStatus> memberNotFoundException() {
        return RESPONSE_NOT_FOUND;
    }

    @ExceptionHandler(**UnAuthorizedAccessException.class**)
    public ResponseEntity<HttpStatus> unAuthorizedAccessException() {
        return **RESPONSE_FORBIDDEN**;
    }

    @ExceptionHandler(**MethodArgumentNotValidException.class**)
    public ResponseEntity<String> validationNotValidException(MethodArgumentNotValidException e) {
        return new ResponseEntity<>(e.getFieldError().getDefaultMessage(), **HttpStatus.BAD_REQUEST**);
    }

    @ExceptionHandler(CategoryNotFoundException.class)
    public ResponseEntity<String> categoryNotFoundException(CategoryNotFoundException e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(AreaInfoNotDefinedException.class)
    public ResponseEntity<String> areaInfoNotDefinedException(AreaInfoNotDefinedException e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN);
    }

    @ExceptionHandler(PostNotFoundException.class)
    public ResponseEntity<HttpStatus> postNotFoundException() {
        return RESPONSE_NOT_FOUND;
    }

    @ExceptionHandler(UnAuthenticatedAccessException.class)
    public ResponseEntity<HttpStatus> unAuthenticatedAccessException() {
        return RESPONSE_UNAUTHORIZED;
    }

    @ExceptionHandler(PasswordNotMatchedException.class)
    public ResponseEntity<HttpStatus> passwordNotMatchedException() {
        return RESPONSE_BAD_REQUEST;
    }

    @ExceptionHandler(FileSizeLimitExceededException.class)
    public ResponseEntity<HttpStatus> fileSizeLimitExceededException() {
        return RESPONSE_PAYLOAD_TOO_LARGE;
    }
}

0개의 댓글