본문으로 바로가기

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.


 

프로젝트를 진행하다 보면 컨트롤러의 메소드가 핵심 기능을 처리하는 과정뿐만 아니라 로그인을 확인하거나, 현재 로그인 되어있는 사용자의 ID를 가져오는 작업등의 부가기능을 동시에 처리하는 경우가 존재한다.

 

이때 우리는 AOP , Interceptor, Filter 등을 통해 반복되는 부가기능을 공통적으로 처리할 수 있다.

해당 포스팅에서는 InterceptorHandlerMethodArgumentResolver를 이용해 리팩토링 하는 방법을 알아보려고 한다.

1. 로그인 확인이 필요한 상황

애플리케이션을 사용하다 보면 로그인 없이 접근 가능한 페이지가 있고, 로그인을 하거나 추가 권한이 있는 경우에만 접근이 가능한 페이지가 존재한다. 예를 들어 마이페이지, 회원 전용 게시판에 글쓰기, 비밀번호 변경 등등이 있다.

Interceptor를 적용하기 전 로그인 확인 과정

먼저 Interceptor를 적용하지 않았을 때의 코드를 살펴보면 다음과 같은 과정을 진행해야 한다.

  1. session에서 현재 로그인된 사용자의 정보를 꺼내온다.
  2. 만약 session에서 꺼낸 정보가 없다면 (null이라면) , 해당 사용자는 로그인을 하지 않은 상태이므로 401 UNAUTHORIZED 에러를 반환한다.
  3. 만약 정상적으로 session에서 로그인 정보를 꺼낼 수 있다면 200 OK를 반환한다.
Controller
    @GetMapping("/my-infos")
    public ResponseEntity<UserInfoDto> myPage() {
        String currentUser = loginService.getLoginUser();
        UserInfoDto userInfoDto = userService.getUserInfo(currentUser);
        return ResponseEntity.ok(loginUser);
    }
Service
public String getLoginUser() {
    String userId = session.getAttribute(USER_ID);
    if(userId == null) {
        throw new UnauthenticatedUserException();
    }
   return userId; 
}

위 코드처럼 현재 사용자가 로그인 된 상태인지 확인하는 로직이 로그인을 요구하는 핸들러에 사용되고 있다.

 

Interceptor 적용 과정

이제 Interceptor를 적용해서 해당 메소드가 자신의 핵심 기능만 집중할 수 있도록 리팩토링 해보자.

 

어노테이션 생성하기

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * @LoginCheck :  현재 사용자가 로그인 한 사용자인지 확인한다.
 * @Retention : 어느 시점까지 어노테이션의 메모리를 가져갈 지 설정
 * @Target : 어노테이션이 사용될 위치를 지정한다.
 */

@Retention(RUNTIME)
@Target(METHOD)
public @interface LoginCheck {

}

인터셉터 정의하기

/**
 * HandlerMethod  : 실행될 핸들러(컨트롤러의 메소드) loginCheck가 null이라면 로그인 없이 접근가능한 핸들러이므로 true 리턴
 */

@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {

    private final LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler)
        throws Exception {

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        LoginCheck loginCheck = handlerMethod.getMethodAnnotation(LoginCheck.class);

        if (loginCheck == null) {
            return true;
        }

        if (loginService.getLoginUser() == null) {
            throw new UnauthenticatedUserException("로그인 후 이용 가능합니다.");
        }
        return true;

    }
}

Interceptor의 실행 메서드는 크게 preHandler() , postHandler() , afterCompletion() 로 구성되어 있다.

  • preHandler() : 컨트롤러 메서드(핸들러)가 실행되기 전
  • postHandler() : 컨트롤러 메서드(핸들러) 실행 직 후, view 페이지가 렌더링 되기 전
  • afterCompletion() : view 페이지가 렌더링 되고 난 후

우리는 컨트롤러의 메서드 실행 직후에 해당 요청을 가로채서 요청을 한 사용자의 로그인 여부를 판단할 것이다. 판단하는 과정은 다음과 같다.

HandlerMethod : 실행될 컨트롤러의 메소드(핸들러)

