개요
이전 포스팅에 이어서 바로 API 요청을 Spring Boot Framework에서 어떻게 처리하는지에 대한 내용을 포스팅 할 예정이다.
나름 Spring을 조금 공부했다고 생각했어는데, 정작 이런 흐름을 알게 된지는 얼마 안됐다.
본론으로 들어가기 전에...
바로 본론으로 들어가기 전에 다음 그림과 같은 흐름대로 API가 처리된다고 보면 될 것같다.
1. Filter
Filter는 사용자가 서버로 전송한 정보를 가장 먼저 획득하는 부분입니다. 일반적으로 Filter에서는 전처리, 인증과 인가와 같은 작업을 수행합니다. 흔히 우리가 로그인하는 과정에서 JWT를 사용하면, Access Token과 같은 토큰을 발급받습니다. 이후 마이 페이지와 같은 사용자별로 다른 정보를 받기 위한 요청을 할 때, Request 헤더의 Authorization 헤더에 Access Token을 넣어 요청을 보냅니다.
Filter에서는 이러한 토큰이 존재하는지를 검증하고, 해당 토큰의 유효기간을 확인합니다. 유효기간이 끝난 경우, Refresh Token을 사용하여 새로운 Access Token을 발급받는 등의 전처리 작업과 인증, 인가 작업을 처리하게 됩니다.
추가적으로, Filter의 역할은 다음과 같습니다:
- 로깅 및 감사: 모든 요청과 응답을 로깅하여 감사 로그를 남깁니다.
- 데이터 압축 및 변환: 요청 데이터의 압축 해제 또는 응답 데이터의 압축을 수행합니다.
- CORS 설정: Cross-Origin Resource Sharing 설정을 통해 도메인 간의 요청을 허용하거나 차단합니다.
- 캐싱 헤더 설정: 응답에 캐싱 관련 헤더를 추가하여 클라이언트 측에서 캐싱을 제어합니다.
Filter는 DispatcherServlet 이전에 동작하며, 서블릿 컨테이너 수준에서 요청과 응답을 전역적으로 처리할 수 있는 강력한 도구입니다. 이를 통해 애플리케이션의 보안과 성능을 향상시킬 수 있습니다.
2. Dispatcher Servlet
Dispatcher Servlet은 Tomcat WAS에 포함된 서블릿으로, 사용자의 요청을 전체적으로 핸들링하며, 요청을 적절한 Request Handler로 매핑합니다. 사용자가 요청한 경로에 해당하는 Handler가 존재하지 않을 경우, 404 Not Found를 반환합니다.
추가로, Dispatcher Servlet은 다음과 같은 역할도 수행합니다:
- 핸들러 어댑터(Handler Adapter): 매핑된 핸들러를 실행하기 위해 적절한 HandlerAdapter를 찾아 요청을 처리합니다.
- 뷰 리졸버(View Resolver): 핸들러가 반환한 논리적 뷰 이름을 실제 뷰로 변환하여 응답을 생성합니다.
- 예외 처리: 요청 처리 중 발생한 예외를 전역적으로 처리하고, 적절한 응답을 생성합니다.
이를 통해 Spring MVC 애플리케이션의 핵심 역할을 수행하며, 요청을 효율적으로 관리하고 적절한 응답을 제공합니다.
3. Interceptor
Interceptor는 Dispatcher Servlet에서 전달받은 데이터를 정의된 인터셉터를 통해서 전처리, 후처리, 반환 후 처리를 수행할 수 있습니다. 또한, Interceptor라는 단어의 뜻 그대로, "가로채다"라는 의미를 가지고 있습니다. Interceptor를 통해서 특정한 컨트롤러의 전 후로 요청과 응답을 가로채서 원하는 동작을 수행할 수 있게 할 수 있습니다.
// Interceptor Configuration 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor)
.addPathPatterns("/**") // 모든 경로에 대해 인터셉터 적용
.excludePathPatterns("/login", "/logout"); // 특정 경로는 제외
}
}
위의 코드와 같은 방식으로 특정 경로로 들어오는 요청에만 적용하거나, 모든 경로에 대해서 데이터의 전후처리와 사후처리를 할 수 있습니다.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 요청 처리 전 로직
System.out.println("Pre Handle method is Calling");
return true; // true를 반환하면 다음 인터셉터나 컨트롤러로 진행, false 반환 시 요청 처리 중단
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 요청 처리 후, 뷰 렌더링 전에 호출
System.out.println("Post Handle method is Calling");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 요청 처리 완료 후, 뷰 렌더링 후에 호출
System.out.println("Request and Response is completed");
}
위의 코드에서 볼 수 있다시피, preHandle로 요청이 처리되기 전에 전처리를 수행할 수 있고, postHandle을 통해서 요청이 처리된 후에 후처리를 수행할 수 있습니다.
afterComplete는 postHandle과 유사하지만, 명확한 차이점이 존재합니다.
postHandle은 사용자의 요청을 내부적인 비즈로직에 의해서 처리한 후 응답을 반환하기 전에 실행되고,
afterComplete는 사용자에게 요청에 대한 응답을 반환한 이후에 처리할 수 있습니다.
이러한 차이점으로 인해서 postHandle로는 JWT Token에 salt 또는 pepper를 적용하는 로직을 수행할 수 있을 것 같고, afterComplete는 로깅을 수행하는 것에 적합하다는 것을 알 수 있습니다.
4. 궁금증 - Filter & Interceptor VS Controller
혹자는 아래와 같은 궁금증을 품을 수 있다고 생각합니다.
왜 굳이 Filter랑 Interceptor를 사용해야 하지?
컨트롤러단에서도 헤더의 값과 데이터를 전/후 처리할 수 있지 않나?
엄밀히 말해서, 컨트롤러 단에서도 헤더 값 인증과 인가, 데이터 전후 처리 작업을 하는 것은 가능합니다. 그러나 Filter에서 토큰을 검사하는 것이 더욱 효율적입니다. 또한, 객체 지향 언어에서 하나의 구조에 여러 책임을 부여하는 것은 옳지 않기 때문에 컨트롤러단에서 이러한 작업을 처리하는 것은 지양하는 것이 좋습니다.
컨트롤러단에서 처리하게 될 경우, 중복되는 코드가 매우 많아지게 됩니다. 예를 들어, 한 API가 사용자로부터 토큰을 받아서 유효성을 검증한다고 가정해 봅시다. 만약 API가 10개, 100개, 1000개가 된다면, 이러한 중복되는 코드가 1000개 이상이 될 것입니다. 이는 유지보수 측면에서 매우 비효율적입니다.
이런 측면에서 볼 때, 필터는 필터의 역할을, 컨트롤러는 컨트롤러의 역할만을 수행하는 것이 바람직하다고 생각합니다.
5. AOP
AOP는 Aspect-Origin Programming의 약자로 관점 지향 프로그래밍의 줄임말입니다.
AOP는 모든 비즈니스 로직의 공통적인 관심사를 하나의 개념으로 추출하여, 중복되는 코드의 양을 줄이고, 더욱 쉽게 기능을 확장할 수 있도록 합니다. 일반적으로 스프링 프로젝트의 전역적인 범위에서 로깅을 수행하거나, 만약 결제시스템의 경우에는 Transaction을 제어하고 Lock을 관리하는 측면을 하나의 개념으로 분리하여, 필요한 부분에 쉽게 적용할 수 있습니다.
이렇게 사용자의 입력이 인증인가 과정을 거치고 Dispatcher Servlet을 통해 Mapping되며, Interceptor에 의해 전후처리 및 사후처리를 진행하고 AOP를 통해서 웹 애플리케이션 서비스의 공통적인 관심사를 효율적으로 적용할 수 있습니다.
6. 이후의 과정
이후에는 기존에 알고있던 Controller, Service, Model, Persistence Context를 통해서 비즈니스 로직을 적용하고 내부의 로직에 따라서 영속적 컨텍스트에 포함됩니다. 여기서 영속성 컨텍스트라 함은 SQL Mapper 혹스 Object Realtive Mapper를 통해서 DB에 Entity를 저장하는 순간을 의미합니다. 사용자의 입력이 내부적 로직에 따라 저장되면 해당 요청이 끝나더라도 사라지지 않는 하나의 객체로서 존재하기 때문에 영속성 컨텍스트라고 합니다.
따라서 사용자가 전달한 요청 API가 처리되는 흐름은 1번 Filter부터 시작해서 4번까지 수행되고, 5번부터는 기존에 알고있던 Controller, Service, Repository 흐름대로 진행됩니다.
정리
스프링부트를 사용하고나서 무작정 Controller, Service, Repository, JPA를 사용해서 개발할 때는 놓치기 쉬운 부분이라고 생각합니다.
저도 개발을 시작하고 꽤 시간이 흘렀음에도 최근에 알게 된 내용이라 흥미로운 부분들이 많았고,
이런 부분들을 알고만 있어도 나중에 '아 이런 로직이 추가되면 되겠구나!' 하는 순간이 찾아올 거라고 생각해서 이런 포스팅을 하게 됐습니다.
'Backend > Framework' 카테고리의 다른 글
[Spring Boot - Trouble Shooting] Bean 타입이 달라도 Bean 주입이 안 될 수 있다 (1) | 2024.10.24 |
---|---|
[Spring Boot] Spring Framework의 구성 요소와 배경 (0) | 2024.07.09 |
[Springboot] AWS S3 with Spring Boot3 (1) | 2024.04.29 |
[Springboot] 로그인 구현 & JWT (2) - 로그인 구현 (1) | 2023.11.15 |
[Springboot] 로그인 구현 & JWT (1) - JWT 개념 (0) | 2023.11.09 |