Intro
안녕하세요! 진행하고 있는 언어교환 플랫폼 프로젝트 Hello-World에 로그인 여부를 확인할 수 있는 기능을 추가했습니다. 세션을 활용한 로그인 방식을 구현했기 때문에 그동안 매번 Argument Resolver와 annotation의 조합으로 세션에서 현재 로그인 중인 사용자에 대한 정보를 주입해주었는데요. 현재 로그인 한 사용자들의 아이디를 주입해주는 부분은 반복 코드의 고리에서 탈출 시킬 수 있었으나, 로그인 여부 확인기능의 경우에는 여전히 대부분의 메소드에서 중복으로 나타나고 있었습니다.
로그인 여부 확인 기능은 핵심로직이라기 보다는 핵심로직을 수행하도록 도와주는 부가로직에 가깝다는 성격을 지니고 있습니다. 그래서 처음에는 무작정 스프링에서 공통되는 부가로직을 분리하는데 대표적으로 활용되는 AOP를 활용하고자 했습니다. 그러다 혹시 같은 기능을 제공하면서도 좀 더 나은 선택지가 있을까 하면서 조금 더 조사를 하게 되었습니다. 그렇게 찾아낸 것이 AOP 외에도 바로 Filter, Interceptor가 있다는 것이었습니다.
개발 공부와 프로젝트를 진행하면서 제가 목표하고 있는 부분은 구현도 구현이지만 '여러 선택지가 있다면 각 선택지를 먼저 제대로 비교해본 뒤 상황에 맞는 최선의 선택을 내리기' 입니다. 따라서 이번 포스팅에서는 로그인 구현 기능에 활용할 수 있는 세 가지 옵션인 Filter, Interceptor, AOP가 지니는 공통점과 차이점에 대해서 조금 더 알아보고 이를 바탕으로 어떤 결정을 내렸는지를 적어보도록 하겠습니다.
공통점
세 기능이 모두 가지고 있는 공통점은 어떠한 동작이 행해지기 지기 전 후에 사전처리와 사후처리를 담당하합니다. 그래서 셋 모두가 공통 부가 로직을 처리함에 있어서 코드 중복을 없애는 데 적절한 솔루션을 제공해 줄 수 있습니다. 그러나 각각이 적용되는 시점이 다르기 때문에 비슷하면서도 조금씩 다른 부분이 생기게 됩니다.
차이점
1. Filter
: 요청과 응답을 거르고 정제하는 역할

- 필터를 기능은 한 문장으로 정의하자면 정해진 범위 내에 공통 기능(보안, 로깅 등)을 추가하도록 돕는 것입니다.
- 그림에서도 볼 수 있듯이 여러 개의 필터가 chain의 형태로 연결될 수 있습니다.
- init( ) -> doFilter( ) -> destory( )의 과정으로 동작하는데 이때 doFilter( ) 메소드에서 요청(혹은 응답)을 필터처리하고 chain.doFilter(request, response)를 통해 필터 체인의 다음 필터로 넘깁니다.
- 체인의 마지막 필터인 경우 필터링 된 요청이 자원에 해당하는 부분으로 넘어갑니다.
- 응답을 필터링 하는 경우는 맨 마지막 필터부터 역순으로 적용이 됩니다.
- 요청이 자원(Servlet, JSP 등) 에 접근하기 전에 적용이 되며, 각 필터를 통과하는 요청을 주로 URL 패턴을 기준으로 설정합니다.
- 스프링 MVC의 기본구조는 Front Controller라고 불리는 Dispatcher Servlet이 요청을 전달 받은 뒤 각 컨트롤러에 해당 요청을 전달해주는 식으로 동작하기 때문에, 필터의 적용시점은 요청이 Dispatcher Servlet으로 전달되기 전까지로 볼 수 있습니다.
Filter 기본 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Slf4j
public class FilterPractice implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("only when I am created the first time, will I be printed.");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("I, otherwise, will be printed everytime a request passes through me.");
// 1. 체인 내에 다음 필터가 존재하는 경우 다음으로 넘어가거나
// 2. 없을 경우 바로 서블릿으로 넘어간다.
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("I will turn up only once when I'm sadly in no use at all and am about to be destroyed.");
}
}
|
cs |
2. Interceptor
: 요청을 처리 전/후로 가로채는 역할

