실제 기능 구현하기에 앞서 공통 응답, 에러 처리를 진행했다.
공통 에러 응답 형식은 {status, code, message}로 진행했다.
여태 협업 하며 코드별로 명확하게 에러 분기처리를 프론트에서 진행할 수 있었기에
문서화만 잘 된다면 좋은 방법이라고 생각한다.
public record ErrorResponse(
int status,
String code,
String message
) {
}
public enum Errorcode {
private final String code;
private final HttpStatus status;
private final String message;
}
불변 객체인 레코드를 활용해 ErrorResponse를 만들었고, 각각 들어가는 값들은 enum을 활용해 관리하였다.
따라서 이후 애플리케이션 단으로 에러가 필요할 때는 다음과 같이 던질 수 있게된다.
throw new BusinessException(ErrorCode.INVALID_USER);
스프링은 AOP라는 기술을 통해 공통 관심사를 분리하고 있다.
실패한 기술이라고도 말하기는 하지만, 비즈니스 로직에서 반복되는 공통 로직(에러, 트랜잭션)등을 분리해서
로직은 로직답게 유지하고 중복 코드를 줄인다는 점에 충분한 의의가 있다.
그리하여 우리는 @RestControllerAdvice라는 어노테이션이 전역적으로 동작함에 있어 굉장히 편리하게 에러처리를 할 수 있다.
Controller Advice :: Spring Framework
@ExceptionHandler, @InitBinder, and @ModelAttribute methods apply only to the @Controller class, or class hierarchy, in which they are declared. If, instead, they are declared in an @ControllerAdvice or @RestControllerAdvice class, then they apply to any c
docs.spring.io
일반적으로 스프링 MVC 패턴에 의해 http 요청이 들어오면 다음과 같은 순서로 동작한다.
Client -> Tomcat -> Servlet filter chain -> DispatcherServlet -> HandlerMapping -> HandlerAdapter
-> Service -> 로직 수행 -> Model and view 응답 생성 -> 응답 리턴
이때 Tomcat 같은 WAS는 HTTP 요청을 받아 HttpServletRequest, HttpServletResponse로 변환하고, 등록된 Servlet인 DispatcherServlet을 호출한다. 디스패처 서블릿부터 실질적인 Spring MVC 요청 처리가 시작된다고 볼 수 있다.
따라서 Spring Security 필터나 일반 Servlet Filter Chain 단에서 에러가 터지면 @RestControllerAdvice에서 에러를 잡지 못하는 경우가 있기 때문에 주의할 필요가 있다.
즉, @RestControllerAdvice는 보통 DispatcherServlet 내부에서 발생한 MVC 예외를 처리한다.
정상적으로 요청이 DispatcherServlet까지 들어오면,
DispatcherServlet은 HandlerMapping을 통해 어떤 컨트롤러가 요청을 처리할지 찾는다.
그 후 HandlerAdapter를 통해 해당 컨트롤러 메서드를 실행한다.
대부분 @RestController가 붙은 메서드를 사용하기 때문에 내부적으로는 RequestMappingHandlerAdapter가 실행한다고 보면 된다.
이 과정에서 HTTP 요청을 필요에 맞게 자바 객체로 파싱하는 과정이 일어난다.
예를 들어 @RequestBody를 통해 JSON 요청 body를 DTO로 변환할 때 HttpMessageConverter가 사용되고, Spring Boot 환경에서는 보통 Jackson이 JSON 변환을 담당한다. 또한 @Valid가 붙어 있다면 DTO 필드에 선언된 Bean Validation annotation을 기반으로 검증도 수행된다.
그렇다면 실제적으로 예외가 발생했을 때 어떻게 처리될까?
컨트롤러나 서비스 로직 수행 중 예외가 발생했다면, 역순으로 DispatcherServlet 방향으로 예외가 전파된다.
여기서 직접 예외를 처리하는 것이 아니라 스프링 빈으로 등록되어있는 HandlerExceptionResolver에 처리를 위임한다.
전략 패턴이 여기서 사용되어있으며, 내부적으로 실제 사용되는 Resolver는 다음과 같다.
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
우리가 많이 사용하는 @ExceptionHanler는 ExceptionHandlerExceptionResolver가 처리한다.
즉 GlobalExceptionHandler는 예외가 발생했을 때 갑자기 실행되는 것이 아니고 순차적으로 실행된다.
DispatcherServlet →
HandlerExceptionResolver →
ExceptionHandlerExceptionResolver →
@RestControllerAdvice의 @ExceptionHandler
서비스 로직에서 throw new BusinessException()을 했을 때,
컨트롤러 방향으로 전파되고, 별도로 try-catch 수행 로직이 없다면 서블릿단까지 다시 올라간다.
여기서 예외를 발생했음을 감지하여 HandlerExceptionResolver들에게 예외 처리가 가능한지 확인 후,
ExceptionHandlerExceptionResolver가 @RestControllerAdvice가 붙은 클래스를 확인하고
그 내부에서 BusinessException을 처리하는 메서드를 찾아 수행해 우리가 알고 있는 에러 로직이 작동하는 것이다.
여기서도 책임이 잘 분리되어 있음을 볼 수 있다.
서비스는 ErrorCode에 Enum으로 등록된 비즈니스 상황만 던지면
에러 응답으로 변환하는 책임은 GlobalExceptionHandler가 수행하고 있다.
여기에 서비스 로직에서 던지는 예외 이외에도 Bean Validaiton 기반 어노테이션 검증 메소드까지 추가해서
서비스 로직 이전 단의 예외까지 처리하였다.
또한 개발하면서 서버 차원에서 인지하지 못해 캐치하지 못한 예외가 때때로 500으로 덮어쓰일 때가 많은데,
이 기반은 차차 잡아가도록 노력하겠다.
'개발 일기' 카테고리의 다른 글
| 밴드 플랫폼 서버 개발 일지 - 협업 툴로서 Postman 도입 및 작업 환경 (0) | 2026.06.25 |
|---|---|
| 밴드 플랫폼 서버 개발 일지 - LazyCodex 도입 (1) | 2026.06.25 |
| 밴드 플랫폼 서버 개발 일지 6/10 - 프로젝트 세팅 (0) | 2026.06.11 |