일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Atomic Type
- gc
- text
- reflection
- 동시성 문제
- MySQL
- 자바
- 백엔드
- Lock
- 스프링
- Locking Read
- 데이터 타입
- Varchar
- CAS
- 가비지 컬렉터
- jpa
- foreach
- iterable
- iterator
- MVCC
- Di
- Synchronized
- java
- db
- 가비지 컬렉션
- 동시성
- Today
- Total
과정을 즐기자
Spring에서 공통 로직을 처리하는 방법 - 로그인 방식으로 알아보기 본문
Spring을 사용하여 개발을 하다보면 공통 로직에 대한 처리에 대해 개발자가 구현하기 쉽도록 해놓은 것을 알 수 있습니다.
예를들면 AOP, Filter, Interceptor, ArgumentResolver 등이 있습니다. 이번 글에서는 로그인 인증과 관련된 부분에
대해 위와 같은 기술 중에서 어떤 것이 적합한 지 생각해보며 어떻게 처리를 할 지에 대해 작성해보겠습니다.
AOP
AOP는 Aspect Oriented Programming의 약자로 횡단에 걸쳐 계속 반복하여 사용하는 코드의 중복을 줄일 수 있는
좋은 방법입니다. 로그인 인증과 관련된 부분도 AOP로 충분히 처리가 가능합니다. 하지만 AOP는 굉장히 범용적입니다.
컨트롤러는 파라미터나 리턴 값이 일정하지 않고 HttpServletRequest, HttpServletResponse 객체도 얻기 어려워
웹 요청과 관련된 로직의 경우에는 Filter, Interceptor 와 같은 좀 더 명확한 목적을 가진 기술을 사용하는 것이 더 적합해
보입니다.
Filter
Filter는 웹 애플리케이션에서 요청과 응답을 가로채서 처리하는 역할을 합니다.
Filter는 Spring에서 지원하는 기술이 아니고 J2EE의 표준 스펙입니다.
위 그림과 같이 Filter는 스프링 컨테이너의 Dispatcher Servlet에 요청이 전달되기 전/후에 url 패턴에 맞는 모든 요청에
대한 부가 작업을 처리할 수 있습니다. 이는 스프링과 관련없이 전역적으로 처리해야 하는 작업에 적합합니다.
따라서 로그인 인증과 관련된 부분도 충분히 처리할 수 있습니다. 하지만 한 가지 큰 단점이 있는데 바로 Spring과 관련이
없는 기술이기 때문에 공통 예외 처리를 위한 ExceptionHandler를 사용할 수 없다는 점입니다.
로그인 인증 과정에서 많은 예외처리가 필요한데 이 부분을 사용할 수 없다는 것은 큰 단점으로 다가옵니다.
Interceptor
Interceptor는 Spring에서 제공하는 기술입니다. Interceptor는 Dispatcher Servlet이 Controller를 호출하기 전/후에
요청과 응답을 참조하거나 가공할 수 있는 기능을 제공합니다.
Filter와 다르게 Spring이 지원하는 ExceptionHandler를 통해 예외처리를 할 수 있습니다. 또한 Interceptor는
Filter 방식과 마찬가지로 HttpServletRequest, HttpServletResponse 객체를 다루기 쉬워 웹 관련 로직을 처리하기에
적합합니다. 이번 글의 뒷 부분에서는 Interceptor를 이용한 방식을 살펴보겠습니다.
참고로 DispatcherServlet이 스프링 컨테이너 안에 있지만 서블릿이며 Filter는 서블릿 컨테이너 안에 있지만
충분히 스프링 빈으로도 만들 수 있습니다.
Argument Resolver
어떠한 요청이 Servlet Filter, Dispatcher Servlet, Interceptor를 지나 Controller로 들어왔을 때 들어온 값으로부터
원하는 객체를 만들어내는 일을 Argument Resolver를 이용해서 할 수 있습니다.
Interceptor의 prehandle 메소드는 반환 값 true, false 여부에 따라 작업을 진행할 지 중단할 지를 결정합니다.
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
하지만 어떠한 요청으로부터 쿠키 값이나 헤더 값을 확인하여 원하는 객체를 만들어서 사용할 필요가 있을 수 있습니다.
이러한 경우에 Argument Resolver를 사용합니다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(
MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception;
}
HandlerMethodArgumentResolver 인터페이스를 구현하여 사용할 수 있습니다.
supportsParameter는 어떠한 파라미터에 적용할 지에 대한 설정을 할 수 있고 resolveArgument는 원하는 특정 객체를
반환하도록 할 수 있습니다.
로그인 방식에 적용해보기
ArgumentResolver 먼저 적용
먼저 파라미터에 적용할 @Login 어노테이션을 만들어줍니다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
다음으로 HandlerMethodArgumentResolver 인터페이스를 구현한 클래스를 생성합니다.
public class MemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(final MethodParameter parameter) {
boolean hasMemberSessionType = parameter.getParameterType().equals(MemberSession.class);
boolean hasLoginMemberAnnotation = parameter.hasParameterAnnotation(Login.class);
return hasMemberSessionType && hasLoginMemberAnnotation;
}
@Override
public Object resolveArgument(final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
// 로그인 인증 관련 예외처리, 사용자 정보로 원하는 객체 생성
return MemberSession;
}
}
이제 로그인 인증 후 원하는 객체를 반환하는 로직을 다음과 같이 작성할 수 있습니다.
@PatchMapping("/members")
public void updatePassword(@Login final MemberSession memberSession,
@RequestBody final MemberUpdate dto) {
memberService.update(memberSession.id(), dto);
}
한 가지 문제점
하지만 Argument Resolver만 사용할 경우 한 가지 문제점이 있습니다.
굳이 MemberSession 객체가 필요하지 않은 경우에도 로그인 인증이 필요하다면 아래와 같이 작성해줘야 합니다.
@GetMapping("/channels")
public ChannelResponses findAllChannels(@Login final MemberSession memberSession) {
return channelService.findAll();
}
Interceptor 추가하기
이러한 문제를 해결하기 위해 Interceptor와 Argument Resolver를 같이 사용해보겠습니다.
로그인 인증과 관련된 부분은 Interceptor에서 처리하고 원하는 객체를 생성하는 부분은 Argument Resolver가 처리
하도록 분리하겠습니다. 아래 코드 JWT AccessToken을 이용하여 사용자에 대한 정보를 가져오기 위한 코드입니다.
public class LoginInterceptor implements HandlerInterceptor {
...
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler) {
String accessToken = request.getHeader(ACCESS_TOKEN.value);
MemberSession memberSession = getMemberSessionFromToken(accessToken, request, response);
request.setAttribute(MEMBER_SESSION.value, memberSession);
return true;
}
...
Interceptor의 prehandle 메소드는 반환 값 true, false 여부에 따라 작업을 진행할 지 중단할 지만 결정합니다.
Argument Resolver와 같이 사용하기 위해 HttpServletRequest의 setAttribute를 이용해 특정 객체 정보를 넣을 수 있습니다.
이 다음에 Argument Resolver에서는 HttpServletRequest의 getAttribute를 통해 원하는 객체를 반환할 수 있습니다.
public class MemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(final MethodParameter parameter) {
boolean hasMemberSessionType = parameter.getParameterType().equals(MemberSession.class);
boolean hasLoginMemberAnnotation = parameter.hasParameterAnnotation(Login.class);
return hasMemberSessionType && hasLoginMemberAnnotation;
}
@Override
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
assert request != null;
return request.getAttribute(MEMBER_SESSION.value);
}
}
이렇게 함으로써 굳이 MemberSession 객체가 필요하지 않지만 로그인 인증만 필요한 부분에서는 불필요하게 작성하는
코드를 없앨 수 있습니다.
참고한 자료
'Spring' 카테고리의 다른 글
스프링 빈을 싱글톤으로 유지하는 방법과 이유 (0) | 2024.08.21 |
---|---|
Spring에서 멀티 쓰레드 비동기 프로그래밍 해보기 (2) | 2023.12.08 |
바이너리 데이터를 처리하는 방법 (0) | 2023.10.28 |
Spring과 Node.js 비교하기 (0) | 2023.08.26 |
내부 클래스를 스프링 빈으로 등록할 수 있을까? (0) | 2023.07.07 |