SPRING

스프링에서 예외 처리하기 (@ExceptionHandler, @ControllerAdvice, @ResponseStatus)

혀내 2023. 3. 9. 18:26
반응형

자바의 예외 종류와 기본적인 예외 처리 방법부터 간단히 알아보자!

그 뒤에 스프링에서의 예외 처리 방법을 살펴보면 이해가 훨씬 편할 것이다 :)

 

 

 


예외의 종류: 체크 예외와 언체크 예외

자바의 예외는 java.lang.Exception 클래스와 그의 하위 클래스들로 이루어져 있다.

예외는 RuntimeException 클래스의 상속 여부를 기준으로 다시 체크 예외언체크 예외로 나뉜다.

 

체크 예외

 체크 예외RuntimeException을 상속하지 않는 예외 클래스들을 말한다. 복구가 가능한 예외들이기 때문에 반드시 예외 처리 구문이 필요하다. try ~ catch문을 통해 해당 메소드 안에서 예외를 처리하거나 throws 문으로 해당 메소드를 호출하는 상위 메소드에게 예외를 넘길 수 있다. 체크 예외의 발생 여부는 프로그램을 실행하기 전인 컴파일 시점에서 바로 확인할 수 있다.

 

 

체크 예외 간단 정리

  • 체크 예외는 RuntimeException 클래스를 상속하지 않는 예외 클래스
  • 예외 처리가 필수!
    • try ~ catch 문: 해당 메소드에서 예외를 처리한다.
    • throws 문: 호출하는 상위 메소드로 예외를 넘긴다.
  • 발생 확인 시점: 컴파일 시점
  • 예시: FileNotFoundException, ClassNotFoundException, SQLException

 

 

언체크 예외

 언체크 예외RuntimeException의 하위 예외 클래스들을 말한다. 프로그램이 실행 중인 런타임 시점에서 언체크 예외의 발생 여부를 알 수 있다. 언체크 예외는 프로그램에 문제가 있을 때 의도적으로 발생되기 때문에 예외 처리를 꼭 하지 않아도 된다는 편리함이 있다.

 

 

언체크 예외 간단 정리

  • 언체크 예외는 RuntimeException 클래스를 상속하는 예외 클래스
  • 예외 처리는 선택
  • 발생 확인 시점: 런타임 시점
  • 예외 종류: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException

 

 

 


기본적인 예외 처리 방법 3가지

예외를 처리하는 방법 3가지를 간단히 알아보자.

 

1. 예외 복구 (try ~ catch 문)

 문제가 되는 코드를 예외가 발생하지 않도록 코드를 정상적으로 수정하거나 예외가 발생했을 때 try ~ catch ~ finally 문으로 다른 비즈니스 로직 흐름을 실행하도록 유도한다.

 

2. 예외 처리 회피 (throws 문)

 자신이 직접 직접 예외를 복구할 수 없는 경우에 사용하는데 throws 문으로 호출한 상위 메소드에게 예외를 던진다. throws 문을 중첩해서 사용하는 것은 무책임한 회피가 될 수 있으므로 꼭 신중하게 사용하도록 한다!

 

3. 예외 전환

 2번과 같이 발생한 예외를 직접 복구할 수 없는 상황에 이해하기 적절한 다른 예외로 변환해 던진다.

 

 

 

 

 이제부터 설명할 스프링의 예외 처리 방법은 바로 3번 예외 전환에 해당한다. 500 Internal Server Error가 서버 개발자의 실수가 아닌 프론트엔드의 요청 실수에 의해 발생한 경우에는 BAD REQUEST(400)이라는 응답 코드와 예외 발생 원인을 클라이언트에게 전달해야 한다. 예외 전환은 불필요한 throws 문, try ~ catch문을 줄여주기 때문에 유지보수 측면에서도 매우 유리하다.

 

 

 


1. @ExceptionHandler로 간단히 예외를 처리하는 방법

 

일단 커스텀 예외 클래스를 하나 만들자.

서버가 아닌 클라이언트의 요청 실수로 인해 발생한 예외는 CustomException으로 전환해서 다시 던질 것이다.

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
    private final ResponseCode responseCode;
}

 

 

CustomException 클래스로 던질 수 있는 예외 종류들은 ResponseCode enum 클래스를 만들어 관리한다.

httpStatus 필드에는 해당 예외가 발생했을 때의 상태 코드를, detail 필드에는 예외 메시지를 저장한다.

 

@Getter
@AllArgsConstructor
public enum ResponseCode {