- Interceptor는 해당 단어가 의미하는 것처럼 요청을 가로채는 역할을 합니다.
- 다음의 3가지 시점에서 요청을 가로채기할 수 있습니다.
1) 컨트롤러에 요청을 전달하기 전 (preHandler)
2) 컨트롤러에서 요청을 처리하고 난 후 (postHandler) 그리고 View로 처리된 요청을 전달하기 직전
(아직 화면 처리가 되지 않은 상태이므로 필요한 경우 메소드 실행 이후 추가적인 작업이 가능합니다.)
3) 모든 요청을 처리한 뒤 View가 렌더링 되기 직전 (afterCompletion)
- DispatcherServlet을 통과한 뒤 작업이 이루어지므로 스프링 컨텍스트의 내부에서 Controller에 관한 요청과 이에 대한 응답을 처리합니다.
- 스프링 컨텍스트의 내부에 존재하기 때문에 스프링의 빈에 자유롭게 접근할 수 있습니다.
(필터로도 아예 불가능하지는 않지만, 궁금해서 조사해보니 꽤나 많은 수고가 들어 배보다 배꼽이 더 큰 경우라는 것을 배우게 되었습니다.)
- 필터 체인과 마찬가지로 여러 개의 Interceptor를 연결하는 것도 가능합니다.
(이때 실행 순서는 preHandler의 경우 앞에서부터, postHandler와 afterCompletion의 경우는 역순으로 실행됩니다.)
Interceptor 기본 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Slf4j
public class InterceptorPractice implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("I'm just about to get into the controller.");
// true 를 리턴하는 경우는 실행 체인이 다음 인터셉터 혹은 핸들러 자체로 진행한다.
// false 일 경우 진행하지 않는다.
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("I've been dealt with by the controller already");
String session = request.getSession().getId();
log.info(session);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info(ex.getMessage());
}
}
|
cs |
3. AOP
: 객체 지향 프로그래밍 만으로는 처리가 불가능한 부분(공통 부가기능)을 한 곳에 모아서 처리할 수 있도록 보완해주는 역할을 합니다.

