상세 컨텐츠

본문 제목

AOP!! Exception을 잡아줘!!

Backend

by 준빅 2023. 4. 29. 22:48

본문

AOP란? 

AOP란 Aspect Oriented Programming. 즉, 관점 지향 프로그래밍이다.

AOP는 횡단 관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 목표인 프로그래밍 패러다임이다.
코드 그 자체를 수정하지 않는 대신 기존의 코드에 추가 동작(어드바이스)을 추가함으로써 수행하며, 어느 코드가 포인트컷(pointcut) 사양을 통해 수정되는지를 따로 지정한다.
이를 통해 기능의 코드 핵심부를 어수선하게 채우지 않고도 비즈니스 로직에 핵심적이지 않은 동작들을 프로그램에 추가할 수 있게 한다.
- 위키백과 -

 

즉, AOP는 핵심 비즈니스 로직에서 횡단 관심사를 분리해 애플리케이션의 특정 부분에 적용하는 것을 말합니다.

 

횡단 관심사?

횡단 관심사란 애플리케이션의 여러 핵심 비즈니스로직에 공통 적으로 사용되는 부가기능을 의미합니다.

횡단 관심사에는 대표적으로 로깅, 보안, 트랜잭션이 존재합니다.


AOP로 Exception을 처리해 보자!

거의 모든 핵심 비즈니스 로직에 등장하는 예외처리. 횡단 관심사라고 보기에 충분합니다.

우리는 AOP 적용하여 예외처리라는 횡단 관심사를 핵심 비즈니스 로직과 분리하기로 했습니다.

 

 

@RestControllerAdvice와 @ExceptionHandler

@RestControllerAdvice

@RestControllerAdvice에 적용되어 있는 @ControllerAdvice 어노테이션에 들어가 봤습니다.

 

첫 번째 문장을 보면 해당 어노테이션이 무슨 기능을 하는지 알 수 있습니다.

간단히 말하면 @ExceptionHandler, @InitBinder, @ModelAttribute 어노테이션을 @Controller 클래스 간에 공유가 가능하도록 해 준다고 합니다.

 

때문에 @RestControllerAdvice@RestController 클래스 간의 공유가 가능하게 해주는 어노테이션이란 것을 알 수 있습니다.

 

여기서 @RestControllerAdvice가 @ExceptionHandler 어노테이션을 RestController 간의 공유가 가능하게 해 준다고 했는데, 그럼 @ExceptionHandler는 뭐 하는 어노테이션일까요?

 

@ExceptionHandler

역시 어노테이션을 들어가 보면 위와 같이 나와있습니다. 

정말 간단하게 Class나 Methode에서 발생하는 Exception을 핸들링하기 위한 어노테이션이란 것을 알 수 있습니다.

 

이 두 가지 어노테이션을 적용하면서 Exception을 핸들링하는 과정이 어떻게 바뀌었는지 보여드리겠습니다.

 

1. 최초의 핸들링

public void testingException(){
        throw new NullPointerException();
    }

NullPointerException을 발생시키는 testingException() 메소드를 만들어줍니다.

@GetMapping("/")
    public void testController(){
        try {
            celebService.testingException();
        }catch (Exception e){
            System.out.println("잡았다!! "+ e.getMessage());
        }
    }

Exception이 발생할 것이 예상되면 try catch를 통해 잡아줘야 했으며, 이는 모든 곳에 일일이 해야 하기 때문에 중복이 발생하기 마련이었습니다.

 

 

2. @ExceptionHandler 추가

@GetMapping("/")
    public void testController(){
        celebService.testingException();
    }

@ExceptionHandler(NullPointerException.class)
	public String testExceptionHandler(NullPointerException exception){
    	System.out.println("잡았다!! "+exception.getMessage());
	    return "에러발생!";
    	}

@ExceptionHandler를 사용하여 NullPoinerException이 발생했을 때의 동작을 만들 수 있습니다.

이렇게 처리하면 해당 Controller안에서 NullPoinerException이 발생하면 해당 동작을 실행합니다.

 

하지만 이렇게 처리하여도 Controller는 1개만 존재하는 게 아니기 때문에 횡단 관심사가 완벽히 분리되지 않습니다.

 

3. @RestControllerAdvice 추가

@GetMapping("/")
    public void testController(){
        celebService.testingException();
    }

Controller의 코드를 간략화하고,

 

