⚠️Spring 예외 처리 완전 정복 - @ControllerAdvice, @ExceptionHandler, 그리고 실무 예외 전략
2025. 6. 18. 20:58ㆍFramework/Spring
✅1. 예외 처리의 필요성
소프트웨어는 실패하지 않는 것이 아니라, 실패를 어떻게 다루느냐가 중요하다.
Spring 애플리케이션에서는 예외를 제대로 처리하지 않으면
- 클라이언트에게 500 에러가 그대로 노출되거나
- 사용자 친화적인 메시지를 전달하지 못하고
- 서비스 전체가 중단될 수 있음
-> 그래서 예외 처리를 계층화하고 구조화하는 전략이 필요함
2. 예외 처리 전략의 계층화
| 계층 | 처리 방식 |
| Controller | @ExceptionHandler, @ControllerAdivce |
| Service | 커스텀 예외 발생 (throw new..) |
| Global | 필터/인터셉터에서의 처리, 예외 로깅 |
🎯3. Spring MVC에서 예외가 발생했을 때 흐름
- 컨트롤러/서비스에서 예외 발생
- HandlerExceptionResolver가 예외를 가로채 처리
- 등록된 @ExceptionHandler, @ControllerAdivce 우선 탐색
- 적젉한 HTTP 상태코드 + 메시지 변환
4. 핵심 어노테이션 : @ExceptionHandler
✅ 특정 예외 처리 전용 메서드 정의
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // NotFoundException 발생 가능
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<String> handleNotFound(NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
}
- 컨트롤러 내에서 발생한 예외 중 NotFoundException이 잡힘
- 응답 코드와 메시지를 커스터마이징 가능
5. 전역 예외 처리기 : @ControllerAdivce
✅ 모든 컨트롤러의 예외를 한 곳에서 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobal(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "서버 오류 발생"));
}
}
- @RestControllerAdvice = @ControllerAdvice + @Responsebody
- 공통 예외 응답 구조 ErrorResponse 설계로 일관된 반환 가능
6. 커스텀 예외 설계
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
- 비즈니스 규칙에 따라 예외를 세분화하면 가독성과 유지보수성 향상
7. 실무에서 사용하는 공통 에러 응답 DTO
public class ErrorResponse {
private String code;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
this.timestamp = LocalDateTime.now();
}
}
- 클라이언트에 일관된 응답 제공
- Swagger/OpenAPI 문서화에도 유리
8. 실무 트러블슈팅 팁
| 문제 | 원인 | 해결책 |
| 500 Internal Server Error만 뜸 | 예외 잡히지 않음 | @ExceptionHandler(Exception.class) 추가 |
| 커스텀 예외인데 200 OK 전달됨 | ResponseEntity 없이 return | 항상 ResponseEntity<> 사용 |
| 예외 메시지가 노출됨 | 디버그용 메시지 그대로 노출 | ErrorResponse에 사용자 메시지 구분 |
| Validation 실패 시 JSON 응답 없음 | MethodArgumentNotValidException 처리 누락 | @Valid 예외 처리 추가 필요 |
9. @Valid 유효성 검사와 연계
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return ResponseEntity.badRequest().body(new ErrorResponse("VALIDATION_ERROR", errorMsg));
}
- @Valid + BindingResult 없이 바로 예외 잡는 방식
✅ 마무리 요약
| 항목 | 핵심 정리 |
| 처리 방식 | @ExceptionHandler, @ControllerAdvice 활용 |
| 설계 원칙 | 커스텀 예외 + 공통 응답 DTO 구성 |
| 실무 전략 | 계층화된 예외 분리 + 글로벌 핸들러 구성 |
🔜 다음 포스팅에서는 Spring Security & JWT 인증 흐름 완전 정복 -> 로그인 인증부터 토큰 발급, 필터 구조, 실무 적용까지 알아보고자 한다.
'Framework > Spring' 카테고리의 다른 글
| Spring Boot 자동 설정 원리 완전 정복 - @EnableAutoConfiguration 내부에서 벌어지는 일 (0) | 2025.06.25 |
|---|---|
| 🔐 Spring Security & JWT 인증 흐름 완전 정복 - 로그인 인증부터 토큰 발급, 필터 구조 (0) | 2025.06.24 |
| 🔄 Spring의 @Transactional 완전 정복 - 개념, 원리, 동작 방식, 트러블 슈팅까지 (0) | 2025.06.11 |
| Spring AOP 완전 정복 - 핵심 로직과 횡단 관심사의 완전 분리 (0) | 2025.06.05 |
| Spring의 DI 방식 완전 정복 - 생성자 / 필드 / 세터 주입, 그리고 언제 어떻게 써야 하는가? (0) | 2025.05.13 |