- 스프링 AOP는 메소드 앞에서 Proxy 패턴으로 실행되며 다음의 5가지의 적용지점을 가지고 있습니다.
1) @Before (메소드 실행 전)
2) @After (메소드 실행 후)
3) @AfterReturning (메소드가 결과 값을 리턴한 후)
3) @AfterThrowing (메소드가 예외를 던진 후)
5) @Around (메소드 실행 전 후)
- 때문에 인터셉터나 필터보다 메소드 전후의 지점에서 더 자유롭게 그리고 더 세밀하게 기능을 부여하는 것이 가능합니다.
(바이트 코드 자체를 조작하는 AspectJ를 활용하는 경우에 훨씬 더 많은 지점에서 부가기능을 부여할 수 있습니다.)
- URL 주소, 파라미터, 어노테이션 등 다양한 방법으로 대상을 지정할 수 있습니다.
- 비즈니스 계층에서의 공통 부가기능 적용은 필터나 인터셉터로는 불가능하므로 꼭 AOP를 활용해야 합니다.
(기본 예시 코드는 이전 포스트로 대체하겠습니다.)
스프링은 프록시 객체를 어떻게 만들까?
Intro 안녕하세요. 개발자가 되기를 꿈꾸며 학습에 임하고 있는 개발 꿈나무입니다. 지금까지는 공부했던 내용을 따로 정리해서 그냥 모아두기만 했는데요. 이제는 프로젝트를 진행하면서 궁금
soolow-and-sooteady.tistory.com
기본적인 내용 들은 모두 살펴보았는데, 그럼 무엇을 선택할까요?
하나씩 비교해가면서 보겠습니다.
Filter vs Interceptor
* 인터셉터를 활용해도 요청 자체를 바꿔서 전달해야 하는 경우가 아니면 필터가 구현할 수 있는 기능들은 기본적으로 구현할 수 있습니다.
(필터만 활용 가능한 경우를 알고 싶으시면 아래 다른 분이 작성해주신 게시글을 참조해주세요.)
(Spring)Filter와 Interceptor의 차이
서론Spring을 익힌지 얼마 되지 않았을 때, 회원 인증 로직을 구현할 일이 생겼었다. 그 인증을 구현하기 위해 Filter와 Interceptor를 조사했었다. 하지만 Filter와 Interceptor를 어떤 경우에 써야 좋은지
supawer0728.github.io
* 스프링 부트에서는 web.xml이 사라졌기 때문에 예전 처럼 따로 web.xml에 일일이 등록해 줄 필요 없이 자바 파일에 @Component를 붙여서 스프링 빈으로 등록이 가능하다는 것을 찾을 수 있었습니다. 하지만 인터셉터보다 설정이 조금 더 복잡했습니다.
* 필터에서 예외가 발생하는 경우는 Web Application에서 따로 처리해줘야 합니다.
- 필터에서 발생하는 DispatcherServlet의 바깥 쪽이므로 스프링이 제공하는 예외처리방법은 활용이 불가능 합니다.
- Tomcat을 사용하는 경우 <error-page>를 따로 선언해 주거나, try-catch 구문을 활용해 예외를 어떻게든 컨트롤러로 전달하고 나면 처리를 할 수 있다고 합니다.
* 인터셉터는 스프링 컨텍스트 내에 존재하므로 스프링이 지원하는 예외 처리를 모두 활용할 수 있습니다.
- 때문에 더 편하게 예외 처리가 가능합니다.
- 예외 처리가 잘 되어있는 프로그램은 돌발 상황에 대처하기가 좋고, 유지보수성도 더 높습니다.
- 현재 진행 중인 프로젝트는 예외 처리를 위해 @RestControllerAdvice를 활용한 ExceptionHandler를 활용하고 있습니다.
- 따라서, 인터셉터를 활용한다면 로그인 여부 확인 시 예외가 발생하는 경우(사용자가 로그인 하지 않았을 때)에는 별도의 예외 처리 대신 그냥 예외를 던지고 이것을 미리 있는 핸들러에 추가하기만 하면 됩니다.
이러한 부분을 고려해서 Interceptor의 손을 들어주었습니다.
Interceptor vs AOP
* AOP의 가장 큰 장점은 다양한 적용 지점에서 오는 유연성이라고 생각합니다.
- 하지만 현재 구현 중인 '로그인 여부 확인기능'은 컨트롤러에 요청을 전달하기 전에만 해당이 되므로 위의 장점은 고려대상에서 제외시킬 수 있습니다. 인터셉터와 AOP 모두 두 시점에 공통기능을 추가할 수 있기 때문입니다.
* AOP는 비즈니스 계층에도 부가기능을 부여할 수 있게 해줍니다.
- 지금 구현하고 있는 기능은 컨트롤러 계층에서 요청이 전달되는 시점에 해당하므로 이러한 부분도 크게 메리트가 없었습니다.
* 인터셉터는 그 자체로 파라미터에 세션을 담고 있습니다.
- 그래서 자체 파라미터를 활용해 세션에서 현재 사용자의 정보를 바로 가져올 수 있었습니다. 또한 이 때문에 인터셉터가 컨트롤러로 들어오는 요청을 다루는 데 좀 더 특화되어 있다는 생각이 들었습니다.
- AOP를 활용해도 세션을 주입받거나 미리 만들어 둔 세션에서 유저 정보 가져오기 등의 메소드를 활용해서 가져올 수 있겠지만 약간의 번거로움이 추가 되겠다고 판단했습니다.
* 인터셉터도 어노테이션을 활용해 적용 대상을 지정해줄 수 있었습니다.
- 처음에 조사했던 내용에서 다뤘던 부분이 인터셉터와 AOP의 차이점 중에 하나가 인터셉터는 필터처럼 URL을 활용해서 적용 대상을 선정한다는 것이었습니다.
- 현재 구현 중인 서비스를 보면 지금 당장은 컨트롤러 메소드 내에 URL별로 @CurrentUser 어노테이션과 Argument Resolver를 사용해서 현재 로그인 중인 사용자의 정보를 주입받는 부분이 잘 정리되어 있습니다. 그러나 차후에 로그인 전용/비 로그인 가능 메소드에 대한 URL 패턴이 섞여버린다면 대상을 설정하는데 꽤나 어려움을 겪을 것이라 판단했습니다.
- 그래서 메소드 단위로 적용대상을 지정할 수 있는 AOP로 전환하려던 찰나 인터셉터도 커스텀 어노테이션을 만들어 등록해주기만 하면 해당 어노테이션을 메소드 위에 붙여줌으로써 메소드별로 지정할 수 있다는 것을 알게 되었습니다.
이러한 부분을 고려해서 다시 한 번 Interceptor의 손을 들어주었습니다.
결론
Interceptor가 가장 적합한 것 같아서 이를 사용하기로 결정했습니다.
OUTRO
사실 셋 모두 적용되는 시점이 다르다는 것 말고는 제공하는 기능이 비슷비슷하기 때문에 하나가 강제되는 상황이 아니고서는 무엇을 선택해야 하는지 고르기가 매우 힘들었던 것 같습니다. 검색을 해봐도 큰 차이점이 두각되지 않았고 '컨트롤러로 들어오는 요청은 인터셉터가 더 낫다.' 정도밖에 못 찾아서 '왜?'라는 질문에 대한 답을 찾는 부분이 가장 어려웠습니다.
아직 배울 것들이 많기에 혹시 제가 고려할 사항들을 놓쳤더라도, '아 이 사람은 이러한 이유로 이렇게 결정하기로 마음먹었구나 정도'로만 봐주시고 댓글로 가르침을 주시면 감사하겠습니다. 그리고 혹여나 비슷한 상황에 계신 분들이 있다면 이 글이 조금이나마 그 분들에게 도움이 되었으면 하는 바람입니다.
References
https://supawer0728.github.io/2018/04/04/spring-filter-interceptor/
https://stackoverflow.com/questions/23381648/interceptors-or-filters
Project Link
github.com/f-lab-edu/Hello-World
f-lab-edu/Hello-World
언어교환 상대찾기 서비스. Contribute to f-lab-edu/Hello-World development by creating an account on GitHub.
github.com