공부/Spring

[공부/Spring] @ExceptionHandler 예외처리

yulee_to 2023. 2. 21. 17:33

스프링

운영체제 : MacOS M1
IDE : IntelliJ
Spring Boot : 2.7.8
Java : 11
Persistence Framework : MyBatis

계기

회원가입을 구현하면서 성공을 제외하고 이메일 형식을 틀리거나, 이미 존재하는 아이디 등을 처리해줄 때 각 상황에 맞는 상태코드와 응답 Body에 넣어줄 메시지를 설정해주고 싶었다. 처음엔 서비스층에서 회원가입을 수행하다가 문제가 발생하면 그 문제를 컨트롤러에 전해주고, 컨트롤러에서는 문제가 무엇인지 판단하고 그에 맞는 상태코드와 응답 메시지를 만들려고 했다. 하지만 이 방법으로 구현하면 컨트롤러의 코드가 많이 지저분해지기도 하고, 서비스에서 어떤 문제인지를 판단을 한 상태이기 때문에 컨트롤러에서 또 그 문제에 맞는 상태코드와 응답 메시지를 지정해주는건 일을 두번하는 느낌이었다. 구글링을 통해 찾아보니 ExceptionHandler라는 것을 사용하면 서비스에서 로직을 수행하다가 예외 상황이 생기면 지정해둔 상태코드와 응답메시지가 응답으로 보내진다는 것을 알게 되어 적용해보았다.

 

@ExceptionHandler란?

@ExceptionHandler 어노테이션은 @Controller나 @RestController 어노테이션이 붙어 있는 클래스에서 발생하는 에러를 잡아서 메소드로 처리해주는 기능을 제공한다. @ExceptionHandler의 value 값으로 구체적인 예외 클래스를 지정해주면 해당 어노테이션이 붙은 메소드에서 그 예외를 처리해준다.

@Controller
public class ControllerExample {
    
    @ExceptionHandler(NullPointerException.class) 
    public ResponseEntity handle(NullPointerException ex) {
        //...
    }
}

value 값에 예외 클래스를 지정해줄 때는 최대한 구체적으로 명시해주는 것이 좋고, 여러개를 지정해주고 싶을 땐 중괄호({})로 묶고 쉼표(,)로 구분해주면 된다. (@ExceptionHandler({NullPointerException.class, IOException.class})) 

 

여러개의 Exception을 한 클래스에서 처리

컨트롤러에서 일어나는 예외들을 처리해주기 위해 그 컨트롤러 클래스에 @ExceptionHandler를 하나하나 지정해주면 재사용성이 낮아져서 안좋은 코드가 된다. 따라서 여러개의 Exception을 한 클래스에서 처리해주면 모든 컨트롤러에서 발생하는 예외들을 처리해줄 수 있다. Exception을 처리해줄 클래스에 @ControllerAdvice라는 어노테이션을 붙여줘 컨트롤러에서 발생하는 예외들을 잡아줄 수 있다. 

@ControllerAdivce
public class GlobalExceptionHandler {
    
    @ExceptionHandler(NullPointerException.class) {
    protected ResponseEntity handle(NullPointerException ex) {
        //...
    }
}

같은 기능을 하는 어노테이션인 @RestControllerAdvice는 @ControllerAdvice와 달리 각 메소드마다 @ResponseBody가 자동으로 붙어 응답을 Json으로 보내준다. 

 

에러 코드와 에러 메시지 상수화

ExceptionHandler를 사용할 때마다 그에 맞는 상태코드와 메시지를 쓰는것보다 상태코드와 메시지를 묶어 하나의 상수로 만들어두면 재사용하기도 쉽고 무엇보다 어떤 예외인지 바로 알아볼 수 있다는 장점이 있다. 

@Getter
@AllArgsConstructor
public enum ErrorCode {
     //...
     
     ID_DUPLICATE(HttpStatus.CONFLICT, "중복된 아이디입니다");
     
     //...
     
