에러 처리 - 지역적, 전역적 처리

2025. 1. 13. 16:17스프링

애플리케이션 개발 중 에러처리는 매우 중요한 부분 중 하나입니다. 

에러 처리가 제대로 되어있지 않다면, 사용자에게 불필요한 내부 시스템 정보가 노출되고 일관성 없는 에러 응답으로 클라이언트가 에러를 처리하는데 문제가 있을 수 있습니다.

 

스프링 부트에서는 에러처리를 위한 어노테이션으로는 @ExceptionHandler@ControllerAdvice가 있습니다.

 


@ExceptionHandler

@ExceptionHandler는 특정 컨트롤러에서 예외를 처리하기 위해 메서드에 사용하는 어노테이션입니다.

즉, 이 컨트롤러에서 에러가 발생하면 나는 여기에 정한 대로 에러를 처리할 거야!라고 스프링 컨테이너에게 알려주는 역할을 합니다.

 

특정 컨트롤러에서 지정해서 사용하는 것이기 때문에 당연하게 다른 컨트롤러에서 발생한 에러에 대한 처리는 스프링 부트의 기본예외 처리기가 처리합니다.

 

컨트롤러 안에서 @ExceptionHandler 어노테이션으로 선언해서 사용하면됩니다.

@RestController
@RequestMapping("temp")
public class TempController {
    @GetMapping("null-error")
    public ResponseEntity<String> nullError() {
        if(true) {
            throw new NullPointerException("Null Error");
        }
        return ResponseEntity.ok().build();
    }

    @ExceptionHandler
    public ResponseEntity<?> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
    }
}

위처럼 작성 후 요청을 보내보면 아래와 같이 응답이 옵니다.

 

@ExceptionHandler를 적용하지 않기 똑같이 요청을 보냈을 때는 아래와 같은 응답을 받을 수 있습니다.

 

추가로 아래와 같이 특정 클래스에 대해서만 처리할 수도 있습니다.

해당 경우 해당 에러가 아니라면 마찬가지로 기존 에러처리기가 동작합니다.

@ExceptionHandler(NullPointerException.class)
public ResponseEntity<?> handleException(Exception e) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
}

한계점

  • 지역적 적용: 이렇게 선언한 @ExceptionHandler는 선언한 컨트롤러 내부에서만 동작합니다.
  • 중복 문제: 동일한 에러처리를 위해서는 모든 컨트롤러에 동일한 로직을 반복해서 처리해야 합니다.
  • 관리 문제: 에러에 대한 내용을 변경하려면 적용된 모든 컨트롤러에서 수정이 필요합니다.

 

 


@RestControllerAdvice

스프링 문서

 

스프링에서 제공하는 @ControllerAdvice@RestControllerAdvice는 컨트롤러와 관련된 여러 공통 기능을 전역적으로 적용하기 위해 사용됩니다.

이 두 어노테이션과 위에서 공부한 @ExceptionHandler를 같이 사용하여 @Controller, @RestController에서 발생한 예외처리를 한 곳에서 전역적으로 처리할 수 있도록 설정할 수 있습니다.

 

따로 지정하지 않는다면 모든 컨트롤러에 대해서 예외처리를 하고 'basePackages' 속성을 통해서 범위 지정도 가능합니다.

 

@ControllerAdvice와 @RestControllerAdvice의 차이는 응답을 어떤 형식으로 하느냐입니다.

(@RestControllerAdvice는 Json형식으로 응답)


@RestControllerAdvice + @ExceptionHandler 적용

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<?> handleIllegalArgumentException(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body("handleIllegalArgumentException");
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGlobalException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "서버 에러가 발생했습니다.", "error", ex.getMessage()));
    }
}

응답

 

전역 에러 처리 적용 후에는 컨트롤러에서 @ExceptionHandler를 입력하지 않아도 @RestControllerAdvice에 정의된 메서드를 따라서 에러처리가 됩니다.


에러처리의 우선순위

  1. 전역적 에러 처리보다 지역적 에러 처리가 우선순위가 높습니다.
    @ControllerAdivce에 있는 @ExceptionHandler보다 @Controller에 있는 @ExceptionHandler가 우선순위가 높음.
  2. 상속 계층에서 더 구체적인 예외 클래스가 우선순위가 높습니다.
    @ExceptionHandler(Exception.class)보다 @ExceptionHandler(NullPointerException.class)가 우선순위가 높음.

 

전역 에러 처리를 추가했을 때 장점

 

일관된 에러 응답 형식 유지

 

전역으로 동일하게 에러 처리가 되어있지 않으면 위와 같이 JWT가 만료되어서 나는 에러지만 위쪽처럼 JSON 형식으로 에러가 응답될 수도 있고, 아래쪽처럼 메시지형태로 올 수 있습니다.

 

이처럼 각 컨트롤러나 메서드마다 다른 형식으로 응답하면, 클라이언트에서 에러 응답을 받을 때 처리하기 어려워질 수 있습니다.

전역 에러 처리를 통해 모든 에러를 동일한 구조(예: JSON 포맷)로 반환하여 클라이언트에서 쉽게 이해하고 처리할 수 있도록 할 수 있습니다.

 

유지보수의 효율성

각 컨트롤러에서 개별적으로 예외를 처리하면 코드가 중복되고 유지보수가 어려워집니다.

전역처리를 하면 중앙에서 로직처리를 하고 비즈니스 로직과 예외 처리 로직이 분리되기 때문에 유지보수가 쉽고

발생한 예외를 체계적으로 추적할 수 있어 디버깅과 모니터링에 유리합니다.