부가 로직 분리와 제거

개나뇽·2024년 12월 30일

컨트롤러에서 로그인 여부 확인, 사용자 정보 호출 등의 부가 기능이 섞여 동시에 처리하는 일이 있다. 이는 가독성과 유지 보수성 저하와 계층간의 분리 문제가 있다.

이러한 문제들은 AOP, Filter, Interceptor 등을 이용해 비즈니스 로직과 부가 기능을 분리할 수 있다.


AOP, Filter, Interceptor 차이

AOPFilterInterceptor
주체스프링서블릿스프링
개념메서드 호출 전-후로 특정 메서드에 부가 기능을 실행(삽입) 시킬 수 있다HTTP 요청과 응답을 가로채 필터링 가능디스패처 서블릿과 컨트롤러(핸들러) 사이에서 컨트롤러 호출 직전에 실행된다.
적용 범위특정 비즈니스 로직이나 메서드에 사용전체 애플리케이션, 특정 서블릿에 대해 적용 가능특정 컨트롤러에 대해 적용
실행 시점메서드 호출 전-후HTTP 요청이 서블릿에 도달하기 전컨트롤러 호출 전-후
사용 목적로깅, 트랜잭션요청 전처리, 인증 및 인가등 HTTP 관련 작업요청-응답 전처리 , 세션 관리 및 인증
사례트랜잭션 관리, 로깅인증 및 인가, 로깅, 요청 및 응답 변환요청 인증, 권한 체크, 공통 로직 처리

선택

  • AOP의 경우 비즈니스 로직 관련 작업에 더 적합할 수 있으나 HTTP 요청 및 응답의 흐릅을 직접 처리하기에는 제한적(AOP에서는 HttpServlet관련 객체를 얻기 어렵다)
  • 서블릿 필터의 경우 애플리케이션에 전역적으로 처리해야하는 작업에 적합하며, 해당 기능은 특정 핸들러 컨트롤러 등에만 적용되므로 오버스펙 이라 생각

→ Interceptor로 선택


1. 로그인 유무 확인

SNS 서비스에서는 로그인을 해야만 접근이 가능한 기능들 예를 들면 게시글 작성, 구도(팔로우), 댓글 등이 존재한다.

Interceptor 적용 전

Interceptor 적용 전에는 로그인 시 생성된 session에서 현재 사용자의 정보를 꺼내와 로그인 유무를 확인한다.

Controller

 		private final LikeService likeService;
    private final SessionLoginService sessionLoginService;
    
    @PostMapping("/{postId}")
    public void feedLike(Long userId, @PathVariable Long postId) {
        userId = sessionLoginService.getCurrentUserId();
        likeService.feedLike(userId, postId);
    }

Service

 public Long getCurrentUserId() {
    Long userId = (Long) httpSession.getAttribute(LOGIN_USER);
    if (userId == null) {
        throw new GlobalException(ErrorCode.UNAUTHENTICATED_USER);
    }
    return userId;
}

Interceptor 적용 순서

  1. 커스텀 애노테이션 작성
  2. 인터셉터 구현
  3. 로그인 유무 확인이 필요한 컨트롤러에 커스텀 애노테이션 사용

커스텀 애노테이션 정의

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}

커스텀 애노테이션을 정의할때는 해당 애노테이션이 사용될 위치(@Target), 라이프 사이클(@Retention)를 정의해야 한다.

  • @LoginRequired 는 메서드에 사용되며, 런타임 시 생성되며 런타임 종료시 사라진다.

Interceptor 정의

@Component
@RequiredArgsConstructor
public class LoginRequiredInterceptor implements HandlerInterceptor {

    private final SessionLoginService loginService;

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

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            LoginRequired loginRequired = handlerMethod.getMethodAnnotation(LoginRequired.class);
            if (loginRequired == null) {
                return true;
            }
            if (loginService.getCurrentUserId() == null) {
                throw new GlobalException(ErrorCode.UNAUTHENTICATED_USER);
            }
        }
        return true;
    }
}

LoginRequiredInterceptor는 HandlerInterceptor를 구현한다.