     private final HttpStatus status;
     private final String message;
}

각 예외들을 잘 나타낼 수 있는 이름을 지정해주고, 상태코드와 응답시 보내줄 메시지를 지정해준다.

@Getter

- 각 상수의 status와 message 값을 받아오기 위한 getter 메소드를 자동 생성해주는 어노테이션을 지정해준다.

@AllArgsConstructor

- 각 상수의 status와 message 값을 넣어주는 생성자들을 만들어주는 어노테이션을 지정해준다.

HttpStatus.*

- 상태코드 숫자를 써놓기 보다는 HttpStatus 클래스에 지정되어 있는 상태코드 상수를 이용해 어떤 상태코드인지 바로 알 수 있게 한다. 

 

응답 포맷 지정

응답을 보낼 때 상태코드와 메시지만 띄워주고 싶다. ResponseEntity의 body에 넣어줄 객체를 만들어주자.

@Getter
public class ErrorResponse {
    private HttpStatus status;
    private String message;
    
    public ErrorResponse(HttpStatus status, String message) {
        this.status = status;
        this.message = message;
    }
}

아직은 이 클래스가 어떤 역할을 하는지 감이 안 올 수도 있다. 계속 보다보면 이럴때 쓰는거구나 싶을거다.

 

Custom Exception

한 예외 중에서도 좀더 세분화해서 어떤 필드에서 해당 에러가 일어났다는 것을 명시해주고 싶을 때가 있다.

예를 들어 회원가입시에 아이디 중복 확인을 했는데 이미 존재하는 아이디인 경우 중복된 아이디가 있다는 예외를 던져줘야 한다.

그럼 적절하지 못한 인자일 때 처리되는 IllegalArgumentException으로 처리해줄 수 있지만, 좀 더 명확하게 DuplicateException이라는 이름으로 처리해줄 수 있다. 위에서 상수로 지정해둔 ErrorCode를 해당 클래스에 담아두고 다음을 보자. 

@Getter
@RequiredArgsConstructor
public class DuplicateException extends IllegalArgumentException {
    private final ErrorCode errorCode;
}

 

다시 ExceptionHandler를 처리하는 클래스로

상태코드와 응답 메시지를 하나로 묶은 상수들을 갖는 ErrorCode 클래스와 그 ErrorCode를 필드로 갖는 IllegalArgumetnException을 상속받는 DuplicateException이라는 클래스를 만들었다. 그리고 응답 객체인 ErrorResponse도 있다. 

이제 이 세 클래스를 가지고 다시 ExceptionHandler들을 모아놓은 클래스의 메소드를 살펴보자.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DuplicateException.class)
    protected ResponseEntity handleDuplicateException(DuplicateException ex) {
        return ResponseEntity
                .status(ex.getErrorCode().getStatus())
                .body(new ErrorResponse(ex.getErrorCode().getStatus(), ex.getErrorCode().getMessage()));
    }
}

DuplicationException을 던진 메소드에서는 throw new DuplicationException(ErrorCode.ID_DUPLICATE);를 수행한다. 

 

@RestControllerAdvice

- 자바 객체를 HTTP 응답 바디에 넣어주기 위한 @ResponseBody 어노테이션을 사용하려고 @RestControllerAdvice 어노테이션을 사용한다. 

@ExceptionHandler(DuplicationException.class)

- 만들어둔 예외 클래스를 ExceptionHandler의 value로 지정해준다.

ResponseEntity.status().body()

- status는 DuplicationException의 ErrorCode에서 상태코드를 가져와 지정해주고, body에는 ErrorResponse 객체를 새로 만들어 그 안에 ErrorCode의 status와 message를 지정해준다. 

 

정리

@Controller나 @RestController가 붙은 클래스에서 발생하는 예외들은 @ExceptionHandler를 붙인 메소드를 통해 처리해줄 수 있고, @ExceptionHandler들을 모아놓으려면 모아놓은 클래스에 @ControllerAdvice나 @RestControllerAdivce를 붙여주면 된다. 

그 외에 응답으로 보내줄 상태코드나 메시지를 상수로 관리해줄 수도 있고, 사용자 정의 예외를 만들어줄 수도 있다. 

 

 

728x90