Back-End/[Spring]

[Spring] @ExceptionHandler와 @ControllerAdvice를 활용하여 예외처리하기

연구소장 J 2024. 5. 14. 21:25

예외 처리

어떤 프로그램을 만들더라도 예외는 발생하기 마련이다.

예전부터 이러한 예외처리를 위해 다양한 방법이 생겨났다. 

기본적인 if문부터 시작해서 try-catch, throws 등등 예외 처리를 위한 많은 방법이 존재한다.

 

그러나 수많은 예외를 if문, try-catch 문으로 모두 처리하기에는 코드가 너무 복잡하고 어려워진다.

 

스프링은 @ExceptionHandler라는 어노테이션으로 매우 편리한 예외 처리 기능을 제공한다.

 

 

@ExceptionHandler

@ExceptionHanlder 어노테이션을 사용하면 컨트롤러(Controller) 계층에서 발생하는 예외를 처리할 수 있다.

 

@RestController
@Slf4j
public class BoardController {

    @ExceptionHandler(IllegalArgumentException.class)
    public void catchIllegalExeption(IllegalArgumentException e){
        log.error(e.getLocalizedMessage());
    }
    
    public ResponseEntity someMethod(SomeReqDto someReqDto){
        ...
    }

}

 

 

예를 들어 위와 같이 @RestController나 @Controller 어노테이션이 붙은 컨트롤러 클래스에

@ExceptionHandler 어노테이션을 붙인 예외 처리 메서드를 선언할 수 있다.

 

이때 해당 컨트롤러의 어떠한 메서드에서 IllegalArgumentException이 발생하더라도 catchIllegalException이 실행된다.

예를 들어 someMethod가 실행될 때 IllegalArgumentException이 발생하면 catchIllegalException이 실행되고 예외 처리를 진행하게 된다.

 

    @ExceptionHandler({IllegalArgumentException.class, IOException.class})
    public void catchIllegalExeption(Exception e){
        log.error(e.getLocalizedMessage());
    }

 

또한 위와 같이 하나의 메서드에 여러 개의 예외를 같이 처리할 수도 있다. 

이때 @ExceptionHandler에서 지정한 예외의 자식 클래스도 전부 캐치하게 된다. 

예를 들어 IOException의 자식 클래스인 FileSystemException, RemotException 등을 모두 잡게 된다.

 

    @ExceptionHandler( IOException.class)
    public void catchIOExeption(IOException e){
        log.error(e.getLocalizedMessage());
    }

    @ExceptionHandler( FileSystemException.class)
    public void catchFileSystemException(FileSystemException e){
        log.error(e.getLocalizedMessage());
    }

 

부모 클래스와 자식 클래스를 처리하는 메서드가 동시에 존재한다면, 항상 자세한 것이 우선순위를 갖는다.

예를 들어 FileSystemException이 발생하면 catchFileSystemException이 실행된다.

그외 IOException이나 그 자식 클래스 예외가 발생하면 catchIOException이 실행된다.

 

    @ExceptionHandler
    public void catchIOExeption(IOException e){
        log.error(e.getLocalizedMessage());
    }

 

위와 같이 @ExceptionHandler의 옵션에 예외를 지정하지 않으면 메서드 파라미터의 예외가 지정된다.

 

@ExceptionHandler의 한계

 

이처럼 @ExceptionHandler를 사용하면 간편하게 예외 처리를 할 수 있지만, 치명적인 단점이 존재한다,

 

그것은 바로 다른 컨트롤러에서 발생한 예외는 처리할 수 없다는 점이다. 예를 들어 위 BoardController가 아닌 UserController에서 같은 IllegalArgumentException이 발생하더라도 같은 예외 처리를 할 수 없다.

 

따라서 @ExceptionHandler만을 이용한다면 모든 컨트롤러에서 각자 예외처리를 해주어야 하기때문에 코드에 중복이 발생할 수 있다.

 

