의문

서비스가 커질수록 커스텀 예외는 계속 늘어날텐데 컨트롤러 어드바이스에 계속 추가해야하나? 라는 의문이 생긴다.

만약 제목에 부적절한 단어가 포함되어 있다면 예외를 던지는 기능이 생긴다면 혹은 다른 예외가 생기면

아래처럼 계속 하나씩 어드바이스에 추가해야할까?

 

PostController

@PostMapping("/posts")
public Post post(@RequestBody @Valid PostCreate request) {
    if (request.getTitle().contains("바보")) {
        throw new InvalidRequestException();
    }
    return postService.write(request);
}

 

InvalidRequestException

public class InvalidRequestException extends RuntimeException{

    private static final String MESSAGE = "잘못된 요청입니다.";

    public InvalidRequestException() {
        super(MESSAGE);
    }
}

 

PostControllerAdvice

아래와 같이 컨트롤러 어드바이스에 하나하나 추가하게 되면 관리하기 너무 힘들어질 것이다.

@ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorResponse invalidRequestHandler(MethodArgumentNotValidException e) {
        ErrorResponse response = ErrorResponse.builder()
            .code("400")
            .message("잘못된 요청입니다.")
            .build();

        for (FieldError fieldError : e.getFieldErrors()) {
            response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
        }
        return response;
    }

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(PostNotFoundException.class)
public ErrorResponse postNotFoundExceptionHandler(PostNotFoundException e) {
    return ErrorResponse.builder()
        .code("404")
        .message(e.getMessage())
        .build();
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InvalidRequestException.class)
public ErrorResponse invalidRequestExceptionHandler(invalidRequestException e) {
    return ErrorResponse.builder()
        .code("400")
        .message(e.getMessage())
        .build();
}

 


최상위 커스텀 예외

스프링에서 제공하는 예외 중 자주 쓰이는 예외들의 익셉션 핸들러는 따로 만들어둬야하지만

커스텀 예외는 예외 종류(checked, unchecked 등) 별로 최상위 커스텀 예외를 만들어서 해결한다.

이 글에서는 우선 RuntimeException 을 상속받는 커스텀 예외 DunpleException 을 만들어서 해결한다.

abstract class 로 최상위 커스텀 예외를 만들고 내부 매서드 getStatusCode() 를 만들어 상태코드를 반환한다.

다른 커스텀 예외들도 기존 예외 대신 DunpleException 을 상속받는다.

 

DunpleException

public abstract class DunpleException extends RuntimeException {

    public DunpleException(String message) {
        super(message);
    }

    public DunpleException(String message, Throwable cause) {
        super(message, cause);
    }

    public abstract int getStatusCode();
}

ㄴ 추상 메소드 getStatusCode()

statusCode 를 동적으로 처리하기 위해 자식 클래스들이 상속받는 getStatusCode() 메서드를 만들어둔다.

 

InvalidRequestException

/**
 *  status -> 400
 */
public class InvalidRequestException extends DunpleException{

    private static final String MESSAGE = "잘못된 요청입니다.";

    public InvalidRequestException() {
        super(MESSAGE);
    }

    @Override
    public int getStatusCode() {
        return 400;
    }
}

 

PostControllerAdvice

@ExceptionHandler(DunpleException.class)
public ResponseEntity<ErrorResponse> dunpleExceptionHandler(DunpleException e) {
    int statusCode = e.getStatusCode();

    ErrorResponse body = ErrorResponse.builder()
        .code(String.valueOf(statusCode))
        .message(e.getMessage())
        .build();

    return ResponseEntity
        .status(statusCode)
        .body(body);
}

ㄴ @ExceptionHandler(DunpleException.class)

익셉션 핸들러를 DunpleException.class 로 변경한다.

DunpleException.getStatusCode()

최상위 예외 클래스에서 정의한 getStatusCode() 메서드를 이용해 statusCode 를 동적으로 처리한다.

ㄴ ResponseEntity

스프링에서 제공하는 ResponseEntity 를 이용하여 HttpStatus 도 동적으로 처리한다.

 


PostControllerTest

맨 위에서 작성한 제목에 '바보' 라는 단어가 포함되면 필터링하는 컨트롤러 테스트를 작성한다.

@DisplayName("글 작성 시 제목에 '바보'는 포함될 수 없다.")
@Test
void post4() throws Exception {
    // given
    PostCreate request = PostCreate.builder()
        .title("미카 공주님은 바보")
        .content("우리 공주님")
        .build();

    String json = objectMapper.writeValueAsString(request);

    // expected
    mockMvc.perform(
            post("/posts")
                .contentType(APPLICATION_JSON)
                .content(json)
        )
        .andExpect(status().isBadRequest())
        .andDo(print());

    assertEquals(0L, postRepository.count());
}

 

우리가 의도한 대로 테스트가 잘 수행됨을 알 수 있다.

 


리팩토링

사실 컨트롤러에서 검증하는것보다 request 객체 내에서 자체 검증을 하는게 깔끔하다.

따라서 간략하게 리팩토링 해보겠다.

 

PostController

@PostMapping("/posts")
public Post post(@RequestBody @Valid PostCreate request) {
    request.validate();
    return postService.write(request);
}

 

PostCreate

@Getter
@ToString
@NoArgsConstructor
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    @Builder
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public void validate() {
        if (title.contains("바보")) {
            throw new InvalidRequestException();
        }
    }
}

 


추가 요구사항

커스텀 예외처리 시 검증 실패한 필드와 에러 메시지 표시

전에 이 글에서 했던것처럼 커스텀 예외처리 시 예외 발생 필드와 에러 메시지를 추가하고 싶으면 어떡해야할까?

 

 

+ Recent posts