[ReSeller Project] 반복되는 부가기능을 공통적으로 처리해 보자

홍정완·2022년 9월 11일
0

ReSeller Project

목록 보기
10/15
post-thumbnail

개발을 하다 보면 컨트롤러에서 핵심 비즈니스 로직 외에 여러 부가기능을 동시에 처리하는 경우가 존재한다.
ex) 로그인 확인, 사용자 정보 확인


이럴 경우 하나의 객체 안에 핵심 비즈니스 로직과 부가 기능 로직이 섞여 들어가게 된다. 또한 부가 기능을 여러 곳에 적용하려면 상당히 귀찮다. 예를 들어 부가 기능이 필요한 클래스가 수십 개면 수십 개 모두 동일한 코드를 반복해서 추가해야 한다.


위와 같은 문제를 AOP, Interceptor, Filter 등을 통해 공통적으로 처리할 수 있다.

그중 InterceptorHandlerMethodArgumentResolver를 이용해 해결해 보자.



① 로그인 확인이 필요한 상황


애플리케이션을 이용하다 보면 로그인을 해야 접근 가능한 페이지가 있다.
예를 들어 마이페이지, 회원 전용 페이지, 비밀번호 변경 등이 있다.


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

Interceptor를 적용하지 않았을 때는 아래와 같은 과정이 필요하다.


  1. session에서 현재 로그인된 사용자의 정보를 꺼내온다.

  2. 만약 session에서 꺼낸 정보가 없다면 (null이라면),
    해당 사용자는 로그인을 하지 않은 상태이므로 401 UNAUTHORIZED 에러를 반환한다.

  3. 만약 정상적으로 session에서 로그인 정보를 꺼낼 수 있다면 200 OK를 반환한다.



Controller

 @GetMapping("/myInfo")
    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를 적용해서 해당 메서드가 핵심 비즈니스 로직에만 집중해서 처리할 수 있도록 만들어보자.


어노테이션 생성

@Retention(RUNTIME)
@Target(METHOD)
public @interface LoginCheck {}
  • @LoginCheck : 현재 사용자가 로그인 한 사용자인지 확인
  • @Retention : 어느 시점까지 어노테이션의 메모리를 가져갈지 설정
  • @Target : 어노테이션이 사용될 위치 지정



인터셉터 정의

@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 페이지가 렌더링 되고 난 후

컨트롤러의 메서드 실행 직후에 해당 요청을 가로채서 로그인 여부를 판단하기 위해 preHandler를 사용했다. 판단하는 과정은 아래와 같다.


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

LoginCheck : LoginCheck 어노테이션이 존재하는지 확인


  1. loginCheck가 null이면 로그인 없이 접근 가능한 페이지 👉 true 리턴

  2. loginCheck가 null이 아니면 session에서 로그인 정보(email)를 꺼내서 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 적용 후


Controller

@LoginCheck
@GetMapping("/myInfo")
public ResponseEntity<UserInfoDto> myPage() {
   String currentUser = loginService.getLoginUser();
   UserInfoDto loginUser = userService.getUserInfo(currentUser);
   return ResponseEntity.ok(loginUser);
}

Service

public String getLoginUser() {
   String userId = session.getAttribute(USER_ID);
   return userId; 
}

Service에서 불필요한 예외 처리를 하지 않아도 된다.



② 로그인된 사용자 정보 확인


Interceptor를 적용하고 나서 LoginCheck 어노테이션을 통해 로그인 확인을 할 수 있다.


String currentUser = loginService.getLoginUser();

하지만 로그인된 사용자 정보를 확인하는 과정은 코드로 남아있다.
HandlerMethodArgumentResolver 인터페이스를 추가해서 마저 해결해 보자.


HandlerMethodArgumentResolver은 컨트롤러 메서드에서 특정 조건에 맞는 파라미터가 있을 때 원하는 값을 바인딩 해주는 인터페이스다.


스프링에서는 Controller에서 @RequestBody 어노테이션을 사용해 Request의 Body 값을 받아올 때, @PathVariable 어노테이션을 사용해 Request의 Path Parameter 값을 받아올 때 HandlerMethodArgumentResolver를 사용해서 값을 받아온다.




HandlerMethodArgumentResolver 추가


어노테이션 생성

@Retention(RUNTIME)
@Target(PARAMETER)
public @interface CurrentUser {}
  • @CurrentUser : 현재 로그인된 USER의 ID(email)를 가져온다.



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();
    }
}

supportsParameter()

  • 현재 parameter를 resolver가 지원할지 true/false로 반환
    즉, 해당 메서드가 참이라면 resolveArgument()를 반환한다.

  • 해당 코드에서는 hasParameterAnnotation 메서드를 사용하여, 해당 메서드에 CurrentUser 어노테이션이 존재하는지 확인한다.


resolveArgument()

  • 실제 바인딩 할 객체 반환한다.

  • 해당 코드에서는 현재 로그인된 사용자 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("/myInfo")
public ResponseEntity<UserInfoDto> myPage(@CurrentUser String email) {
    UserInfoDto loginUser = userService.getUserInfo(email);
    return ResponseEntity.ok(loginUser);
 }
profile
습관이 전부다.

0개의 댓글