[Spring] Interceptor, Resolver 객체 전달을 통한 리팩토링
- Overview
- Interceptor와 Resolver
- 기존 방식
- 리팩토링 진행 후 코드
- 정리
📌 1. Overview
최근 프로젝트를 개발하면서 로그인 인증 처리와 세션 연장 처리 부분을 Spring Security 사용하지 않고 구현해보는 과정을 진행하였습니다. Interceptor
를 사용하였고 인증된 UserSession 객체를 handler parameter에 전달하기 위해 HandlerMethodArgumentResolver
를 사용하였습니다.
문제는 각각의 interceptor와 Resolver 구현체에서 Redis 조회가 이루어져 비용측면에서 비효율적인 부분이 보였습니다. 이를 개선해본 내용들을 블로그에 글로 정리해봤습니다.
📌 2. Interceptor와 Resolver
Interceptor와 Resolver부분에 대한 자세한 내용은 단순 구글링해도 자료가 많이 있기 때문에 대략적으로 언급하겠습니다.
- Interceptor
DispatcherServlet
에서 handler mapping 이후 해당 handler로 접근하기 전에 Request, Response를 가로채는 역할 수행HandlerInterceptor
를 구현해서 적용
- Resolver
- handler에 들어오는 parameter를 조작하거나 객체를 전달하는데 사용
HandlerMethodArgumentResolver
를 구현해서 적용
Interceptor는 들어온 요청이 handler(쉽게 말해 Controller단)로 전달되기 전, 그리고 handler에서 return 함으로써 응답을 전달하려고 할 때 가로채서 원하는 처리를 수행하도록 지시할 수 있습니다. 프로젝트 진행하면서 주로 로그인 관련된 프로세스에 대해 Interceptor를 적용하였습니다.
Resolver는 로그인 이후에 패스워드 변경과 같은 인증된 사용자 정보가 고정적으로 parameter에 전달되어야 하는 상황에서 적용하였습니다.
📌 3. 기존 방식
public class AuthenticationInterceptor implements HandlerInterceptor { //... @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(!(handler instanceof HandlerMethod)){ return true; } Authentication authAnnotation = ((HandlerMethod)handler).getMethodAnnotation(Authentication.class); Optional<String> bearerValue = Optional.ofNullable(request.getHeader("Authorization")); if (authAnnotation != null) { bearerValue.orElseThrow(TokenNotExistedException::new); Optional<UserSessionDto> userSession = Optional.ofNullable(redisUtils.getSession(bearerValue.get().substring(7))); userSession.orElseThrow(() -> new AuthenticationException("로그인이 필요합니다.")); isSeller(userSession.get()); } return true; } private void isSeller(UserSessionDto userSession) { if (userSession.getSeller() == null || !userSession.getSeller()) { throw new AuthenticationException("판매자 권한이 필요합니다."); } } }
로그인 인증여부 확인과 판매자 권한 여부를 확인하는 Interceptor입니다. 여기서 RedisUtils
객체를 통해 Redis에 해당 key값으로 조회합니다.
public class SessionExtensionInterceptor implements HandlerInterceptor { //... @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Optional<String> bearerValue = Optional.ofNullable(request.getHeader("Authorization")); // bearerValue 존재하지 않을 때 세션 연장 로직 패스 bearerValue.ifPresent(bearer -> { UserSessionDto sessionDto = redisUtils.getSession(bearer.substring(7)); // 세션 만료시간 갱신 redisUtils.renewSessionExpire(sessionDto, bearer); }); return true; } }
요청을 할 때마다 로그인 인증된 사용자의 세션 만료시간을 연장처리해주는 Interceptor입니다. 여기서도 RedisUtils
를 통해 Redis에 해당 UserSession 데이터를 조회합니다.
public class SecurityUserResolver implements HandlerMethodArgumentResolver { //... @Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.hasParameterAnnotation(LoginUser.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String bearerValue = webRequest.getHeader("Authorization"); if (bearerValue != null) { return redisUtils.getSession(bearerValue.substring(7)); } return null; } }
Resolver에서도 Redis에서 해당 인증된 사용자 정보 객체를 조회합니다.
여기서 발견할 수 있는 문제점은 다음과 같습니다.
- 매 요청마다 인증된 사용자 정보 데이터라는 같은 객체를 3번이나 불필요하게 Redis에서 가져옵니다.
@LoginUser
를 handler parameter에 적용하기 위해@Authentication
도 해당 handler method에 설정해야 합니다.
이 부분을 하나하나 해결해보겠습니다.
📌 4. 리팩토링 진행 후 코드
불필요한 Redis 조회부분 제거
// SessionExtensionInterceptor.java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Optional<String> bearerValue = Optional.ofNullable(request.getHeader("Authorization")); bearerValue.ifPresent(bearer -> { UserSessionDto userSession = redisAuthUtils.getSession(bearer.substring(7)); // 세션 만료시간 갱신 redisAuthUtils.renewSessionExpire(userSession, bearer.substring(7)); // interceptor 전달을 위한 처리 request.setAttribute("authSession", userSession); }); return true; }
세션 만료 연장 처리하는 Interceptor를 첫 시작점으로 설정했습니다. 여기서 Redis 조회를 통해 인증된 사용자 정보를 조회합니다. 그리고 request에 해당 객체를 담아서 다음 Interceptor에 전달하게 됩니다.
// AuthenticationInterceptor.java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 모든 handler가 method 타입이 아닐수도 있다. if(!(handler instanceof HandlerMethod)){ return true; } Authentication authAnnotation = ((HandlerMethod)handler).getMethodAnnotation(Authentication.class); Optional<UserSessionDto> userSession = Optional.ofNullable((UserSessionDto) request.getAttribute("authSession")); if (authAnnotation != null) { userSession.orElseThrow(() -> new AuthenticationException("로그인이 필요합니다.")); isSeller(userSession.get()); } return true; }
앞선 Interceptor에서 전달해준 사용자 정보를 가져와서 @Authentication
에 대한 로그인 인증 여부, 판매자 권한 여부 처리를 하게 됩니다. 이렇게 함으로써 한 번의 Redis 조회를 통해 모든 처리를 할 수 있게 됐습니다.
@LoginUser
Custom Annotation 하나로 처리하기
// UserController.java @PatchMapping("/password") @Authentication public ResponseDto<?> changePassword(@RequestBody UserPasswordRequestDto resource, @LoginUser UserSessionDto userSessionDto) { userService.updatePassword(userSessionDto.getEmail(), resource.getPassword()); return new ResponseDto<>(0, "성공적으로 비밀번호를 변경하였습니다."); }
기존에는 @LoginUser
를 parameter에 적용하기 위해서 handler method에 @Authentication
을 같이 적용해야 했습니다. @LoginUser
를 처리해주는 Resolver에는 로그인 인증 여부 처리에 대한 로직이 없기 때문에 해당 로직을 처리해주는 @Authentication
이 있어야 제대로 동작할 것입니다.
@LoginUser
하나만으로 인증 여부 처리까지 할 수 있도록 로직을 추가하였습니다.
// SecurityUserResolver.java @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); return Optional.ofNullable(request.getAttribute("authSession")) .orElseThrow(() -> new AuthenticationException("로그인이 필요합니다.")); }
또한 여기서도 request를 통해 Interceptor에서 정의한 인증된 사용자 정보를 가져옴으로써 불필요한 Redis 조회를 줄일 수 있었습니다.
📌 5. 정리
- Filter, Interceptor, Resolver 등 클라이언트 요청 후 handler에 도달하기 까지의 과정 중에 어떠한 로직 처리가 필요할 때 사용할 수 있는 여러 방법들이 존재
- Filter → DispatcherServlet → Interceptor → Resolver 순으로 진행되기에 로직의 성격에 따라 어디에 적용해야 할지 고민해야함
- DB와 비슷하게 Redis access를 최대한 줄이는 방식으로 로직을 작성하면 효율적인 애플리케이션 운영이 가능
위의 예시 코드는 부족한 부분이 많기 때문에 참고용으로 보시면 될 것 같습니다.
틀린 내용이 있을 수 있습니다. 이에 대한 코멘트 언제나 환영입니다!
'Spring' 카테고리의 다른 글
[Spring] 이벤트 처리를 위한 스케줄러 작업 조정(ShedLock 활용) (0) | 2021.09.22 |
---|---|
MyBatis insert 구문 사용시 mapping 주의 사항 (0) | 2021.09.07 |
[Spring] 인터페이스 특징과 ISP(인터페이스 분리 원칙)에 대한 정리 (0) | 2021.07.21 |
Outbox 패턴을 이용한 메일 발송 구현해보기 (0) | 2021.07.08 |
[OOP] 객체 지향 프로그래밍은 무엇일까 (0) | 2021.06.21 |
댓글
댓글(Github)
다른 글
-
[Spring] 이벤트 처리를 위한 스케줄러 작업 조정(ShedLock 활용)
[Spring] 이벤트 처리를 위한 스케줄러 작업 조정(ShedLock 활용)
2021.09.22사용 배경 문제 인식 - 1. outbox 테이블 전체 조회로 인한 문제 - 2. scale-out 상황에서 스케줄러 작업의 중복 실행의 가능성 문제 ShedLock을 위한 프로젝트 설정 ShedLock을 이용한 스케줄링 Lock 설정 - Scale-out 상황에서 스케줄러 중복 실행에 대한 해결 전체 조회 내용을 chunk로 나누어 작업 수행 - 처음에 작은 단위로 조회해보기 - 비동기 방식으로 메일 전송 작업하기 끝나지 않은 고민 - 작업을 꼭 수행해야하는 기능에 대해서는 어떻게 구현할 것인가 📌 1. 사용 배경 프로젝트를 진행하면서 패스워드 변경 요청을 위한 Email 전송 기능이 필요해졌습니다. Email은 spring boot starter mail 라이브러리를 사용하여 테스트용도로 개발을 진행… -
MyBatis insert 구문 사용시 mapping 주의 사항
MyBatis insert 구문 사용시 mapping 주의 사항
2021.09.07📌 MyBatis 사용함에 있어서 살짝의 아쉬움…. (그래도 좋다!) MyBatis를 사용하면서 SQL 구문과 객체 관점을 매핑해준다는 점에서 많은 장점이 있습니다. 지연 로딩 등의 여러 유용한 기능들도 제공해주기도 하고 JPA와 같이 사용하면서 성능 조절을 할 수 있다는 점도 장점이라고 할 수 있겠네요. 하지만 제가 사용하면서 느낀점은 개발할 때 Entity Class와 Mapper xml 설정 내용 둘다 유심히 잘 봐야한다는 점에서 번거로움이 있었습니다. 특히 xml 설정 내용은 철자 하나하나 정확하게 작성해야 하고 java 코드가 아니라 쌩 SQL 구문을 이용해야 한다는 점에서 살짝 불편함을 느꼈습니다. 그 중 제가 자주 에러를 마주했던 부분에 대해 기록하고자 합니다. 📌 기본적인 INSERT 쿼… -
[Spring] 인터페이스 특징과 ISP(인터페이스 분리 원칙)에 대한 정리
[Spring] 인터페이스 특징과 ISP(인터페이스 분리 원칙)에 대한 정리
2021.07.21Java 인터페이스의 특징 OOP의 인터페이스 분리 원칙 인터페이스 상속 정리 📌 1. Java 인터페이스 특징 Java 인터페이스는 다형성을 가장 잘 보여주는 존재입니다. public interface RemoteController { void volumeUp(); void volumeDown(); } 리모컨이라는 인터페이스를 가지고 간단한 예시를 들어보겠습니다. @Component public class SRemoteController implements RemoteController { @Override public void volumeUp() { // volume up 로직 System.out.println("S사 리모컨 볼륨을 1단계 높였습니다."); } @Override public void … -
Outbox 패턴을 이용한 메일 발송 구현해보기
Outbox 패턴을 이용한 메일 발송 구현해보기
2021.07.08Github Repo https://github.com/beaniejoy/resetpw-outbox-scheduler Overview Eventual Consistency & Strong Consistency At Least Once? Outbox 패턴 코드 구현 📌 1. Overview 프로젝트 개발 중에 비밀번호 초기화 관련한 메일 발송 기능을 구현하는 과정이 있었습니다. 여기서 개념도 익힐겸 Eventual Consistency 고려해서 ALO(At Least Once) 방식을 적용한 메일 발송 기능을 구현해봤습니다. 📌 2. Eventual Consistency? Strong Consistency? 위 두 개의 Consistency 개념은 Multiple Replicas of a Database …
댓글을 사용할 수 없습니다.