    OK(HttpStatus.OK, "성공"),
    LECTURE_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "해당 강의를 찾을 수 없습니다."),
    LECTURE_STEP_DUPLICATED(HttpStatus.BAD_REQUEST, "중북되는 섹션의 강의가 존재합니다."),
    QUIZ_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 ID의 퀴즈를 찾을 수 없습니다."),
    QUIZ_BAD_REQUEST(HttpStatus.BAD_REQUEST, "해당 퀴즈의 답안 인덱스가 잘못된 값을 갖고 있습니다."),
    EDUCATION_PROGRESS_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "유저의 교육 진도 정보가 조회되지 않습니다."),
    EDUCATION_PROGRESS_BAD_REQUEST(HttpStatus.BAD_REQUEST, "이전 진도를 모두 완료해야 수강할 수 있습니다."),
    EDUCATION_QUIZ_FAIL(HttpStatus.BAD_REQUEST, "점수가 낮아 퀴즈 테스트를 실패했습니다."),
    EDUCATION_POSTURE_FAIL(HttpStatus.BAD_REQUEST, "점수가 낮아 자세실습 테스트를 실패했습니다."),

    private final HttpStatus httpStatus;
    private final String detail;
}

 

 

 

컨트롤러 단에서 발생하는 모든 Exception은 ExceptionHandler 클래스 한 곳에서 처리된다.

@RestControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler{

    @org.springframework.web.bind.annotation.ExceptionHandler(value = CustomException.class)
    protected ResponseEntity<ResponseTemplate> handleCustomException(CustomException e) {
        return ResponseTemplate.toResponseEntity(e.getResponseCode());
    }
}

 

 @ControllerAdvice, @RestControllerAdvice 어노테이션은 모든 @Controller에서 발생하는 Exception을 한 곳에서 처리하고 싶을 때 사용한다. @RestControllerAdivce 어노테이션은 @ControllerAdvice에 @ResponseBody가 추가된다.

 

 

 @ExceptionHandlervalue 파라미터에 있는 Exception 클래스가 던져졌을 때 아래 메소드가 실행됨을 의미한다. CustomException 클래스가 예외로 던져졌을 때, CustomException이 갖고 있는 ResponseCode 값을 Response의 body에 담아 클라이언트에게 응답하도록 구현하였다.

 

 

 

 클라이언트에게 보내는 Response의 틀은 다음과 같다. 요청에 대한 응답 코드, 응답 메시지, 발생 일시를 담아 보내준다.

@Builder
@AllArgsConstructor
public class ResponseTemplate {
    @Schema(description = "응답 코드", example = "200")
    public int status;

    @Schema(description = "응답 메시지", example = "성공")
    public String message;

    private final LocalDateTime timestamp = LocalDateTime.now();

    public static ResponseEntity<ResponseTemplate> toResponseEntity(ResponseCode responseCode) {
        return ResponseEntity
                .status(responseCode.getHttpStatus())
                .body(ResponseTemplate.builder()
                        .status(responseCode.getHttpStatus().value())
                        .message(responseCode.getDetail())
                        .build()
                );
    }
}

 

 

 위 클래스를 이용해 서비스 단에서 예외를 처리하는 방법은 아래 코드를 참고하면 된다.

    public LectureProgressDto readLectureProgress(String userId) {
        User user = userRepository.findById(userId).orElseThrow(
                () -> new CustomException(ResponseCode.NOT_FOUND_USER_EXCEPTION)
        );
        EducationProgress progress = progressRepository.findByUser(user).orElseThrow(
                () -> new CustomException(ResponseCode.NOT_FOUND_EDUCATION_PROGRESS)
        );

        List<LectureResponseDto> lectureResponseDtoList = lectureRepository.findAll()
                .stream().sorted()
                .map(l -> new LectureResponseDto(l))
                .collect(Collectors.toList());
        return new LectureProgressDto(progress, lectureResponseDtoList);
    }

 클라이언트로부터 잘못된 요청이 들어온 경우에는 파라미터에 예외 발생 원인 코드를 담아서 CustomException 예외를 던진다. 우리가 아까 전에 구현했던 ExceptionHandler가 CustomException을 잡아서 클라이언트에게 해당 예외 코드의 상태 코드와 메시지를 Response의 body에 담아 전달한다.

 

 

 

 

RuntimeException을 상속해 예외를 처리하는 방법

 블로그를 서치하다보면 다양한 예외 처리 방법이 나온다. 나는 @ControllerAdvice를 활용한 예외 처리 방법이 클래스 개수가 적어 주로 활용하는 편인데, 이번에는 RuntimeException을 상속하는 또 다른 예외 처리 방법도 알아보도록 하자!

 

 

두 번째 방법은 매우 매우 간단하다. 예외 종류별로 예외 클래스를 만들어서 RuntimeException을 상속하면 된다.

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
  ...
}

 

@ResponseStatus 어노테이션을 사용하면 이 예외가 발생했을 때 value 파라미터에 있는 응답 코드를 클라이언트에게 전달한다.

 

 

 

사용 방법도 간단하다. 서비스 단에서 문제가 생기면 구현한 예외 클래스를 던져주기만 하면 된다.

    public LectureProgressDto readLectureProgress(String userId) {
        User user = userRepository.findById(userId).orElseThrow(
                () -> new UserNotFoundException()
        );
        ...
    }

 

 구현 방법은 두 번째가 훨씬 간단하지만 예외 클래스를 많으면 수 십개까지 생성해야 한다는 점에서 유지보수가 불편하다는 단점이 있다.

반응형