기술과 산업/언어 및 프레임워크
Spring Framework 시리즈 15화 – 스프링 MVC의 예외 처리 전략과 @ExceptionHandler 사용법
B컷개발자
2025. 7. 18. 12:21
728x90
Spring MVC의 예외 처리 흐름과 함께 @ExceptionHandler, @ControllerAdvice, ResponseEntityExceptionHandler를 실제 사례와 함께 정리합니다. 실무에서 API 오류 응답을 체계적으로 처리하는 전략을 배웁니다.
예외는 반드시 발생한다 – 그래서 설계가 중요하다
API를 만들다 보면 잘못된 요청, 인증 실패, 리소스 없음 등 다양한 예외가 발생합니다.
문제는 예외가 발생했을 때 클라이언트에게 어떻게 응답할지 전략이 없으면 다음과 같은 일이 생긴다는 거죠.
- 500 Internal Server Error가 그대로 노출됨
- HTML 에러 페이지가 REST 응답에 노출됨
- 오류 메시지에 스택트레이스가 그대로 출력됨
이런 건 단순히 보기 안 좋을 뿐 아니라 보안 문제로도 이어질 수 있습니다.
그래서 스프링은 예외 처리 방식을 세분화해서 제공하고 있고, 개발자는 상황에 맞게 정리해둘 필요가 있습니다.
스프링 MVC 예외 처리 방식 – 크게 3단계로 나뉩니다
처리 방법적용 대상특징
| @ExceptionHandler | 개별 컨트롤러 | 특정 예외만 별도 처리 |
| @ControllerAdvice | 전역 컨트롤러 | 전역 예외 처리에 적합 |
| ResponseEntityExceptionHandler | 스프링 제공 추상 클래스 | 복잡한 구조에서 확장성 있게 처리 가능 |
1. @ExceptionHandler – 가장 직관적인 방법
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam(required = false) String name) {
if (name == null) {
throw new IllegalArgumentException("이름은 필수입니다.");
}
return "Hello " + name;
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity
.badRequest()
.body("요청 오류: " + e.getMessage());
}
}
- 같은 클래스 내에서 발생한 예외만 처리 가능
- 응답 형식도 직접 지정할 수 있어 REST 응답에 유용
2. @ControllerAdvice – 전역 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity
.badRequest()
.body("잘못된 요청입니다: " + e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleAll(Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("서버 내부 오류가 발생했습니다.");
}
}
- 모든 컨트롤러에서 발생한 예외를 한 곳에서 처리
- 공통 에러 포맷이 필요한 경우 유용
📌 @ControllerAdvice vs @RestControllerAdvice
→ 후자는 @ResponseBody가 기본 적용되어 JSON 응답용으로 주로 사용
3. ResponseEntityExceptionHandler 확장 – 복잡한 구조에 유리
Spring은 이미 ResponseEntityExceptionHandler라는 기본 클래스에서 몇 가지 예외를 처리하고 있습니다. 예를 들어:
- MethodArgumentNotValidException
- HttpRequestMethodNotSupportedException
이걸 상속받아 내 스타일대로 확장할 수 있습니다.
@ControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
String message = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body("검증 실패: " + message);
}
}
- Bean Validation 실패 시 자동 호출
- 응답 포맷을 커스터마이징할 수 있음
- 여러 컨트롤러에 걸쳐 적용 가능
실무 팁 – 에러 응답 포맷 정형화하자
많은 프로젝트에서는 아래와 같은 형태의 JSON 응답을 사용합니다.
{
"timestamp": "2025-07-18T12:34:56",
"status": 400,
"error": "Bad Request",
"message": "필수 파라미터가 없습니다",
"path": "/api/hello"
}
이런 포맷을 공통으로 만들려면 ErrorResponse 클래스를 만들어 @ControllerAdvice에서 일괄 반환하는 방식이 좋습니다.
@Data
@AllArgsConstructor
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAll(Exception e, HttpServletRequest request) {
ErrorResponse body = new ErrorResponse(
LocalDateTime.now(),
500,
"Internal Server Error",
e.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(500).body(body);
}
예외도 전략이다
- 예외는 언제나 발생합니다
- 사용자에게 예외 상황을 명확히 알릴 수 있어야 합니다
- 스택 트레이스를 그대로 노출하면 위험합니다
- 전역 처리 → 포맷 통일 → 유지보수 편리
예외를 잘 다루는 시스템은 결국 신뢰받는 시스템으로 이어집니다.
728x90