Post

09 Spring Exception

09 Spring Exception

1. Exception

1. Exception

  • 프로그램이 실행되는 도중, 정상적인 흐름을 방해하는 오류 상황을 예외(Exception)
  • 예외는 단순한 문법 오류(Syntax Error)가 아니라, 실행 중(Runtime) 발생하는 문제
  • 코드가 예상치 못한 상태에 빠질 때 시스템이 이를 감지하고 처리 흐름을 바꾸기 위한 메커니즘

2. 일반적인 처리 흐름

  • 예외 발생 (Throw)
    • 코드 실행 중 오류 조건이 발생하면 예외 객체를 생성하고 던짐.
    • 예: Java의 throw new IOException()
  • 전달 (Propagate)
    • 예외가 발생한 메서드 내에서 처리되지 않으면 상위 호출 스택으로 전달
    • 이 과정을 “Exception Propagation”이라고 하며, 처리 가능한 코드 블록을 만날 때까지 계속 올라감.
  • 처리 (Catch)
    • 예외를 처리할 수 있는 블록(try-catch)에 도달하면, 그곳에서 예외를 잡아 적절한 조치를 취함.
    • 예를 들어, 로그를 남기거나, 사용자에게 오류 메시지를 보여주거나, 대체 로직을 실행

3. 다른 언어와의 비교

구분CJavaPythonJavaScript
기본 키워드없음 (반환값 / errno 사용)try / catch / finallytry / except / finallytry / catch / finally
예외 계층 구조없음 (구조적 예외 처리 없음)Exception / Error 계층 구분 명확모든 예외가 Exception 상속ECMAScript Error 기반
Checked Exception 존재없음있음없음없음
예외 선언 필요없음throws 키워드로 명시필요 없음필요 없음
런타임 처리 방식함수 반환값 또는 errno 값 확인JVM이 스택을 타고 전파인터프리터가 예외 전파JS 엔진이 비동기/동기 분리 처리
비동기 예외 처리없음 (직접 구현 필요)제한적 (주로 동기 코드 중심)asyncio의 try/exceptPromise.catch 또는 try/catch

2. Java Exception 처리

1. 계층 구조

1
2
3
4
5
  java.lang.Throwable
  ├── java.lang.Error
  └── java.lang.Exception
      ├── java.lang.RuntimeException
      └── Checked Exceptions (그 외 Exception)

2. Throwable / Error / Exception

구분설명주요 예시특징
Throwable자바 예외 처리 계층의 최상위 클래스. 예외와 오류 모두 상속.모든 예외와 오류는 Throwable을 상속해야 함. 예외 처리의 최상위 개념.
ErrorJVM 내부의 심각한 문제나 시스템 오류를 나타냄.OutOfMemoryError, StackOverflowError, VirtualMachineError시스템 수준의 치명적 오류. 보통 애플리케이션에서 직접 처리하지 않음. JVM 동작 중단 가능성 있음.
Exception애플리케이션에서 처리 가능한 예외의 최상위 클래스.IOException, SQLException, NullPointerException, ArithmeticException프로그램 실행 중 발생하는 처리 가능한 오류. Checked Exception과 Unchecked Exception으로 구분됨.

3. checkedException & uncheckedException

구분설명처리 의무주요 예시특징
Checked Exception컴파일 시점에 반드시 처리해야 하는 예외. 메서드 선언에 throws로 명시 필요.반드시 try-catch 또는 throws 처리해야 함.IOException, SQLException, ClassNotFoundException주로 외부 I/O, 데이터베이스, 네트워크 등 외부 환경 요인에서 발생. 컴파일러가 처리 여부를 강제.
Unchecked Exception컴파일 시점에 처리 여부를 강제하지 않는 예외. 런타임 시 발생.처리 선택 가능.NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException주로 프로그래머의 논리적 오류나 잘못된 코드에서 발생. RuntimeException을 상속.

3. Spring Exception 처리

1. Spring Exception 처리

1. Spring Exception

구조전역 예외 처리 지원@ExceptionHandler 사용전용 예외 처리 인터페이스특징
Spring MVCHandlerExceptionResolverDispatcherServlet 기반. 가장 체계적이고 다양한 처리 방식 제공
Spring WebFluxWebExceptionHandler비동기(Reactive) 방식 예외 처리. 비동기 전역 처리 지원
Spring WebSocket✅ (@MessageExceptionHandler)없음메시지 단위 예외 처리. 지속 연결 기반
서버리스❌ (환경에 따라 다름)일부 가능없음함수 단위 처리. 플랫폼 의존적
Spring BatchSkipPolicy, RetryPolicyStep/Job 단위 예외 처리. HTTP 구조 없음

2. DispatcherServlet과 Exception 흐름

1
2
3
4
  Client → DispatcherServlet → Controller → Exception 발생  
                                ↓
                    HandlerExceptionResolver → Exception 처리
  
  • 클라이언트 요청 → DispatcherServlet으로 전달
  • DispatcherServlet → 요청을 처리할 적합한 컨트롤러 검색 (HandlerMapping)
  • 컨트롤러 실행 → 예외 발생 시 해당 예외를 캡처
  • HandlerExceptionResolver가 예외 처리 담당
  • 예외 처리 결과 → 적절한 뷰(또는 ResponseBody) 반환

3. HandlerExceptionResolver 동작 순서

순서HandlerExceptionResolver 종류설명
1ExceptionHandlerExceptionResolver@ExceptionHandler 애노테이션이 있는 메서드 처리
2ResponseStatusExceptionResolver@ResponseStatus가 지정된 예외 처리
3DefaultHandlerExceptionResolver스프링 기본 예외 처리 (HttpMessageNotReadableException 등)

