흐름도 그림
위의 그림을 보면서, 요청이 전달되는 과정에서 각 단계별 어떤 역할을 하는지 간단하게 이해하고 넘어가 보자.
Tomcat
- Client/ Web Server의 HTTP 요청을 HttpServletRequest 객체로 변환한다.
Filter
- 인증/인가를 Filter를 통해 검증한다. ex) 유효한 권한을 가진 사람의 요청인가?
만약 권한이 없는 사람이라면, 예외를 발생시킨다.
Filter에서 권한 오류가 발생한다면, 해당 오류는 @ControllerAdvice / @ExceptionHandler에서 잡을 수 없다. 위의 그림을 보면 알 수 있듯이, DispatcherServlet 앞단에 Filter가 위치하기 때문이다.
DispatcherServlet (Front Controller)
- HttpServletRequest 요청을 읽고, 적절한 핸들러에게 수행 역할을 위임한다. 누구에게 일을 시킬지 결정하는 팀 리더 같은 개발자라고 생각하면 편하다.
HandlerMapping
- HTTP URI를 읽고, 해당 URI와 일치하는 Controller 메서드가 있는지 탐색한다.
@PostMapping("/login-crew")
public Token crewLogin(@RequestBody LoginRequestDto loginRequestDto) {
return loginService.crewLogin(loginRequestDto);
}
보통 Controller 메서드 상단에 HttpMethod + uri를 명시하는데, HandlerMapping이 이와 일치하는 uri를 탐색한다. 이후, 일치하는 메서드가 존재한다면, HandlerMapping이 HandlerExecutionChain 객체를 DispatcherServlet에게 반환한다.
TMI : HandlerExecutionChain 객체 내부에는 Handler 객체(해당 Controller의 빈 객체 + 메서드 참조값), Handler 객체에 해당하는 Interceptor List가 들어있다.
package org.springframework.web.servlet;
public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
private final Object handler;
private final List<HandlerInterceptor> interceptorList = new ArrayList<>();
private int interceptorIndex = -1;
Interceptor
요청에 대해서 일치하는 uri가 존재한다면, 미리 정의해 둔 메서드(preHandle)를 실행시킨다. Filter와 비슷하지만 조금 더 세밀하게 제어할 수 있음. HandlerExecutionChain에 속한 List <Interceptor>를 순회하면서 preHandle 메서드를 실행한다.
예시코드(안 읽어도 상관없음)
@Component
public class CoachAuthorizeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
String authToken = request.getHeader("Authorization");
String token = resolveToken(authToken);
Claims claims = jwtProvider.getClaimsAndValidateToken(token);
String memberTypeStr = (String) claims.get("memberType");
MemberType memberType = MemberType.valueOf(memberTypeStr);
if (!memberType.name().equals("COACH")) {
throw new IllegalStateException("코치만 접근 가능합니다.");
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final CoachAuthorizeInterceptor coachAuthorizeInterceptor;
public WebConfig(CoachAuthorizeInterceptor coachAuthorizeInterceptor) {
this.loginArgumentHandler = loginArgumentHandler;
this.coachAuthorizeInterceptor = coachAuthorizeInterceptor;
this.crewAuthorizeInterceptor = crewAuthorizeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(coachAuthorizeInterceptor)
.addPathPatterns("/coaches/**")
.addPathPatterns("/coach-reservations/**");
}
}
HandlerAdapter
컨트롤러 실행을 명령한다. 또한 HTTP 요청으로부터 필요한 파라미터들을 추출하고, Controller 메서드의 파라미터 타입에 맞게 변환 및 바인딩한다. ex) @RequestParam, @RequestBody, @PathVariable 등의 애노테이션 처리
이후에 실행된 결괏값 (View)를 DispatcherServlet에게 다시 전달해 준다.
정리해 보니까 어때?
스프링 요청에 대한 흐름은 반드시 알고 있어야 한다고 생각한다. 뭐 몰라도 개발할 수는 있겠지만, 특정 부분에서 오류가 발생했을 때 원인을 파악하고 훨씬 더 빠르게 대응할 수 있다고 생각한다. 또, 흐름을 한 번 이해하면 개발하는 데 있어 조금 더 적절한 선택지를 고를 수 있다고 생각한다. 무작정 다른 사람들의 코드를 따라 하는 게 아니라, 나만의 기준을 세울 수 있다고 해야 할까...?
ex1) 로그를 Filter와 Interceptor 어디서 찍어야 할까?
ex2) 권한 검증을 Filter와 Interceptor 어디서 해야 할까?
스프링은 참 어렵다. 그래도 한 번 정리하니깐 요청/응답에 대해서 전체적인 감은 잡혀서 다행이다...!
잘못된 내용에 대한 피드백은 언제든지 환영입니다!
'Spring' 카테고리의 다른 글
Spring Core 꼬리 질문 해보기 (0) | 2025.04.28 |
---|---|
Spring Response/Request 어노테이션 (0) | 2025.04.16 |