loginCheck : 해당 핸들러에 이전에 만든 LoginCheck 어노테이션이 존재하는지 확인

  1. loginCheck가 null이라면 로그인 없이 접근 가능하기 때문에 true를 리턴하여 다음 작업을 실행한다.
  2. loginCheck가 null이 아니라면 session에서 로그인 정보를 꺼내서 null 여부를 판단한다. null이라면 로그인 후 이용 가능하다는 Exception을 날린다.
  3. 모든 검증을 통과했다면 로그인이 완료된 상태로 true를 리턴해 다음 작업을 실행한다.

인터셉터 등록하기

이제 위에서 우리가 만든 Interceptor를 등록하는 과정만 남아있다. 아래 코드를 보고 인터셉터를 등록 해보자.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor);
    }

}

webMvcConfigurer를 구현할 경우 스프링 부트가 기본으로 설정한 MVC 설정에 추가적으로 기능을 커스터마이징 할 수 있다. (스프링 부트를 통한 개발 환경을 구축했을 경우에만 해당한다.)

Interceptor 적용 후 리팩토링된 코드

@LoginCheck
@GetMapping("/my-infos")
public ResponseEntity<UserInfoDto> myPage() {
   String currentUser = loginService.getLoginUser();
   UserInfoDto loginUser = userService.getUserInfo(currentUser);
   return ResponseEntity.ok(loginUser);
}
public String getLoginUser() {
   String userId = session.getAttribute(USER_ID);
   return userId; 
}

이제 현재 로그인된 사용자의 ID를 가져오는 Service에서 불필요한 Exception 처리를 할 필요가 없게 되었다.


2. 현재 로그인된 사용자의 정보 가져오기

Interceptor를 적용함으로써 로그인이 필요한 경우에 일일이 로그인을 확인하는 로직을 직접 작성하지 않고 LoginCheck 라는 어노테이션 하나로 해결할 수 있게 되었다.

 

하지만 아직도 공통적으로 사용되는 부가기능이 남아있다. 바로 아래와 같이 현재 로그인된 사용자의 정보를 불러오는 과정이다.

String currentUser = loginService.getLoginUser();

위와 같은 과정은 HandlerMethodArgumentResolver 인터페이스를 추가해서 해결할 수 있다.

HandlerMethodArgumentResolver 인터페이스는 컨트롤러에서 파라미터를 바인딩 해주는 역할을 한다.

 

예를 들어 특정 클래스나 특정 어노테이션등의 요청 파라미터를 수정해야 하거나, 또는 클래스의 파라미터를 조작 혹은 공통적으로 사용해야 하는 경우 파라미터들을 바인딩 해주는 것이다.

 

우리가 많이 사용하는 @PathVariable의 원리도 HandlerMethodArgumentHandler에 의해서 처리되는 것이다.

 

HandlerMethodArgumentResolver 추가하는 과정

어노테이션 생성하기

먼저 인터셉터를 적용할때 처럼 어노테이션을 하나 만들자.

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * @CurrentUser : 현재 로그인된 USER의 email(아이디)를 가져온다.
 */

@Retention(RUNTIME)
@Target(PARAMETER)
public @interface CurrentUser {

}

HandlerMethodArgumentResolver 정의하기

HandlerMethodArgumentResolver에는 두 가지의 메서드가 존재한다.

@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final LoginService loginService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer             mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception       {
        return loginService.getLoginUser();
    }
}
  1. supportsParameter() : 현재 parameter를 resolver가 지원할지 true/false로 반환한다. 즉, 해당 메서드가 참이라면 resolveArgument()를 반환한다.
  2. 해당 코드에서는 hasParameterAnnotation 메소드를 사용하여 해당 메소드에 CurrentUser라는 어노테이션이 존재하는지 확인한다.
  3. resolveArgument() : 실제 바인딩할 객체를 반환한다.
  4. 해당 코드에서는 현재 로그인된 사용자의 ID를 반환한다.

HandlerMethodArgumentResolver 등록하기

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {


    private final LoginCheckInterceptor loginCheckInterceptor;
    private final LoginUserArgumentResolver loginUserArgumentResolver;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor);
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginUserArgumentResolver);
    }
}

ArgumentResolver 적용 후 리팩토링된 코드

Controller

@LoginCheck
@GetMapping("/my-infos")
public ResponseEntity<UserInfoDto> myPage(@CurrentUser String email) {
    UserInfoDto loginUser = userService.getUserInfo(email);
    return ResponseEntity.ok(loginUser);
 }

Service

이제 해당 핸들러를 사용할때 위에서 사용했던 String currentUser = loginService.getLoginUser();는 필요가 없게 되었다!