GolbalExceptionHandler 클래스를 추가합니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NullPointerException.class)
    public String testExceptionHandler(NullPointerException exception){
        System.out.println("잡았다!! "+exception.getMessage());
        return "에러발생!";
    }
}

추가한 GolbalExceptionHandler 클래스@RestControllerAdvice를 적용하고, 이전에 Controller에 적용했던 @ExceptionHandler 부분을 GolbalExceptionHandler로 옮겨줍니다.

 

이렇게 되면 모든 RestController에서 NullPointerException이 발생 시 해당 메소드가 실행됩니다.

 

 

이 기능을 적용하여 Exception을 처리해 주는 GlobalExceptionHanlder를 만들어 적용하였습니다.

 

4. Sluv

Sluv에는 현재 Exception, DataAccessException, RuntimeException, ApplicationException에 대한 @ExceptionHandler를 GlobalExceptionHandler 클래스에 적용 중입니다.

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    private static final String LOG_FORMAT = "Error: {}, Class : {}, Message : {}";
    private static final String LOG_CODE_FORMAT = "Error: {}, Class : {}, Code : {}, Message : {}";

    @ExceptionHandler(ApplicationException.class)
    public ResponseEntity<ErrorResponse> applicationException(ApplicationException exception){
        log.error(
                LOG_CODE_FORMAT,
                "ApplicationException",
                exception.getClass().getSimpleName(),
                exception.getErrorCode(),
                exception.getMessage()
        );

        return ResponseEntity
                .status(exception.getHttpStatus())
                .body(ErrorResponse.builder()
                        .code(exception.getErrorCode())
                        .message(exception.getMessage())
                        .build()
                );
    }

그중에서 살펴보아야 할 곳은 ApplicationException입니다.

ApplicationException은 Sluv 애플리케이션에서 발생하는 Custom Exception을 처리하기 위해 만들었습니다.

 

@Getter
public abstract class ApplicationException extends RuntimeException{

    private final int errorCode;
    private final HttpStatus httpStatus;

    protected ApplicationException(int errorCode, HttpStatus httpStatus, String message){
        super(message);
        this.errorCode = errorCode;
        this.httpStatus = httpStatus;
    }
}

ApplicationException은 RuntimeException을 상속받으며, 애플리케이션에서 만드는 CustomException들은 ApplicationException을 상속받아 구현됩니다.

 

이렇게 되면 CustomException이 발생하면 GlobalExceptionHandler에@ExceptionHandelr(ApplicationException.class)가 실행됩니다.

 


주의사항

@RestControllerAdvice 클래스 안에 @ExceptionHandler를 사용할 때 순서가 매우 중요합니다.

 

@RestControllerAdvice 클래스 안에 정의된 @ExceptionHandler들은 순서에 따라 실행되기 때문입니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NullPointerException.class)
    public String testExceptionHandler(NullPointerException exception){
        System.out.println("잡았다!! "+exception.getMessage());
        return "에러발생!";
    }

    @ExceptionHandler(RuntimeException.class)
    public String runtimeExceptionHandler(RuntimeException exception){
        System.out.println("잡았다!! "+exception.getMessage());
        return "에러발생!";
    }
}

위와 같이 작성되었을 경우, NullPointerException이 발생하면 @ExceptionHandler(NullPointerException.class) 메소드가 실행되고, 그 이외의 RuntimeException에 포함된 예외가 발생하면 @ExceptionHandler(RuntimeException.class) 메소드가 실행됩니다.

 

하지만 순서를 다음과 같이 바꾸면 결과가 달라집니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public String runtimeExceptionHandler(RuntimeException exception){
        System.out.println("잡았다!! "+exception.getMessage());
        return "에러발생!";
    }
    
    @ExceptionHandler(NullPointerException.class)
    public String testExceptionHandler(NullPointerException exception){
        System.out.println("잡았다!! "+exception.getMessage());
        return "에러발생!";
    }
    
}

이렇게 되면 NullPointerException이 발생해도 @ExceptionHandler(RuntimeException.class) 메소드가 먼저 실행되기 때문에 @ExceptionHandler(NullPointerException.class) 메소드가 실행되지 않습니다.

 

때문에 순서를 고려하여 정의해야 합니다.

 

참고한 곳

- Tecoble
- 위키백과

 

 

관련글 더보기

댓글 영역