HandlerInterceptor에는 3가지 메서드가 있다.

  • preHandle()
    • 핸들러 어댑터 호출전 호출
    • 반환값이 true면 다음 실행
    • false의 경우 다른 인터셉터, 핸들러 어댑터 진행x
  • postHandle()
    • 핸들러 어댑터 호출후 호출
    • 예외 발생시 호출 x
  • afterCompletion
    • 뷰 렌더링 이후 호출됨
    • 항상 호출되는 메서드로 예외 발생시 예외(ex)를 파라미터로 받아 어떤 예외가 발생했는지 로그 출력

→ 로그인 유무 확인시에는 컨트롤러 호출 전에만 호출하면 되므로 preHandle()만 구현

동작 과정

  1. HandlerMethod handlerMethod = (HandlerMethod) handler : 호출될 컨트롤러의 메서드를 의미
  2. handlerMethod.getMethodAnnotation(LoginRequired.class) : 메서드가 @LoginRequired가 존재하는지 확인
  3. null 이라면 로그인이 필요없는 메서드로 true 반환
  4. loginService.getCurrentUserId() == null : session에 저장된 사용자 정보가 null 이라면 GlobalException(ErrorCode.UNAUTHENTICATED_USER)401 예외 발생
  5. 모든 검증을 마치고 true 반환 후 다음 작업 실행

Interceptor 등록 및 적용 후

Interceptor 등록

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final LoginRequiredInterceptor loginRequiredInterceptor;

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

Interceptor 적용 후

@LoginRequired
@PostMapping("/{postId}")
public void feedLike(Long userId, @PathVariable Long postId) {
		userId = sessionLoginService.getCurrentUserId();
    likeService.feedLike(userId, postId);
}
public Long getCurrentUserId() {
   return (Long) httpSession.getAttribute(LOGIN_USER);
}

→ 현재 로그인한 사용자의 정보를 가져오는 불필요한 Exception 처리가 사라진다.


2. 로그인한 사용자 정보 불러오기

Interceptor를 통해 Service에서 불필요한 Exception을 제거했지만 로그인 사용자의 정보를 가져오는 로직인

userId = sessionLoginService.getCurrentUserId() 아직 존재한다. 해당 로직은 부가 기능이면서 또한 중복되는 로직이다.

이번에는 위에서 사용한 커스텀 애노테이션과 HandlerMethodArgumentResolver 를 통해 처리가 가능하다.
HandlerMethodArgumentResolver 의 대표적인 예로는 @PathVariable이 존재한다.

처리 과정

  • 커스텀 애노테이션 정의
  • HandlerMethodArgumentResolver 구현

커스텀 애노테이션 정의

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface CurrentUser {

}
  • @CurrentUser는 파라미터에 사용되며 런타임 동안 유지된다.

HandlerMethodArgumentResolver 구현

@Component
@RequiredArgsConstructor
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final SessionLoginService loginService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {

        boolean hasAnnotation = parameter.hasParameterAnnotation(CurrentUser.class);
        boolean typeMatch = Long.class.isAssignableFrom(parameter.getParameterType());
        return hasAnnotation && typeMatch;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        return loginService.getCurrentUserId();
    }
}
  • supportsParameter() : 해당 메서드의 반환값이 true면 resolveArgument()를 실행
  • hasAnnotation() : 메서드 파라미터에 @CurrentUser 이 있는지 확인
  • typeMatch() : 파라미터의 타입이 Long 타입인지 확인
  • resolveArgument() : 현재 로그인한 사용자의 정보를 반환

HandlerMethodArgumentResolver 등록

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final CurrentUserArgumentResolver currentUserArgumentResolver;

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

HandlerMethodArgumentResolver 적용 후

private final LikeService likeService;

@PostMapping("/{postId}")
public void feedLike(@CurrentUser Long userId, @PathVariable Long postId) {
    likeService.feedLike(userId, postId);
}

→ 컨트롤러에서는 이제 별도의 부가 기능 로직을 처리하지 않으며 동시에 중복 로직도 제거가 된다.

profile
정신차려 이 각박한 세상속에서!!!

0개의 댓글