과정을 즐기자

Spring에서 공통 로직을 처리하는 방법 - 로그인 방식으로 알아보기 본문

Spring

Spring에서 공통 로직을 처리하는 방법 - 로그인 방식으로 알아보기

320Hwany 2023. 12. 23. 21:30

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] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)

Spring은 공통적으로 여러 작업을 처리함으로써 중복된 코드를 제거할 수 있도록 많은 기능들을 지원하고 있다. 이번에는 그 중에서 필터(Filter) vs 인터셉터(Interceptor)의 차이에 대해 알아보고자

mangkyu.tistory.com

 

Spring ArgumentResolver와 Interceptor

이번 글에서는 ArgumentResolver와 Interceptor를 사용할 때 spring이 요청을 처리하는 순서를 알아보고자 한다. 그 전에 ArgumentResolver는 무엇인지, Interceptor는 무엇인지 알아보도록 하자. Spring…

tecoble.techcourse.co.kr