또한 비즈니스 로직과 예외처리 로직이 같은 곳에 위치하여 코드가 복잡해진다는 단점이 있다.

한 컨트롤러에 비즈니스 로직과 예외 처리 로직이 같이 있는 것보다는 기능을 분리하는 것이 유지보수에 좋을 것이다.

 

이러한 단점을 극복할 수 있는 기능이 바로 @ControllerAdvice이다.

 

@ControllerAdvice와 @RestControllerAdvice

@ControllerAdvice는 대상으로 지정한 컨트롤러들에 @ExceptionHandler 기능을 부여해준다.

참고로 @RestControllerAdvice는 @ControllerAdvice에서 @ResponseBody가 추가된 어노테이션이라고 보면 된다.

 

// @RestController가 붙은 모든 컨트롤러
@ControllerAdvice(annotations = RestController.class)
public class ControllerAdvice1 {}

// 특정 패키지의 컨트롤러
@ControllerAdvice("org.example.controllers")
public class ControllerAdvice2 {}

// 특정 컨트롤러 지정
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ControllerAdvice3 {}

// 모든 컨트롤러 적용(글로벌 적용)
@ControllerAdvice
public class ControllerAdvice4{}

 

위와 같이 @ControllerAdvice는 특정 컨트롤러들에 적용하거나 모든 컨트롤러에 적용할 수 있다.

 

 

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ResponseDto> catchIllegalArgumentException(IllegalArgumentException e){
        // 예외 처리
    }

    @ExceptionHandler(IOException.class)
    public ResponseEntity<ResponseDto> catchIOException(IOException e){
        // 예외 처리
    }
    
    
}

 

위와 같이 GlobalExceptionHandler 클래스를 작성해 모든 예외를 한 곳에서 처리할 수도 있다.

어떤 컨트롤러에서 IllegalArgumentException이나 IOException이 발생하더라도 공통적으로 예외 처리를 진행 할 수 있다.

 

특히 @RestControllerAdvice를 사용하면 자세한 오류 내용을 Json 형태로 내려줄 수 있다는 점이 큰 강점이다.

직접 만든 에러 응답 객체를 이용해 다양한 정보를 전달 할 수 있다.

 

 

실무 활용법

나는 실무에서 @ExceptionHandler와 @RestControllerAdvice를 아래와 같이 많이 활용한다.

 

우선 messages.properties에 에러 메시지들을 기록한다.

 

예를 들어

USER_INVALID_PARAMETER = {0}이/가 유효하지 않습니다

 

위와 같은 에러 메시지를 만든다. 

 

@RestController
@Slf4j
public class BoardController {

    @PostMapping(...)
    public ResponseEntity login(LoginReq loginReq){
        if(!validParameter(loginReq)){
        	throw new BizException(MessageSource.getMessage("USER_INVALID_PARAMETER"));
        }
    }

 

컨트롤러에서 validParameter와 같은 유효성 검증 메서드를 만들고, 해당 메서드를 통과하지 못하면 직접 정의한 BizException 같은 예외를 throw한다.

 

public class BizException extends RuntimeException{
    private final String code;
    
    public BizException(String code){
        super();
        this.code = code;
    }
}

 

위와 같이 비즈니스 로직 예외 클래스를 하나 만들어두고 공통 처리를 하면 편리하다.

 

@ControllerAdvice
public class MyExceptionHandler{

    @ExceptionHandler(BizException.class)
    public ResponseEntity<ResponseDto> bizHandler(BizException e){
        log.error(e.getMessage());
        
        return new ResponseEntity<>(
        ResponseDto.create(e.getCode(), e.getLocalizedMessage()), HttpStatus.OK);
   }
   
}

 

마지막으로  @ControllerAdvice를 붙인 예외 처리 핸들러 클래스를 작성해서 BizException을 처리하는 메서드를 작성하면 된다.

 

이때 미리 선언해둔 객체를 이용해 통일화된 응답을 내려줄 수 있다.

 

 

반응형