개발/Spring
[Spring] 예외처리
함수형 인간
2025. 3. 27. 17:33
1. 기본 try-catch 블록
- 가장 기본적인 자바의 예외 처리 방식.
- 메소드 내에서 특정 로직 수행 중 발생할 수 있는 예외를 직접 잡아서 처리.
- 단점: 컨트롤러 메소드마다 유사한 try-catch 코드가 반복될 수 있어 코드가 지저분해지고 유지보수가 어려워질 수 있다.
2. 스프링의 데이터 접근 예외(Data Access Exception) 계층
- 스프링은 데이터 접근 기술(JDBC, JPA, Hibernate 등)에 상관없이 일관된 방식으로 예외를 처리할 수 있도록 DataAccessException이라는 런타임 예외 계층을 제공.
- 각 데이터 접근 기술에서 발생하는 고유한 예외(예: SQLException)를 스프링이 DataAccessException의 하위 예외 (예: DataIntegrityViolationException, DataAccessResourceFailureException 등)로 변환하여 던져준다.
- 장점: 개발자는 특정 데이터 접근 기술에 종속되지 않고, 스프링이 제공하는 추상화된 예외를 사용하여 데이터 관련 오류를 처리할 수 있다. 서비스 계층 등에서 기술 독립적인 예외 처리가 가능해진다.
3. @ExceptionHandler 애노테이션 (컨트롤러 레벨)
- 특정 컨트롤러 클래스 내에서 발생하는 예외를 처리하는 메소드를 지정할 때 사용.
- 해당 컨트롤러 내에서 지정된 타입의 예외가 발생하면 @ExceptionHandler가 붙은 메소드가 호출됨.
- 장점: 컨트롤러 내에서 발생하는 특정 예외에 대한 처리를 해당 컨트롤러 내에 응집시킬 수 있다.
- 단점: 여러 컨트롤러에서 동일한 예외 처리 로직이 필요할 경우 코드 중복이 발생.
Java
@Controller
public class MyController {
@GetMapping("/some-resource")
public String getResource() {
if (/* some error condition */) {
throw new MyCustomException("Something went wrong!");
}
return "successView";
}
// MyCustomException 발생 시 이 메소드가 처리
@ExceptionHandler(MyCustomException.class)
public ResponseEntity<String> handleMyCustomException(MyCustomException ex) {
// 로깅, 오류 응답 생성 등
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}
// 다른 종류의 예외 처리
@ExceptionHandler(Exception.class)
public String handleGenericException(Exception ex, Model model) {
model.addAttribute("errorMessage", "An unexpected error occurred.");
return "errorView"; // 오류 페이지 뷰 이름 반환
}
}
4. @ControllerAdvice / @RestControllerAdvice (글로벌 레벨)
- 여러 컨트롤러에 걸쳐 전역적으로 발생하는 예외를 처리하기 위한 클래스를 지정할 때 사용.
- @ControllerAdvice는 주로 View를 반환하는 경우, @RestControllerAdvice는 @ControllerAdvice와 @ResponseBody가 합쳐진 것으로 주로 REST API에서 JSON/XML 등의 응답 본문을 반환하는 경우 사용됨.
- 이 클래스 내부에 @ExceptionHandler를 사용하여 특정 예외를 처리하는 메소드를 정의.
- 장점: 애플리케이션 전반의 예외 처리 로직을 한 곳에서 관리하여 중복을 제거하고 일관성을 유지할 수 있다. 특정 패키지나 특정 애노테이션이 붙은 컨트롤러만 대상으로 지정할 수도 있다.
- 가장 권장되는 방식 중 하나.
Java
@RestControllerAdvice // 또는 @ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponse response = new ErrorResponse("INVALID_INPUT", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // HTTP 상태 코드를 직접 지정할 수도 있음
public ErrorResponse handleNoSuchElementException(NoSuchElementException ex) {
return new ErrorResponse("RESOURCE_NOT_FOUND", ex.getMessage());
}
// 처리되지 않은 모든 예외를 처리 (가장 마지막에 위치시키는 것이 좋음)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
// 중요한: 실제 운영 환경에서는 보안 상 스택 트레이스 전체를 노출하지 않도록 주의
ErrorResponse response = new ErrorResponse("INTERNAL_SERVER_ERROR", "An unexpected error occurred.");
// 로깅은 필수!
log.error("Unhandled exception occurred:", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
// 필요에 따라 ErrorResponse DTO 정의
@Getter
@RequiredArgsConstructor
static class ErrorResponse {
private final String code;
private final String message;
}
}
5. HandlerExceptionResolver 인터페이스
- 스프링 MVC에서 예외 처리를 위한 핵심 전략 인터페이스.
- 컨트롤러(핸들러) 실행 중 발생한 예외를 받아 처리하고, 그 결과로 ModelAndView (오류 페이지 렌더링), HttpServletResponse 직접 조작 (상태 코드 설정 등), 또는 null (다른 Resolver에게 처리 위임) 등을 반환할 수 있다.
- 스프링은 기본적으로 여러 HandlerExceptionResolver 구현체를 등록하여 사용.
- ExceptionHandlerExceptionResolver: @ExceptionHandler 애노테이션을 처리. (가장 높은 우선순위)
- ResponseStatusExceptionResolver: @ResponseStatus 애노테이션이 달린 예외나 ResponseStatusException을 처리하여 HTTP 상태 코드를 설정.
- DefaultHandlerExceptionResolver: 스프링 내부의 특정 예외를 HTTP 상태 코드로 변환. (예: MissingServletRequestParameterException -> 400 Bad Request)
- 개발자가 직접 HandlerExceptionResolver를 구현하여 커스텀 예외 처리 로직을 추가할 수도 있지만, 보통은 @ControllerAdvice를 사용하는 것이 더 간편하고 일반적.
6. @ResponseStatus 애노테이션
- 사용자 정의 예외 클래스에 이 애노테이션을 붙이면, 해당 예외가 발생했을 때 지정된 HTTP 상태 코드와 이유메시지를 응답에 설정.
- ResponseStatusExceptionResolver가 이 애노테이션을 처리.
- @ExceptionHandler 메소드에도 사용하여 해당 메소드가 처리하는 예외에 대한 기본 응답 상태를 지정할 수 있다.
Java
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "요청한 리소스를 찾을 수 없습니다.")
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
7. 스프링 부트의 기본 오류 처리 (BasicErrorController)
- 스프링 부트는 별도의 예외 처리를 설정하지 않았을 경우를 대비해 기본적인 오류 처리 기능을 자동 구성.
- BasicErrorController가 /error 경로로의 요청을 처리.
- 요청의 Accept 헤더에 따라 HTML 오류 페이지(주로 src/main/resources/templates/error.html 등)를 보여주거나, JSON 형태의 오류 정보를 응답.
- application.properties 또는 application.yml에서 server.error.whitelabel.enabled=false 등으로 일부 동작을 제어하거나, ErrorAttributes 빈을 커스터마이징하여 오류 응답 내용을 변경할 수 있다.
예외 처리 전략 및 Best Practice
- 전역 처리 우선: @ControllerAdvice 또는 @RestControllerAdvice를 사용하여 애플리케이션 전반의 예외 처리 로직을 중앙에서 관리하는 것이 좋다. 코드 중복을 줄이고 일관성을 확보할 수 있다.
- 구체적인 예외 먼저 처리: @ExceptionHandler 메소드를 정의할 때는 구체적인 예외 타입을 먼저 처리하고, 그 부모 타입이나 Exception과 같은 일반적인 예외는 나중에 처리하도록 순서를 고려.
- 커스텀 예외 활용: 비즈니스 로직 상 의미있는 예외 상황에 대해서는 명확한 이름의 커스텀 예외 클래스를 정의하여 사용하는 것이 가독성과 유지보수 측면에서 유리.
- 적절한 HTTP 상태 코드 반환: REST API의 경우, 예외 상황에 맞는 HTTP 상태 코드(4xx, 5xx 등)를 반환하는 것이 중요. @ResponseStatus, ResponseEntity 등을 활용.
- 사용자 친화적인 오류 메시지: 최종 사용자에게 노출되는 오류 메시지는 이해하기 쉽고 문제 해결에 도움이 되는 정보를 제공해야 한다. 상세한 기술 정보나 스택 트레이스는 사용자에게 직접 노출하지 않도록 주의.
- 충분한 로깅: 예외 발생 시 원인 분석을 위해 반드시 로그를 남겨야 한다. 스택 트레이스 전체와 함께 예외 발생 시점의 주요 파라미터, 사용자 정보 등 컨텍스트 정보를 포함하는 것이 좋다. (Logback, Log4j2 등 로깅 프레임워크 사용)
- 트랜잭션 고려: 서비스 계층 등에서 예외 발생 시 트랜잭션 롤백 처리가 제대로 이루어지는지 확인해야 한다. 스프링의 @Transactional은 기본적으로 런타임 예외 발생 시 롤백을 수행.