4. 주요 애노테이션 (@ExceptionHandler, @ControllerAdvice, @RestControllerAdvice)

  • @ControllerAdvice
    • 전역(Global) 예외 처리를 위해 사용.
    • 모든 컨트롤러에서 발생한 예외를 공통적으로 처리 가능.
    • @ExceptionHandler와 결합해 사용.
    • 예시
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      @ControllerAdvice
      public class GlobalExceptionHandler {
        
        @ExceptionHandler(value = Exception.class)
        public ResponseEntity<String> handleException(Exception ex) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body("전역 예외 발생: " + ex.getMessage());
        }
      }
      
  • @RestControllerAdvice
    • @ControllerAdvice + @ResponseBody 기능 통합.
    • REST API 예외 처리를 전역에서 적용할 때 사용.
    • JSON 형태로 예외 응답을 제공.
    • 예시
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      @RestControllerAdvice
      public class ApiExceptionHandler {
        
        @ExceptionHandler(value = Exception.class)
        public Map<String, String> handleApiException(Exception ex) {
            Map<String, String> error = new HashMap<>();
            error.put("error", ex.getMessage());
            return error;
        }
      }
      
  • @ExceptionHandler
    • 특정 컨트롤러 또는 클래스에서 발생하는 예외를 처리할 수 있다.
    • 메서드에 선언하며, 처리할 예외 타입을 지정한다.
    • 예시
      1
      2
      3
      4
      5
      
      @ExceptionHandler(value = { NullPointerException.class })
      public ResponseEntity<String> handleNullPointer(Exception ex) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
      .body("NullPointerException 발생: " + ex.getMessage());
      }
      

2. Exception 처리 구현 방식

1. 개별 컨트롤러에서 처리 (@ExceptionHandler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  @RestController
  public class MyController {
  
      @GetMapping("/test")
      public String test() {
          throw new CustomException("테스트 예외");
      }
  
      @ExceptionHandler(CustomException.class)
      public ResponseEntity<String> handleCustomException(CustomException ex) {
          return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                               .body("컨트롤러 전용 처리: " + ex.getMessage());
      }
  }
  • 여기서 Exception은 별도로 선언이 필요함.
  • 해당 컨트롤러 전용 예외 처리.
  • 전역 처리기보다 우선 순위가 높음.
  • 코드 관리가 간단하지만, 컨트롤러마다 중복 코드가 생길 수 있음.

2. 전역 처리용 핸들러 (@ControllerAdvice)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  @RestControllerAdvice
  public class GlobalExceptionHandler {
  
      @ExceptionHandler(CustomException.class)
      public ResponseEntity<String> handleCustomException(CustomException ex) {
          return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                               .body("전역 예외 처리: " + ex.getMessage());
      }
  
      @ExceptionHandler(Exception.class)
      public ResponseEntity<String> handleGeneralException(Exception ex) {
          return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                               .body("알 수 없는 예외 발생: " + ex.getMessage());
      }
  }
  • 전역적으로 예외를 처리 → 중복 코드 최소화.
  • 유지보수성이 높음.
  • 예외별 응답 표준화 가능.
  • REST API에서는 @RestControllerAdvice를 주로 사용.

3. ResponseEntityExceptionHandler 상속 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  @RestControllerAdvice
  public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
  
      @Override
      protected ResponseEntity<Object> handleMethodArgumentNotValid(
              MethodArgumentNotValidException ex,
              HttpHeaders headers,
              HttpStatus status,
              WebRequest request) {
  
          Map<String, String> errors = new HashMap<>();
          ex.getBindingResult().getFieldErrors()
            .forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
  
          return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
      }
  
      @ExceptionHandler(CustomException.class)
      public ResponseEntity<String> handleCustomException(CustomException ex) {
          return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                               .body("ResponseEntityExceptionHandler 처리: " + ex.getMessage());
      }
  }
  • 스프링 기본 예외 처리 기능 활용 가능.
  • 세밀한 커스터마이징이 가능.
  • 검증 실패, 메시지 변환 오류 등 다양한 예외 처리에 적합.
  • REST API 표준 응답 구조 설계에 유리.

3. 커스텀 Exception 설계

1. Custom Exception 클래스 (에러 클래스)

1
2
3
4
5
6
7
8
9
10
11
12
  public class CustomException extends RuntimeException {
      private final String errorCode;
  
      public CustomException(String errorCode, String message) {
          super(message);
          this.errorCode = errorCode;
      }
  
      public String getErrorCode() {
          return errorCode;
      }
  }

2. ErrorResponse DTO (에러를 넘겨주는 DTO)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  public class ErrorResponse {
      private String errorCode;
      private String message;
      private LocalDateTime timestamp;
  
      public ErrorResponse(String errorCode, String message) {
          this.errorCode = errorCode;
          this.message = message;
          this.timestamp = LocalDateTime.now(); // 필요한 정보 생성
      }
  
      // Getter, Setter 생략 
      // lombok처리
  }

3. 예외 공통 처리 로직 (에러 발생시 Json리턴)

  • 에러 핸들러
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
      
        private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
      
        @ExceptionHandler(CustomException.class)
        public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
            logger.error("CustomException 발생: {}", ex.getMessage(), ex);
      
            ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage());
            return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
        }
      
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
            logger.error("알 수 없는 예외 발생: {}", ex.getMessage(), ex);
      
            ErrorResponse errorResponse = new ErrorResponse("INTERNAL_ERROR", "서버 내부 오류가 발생했습니다.");
            return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    
  • 실제 리턴값
    1
    2
    3
    4
    5
    
    {
        "errorCode": "CUSTOM_001",
        "message": "테스트 예외 발생",
        "timestamp": "2025-10-10T18:15:00"
    }
    
This post is licensed under CC BY 4.0 by the author.