(JPA+Boot)AOP와 인터셉터와 리졸버

스프링 프레임워크에서 인터셉터, ArgumentResolver, AOP를 활용하여 웹 애플리케이션의 공통 관심사를 효과적으로 분리하고 처리하는 방법과 각 기술의 동작 원리 및 구현 패턴을 상세히 설명하는 가이드입니다.



인터셉터, 리졸버, AOP

요청 흐름: requset(HTTP) > Filter(서블릿필터) > Servlet(디스패처서블릿) > Interceptor > Argument Resolver > AOP > Controller


AOP(공통 해결 관심사): 관점지향프로그래밍

  • 동작흐름 : 프록시 객체 생성 -> 실제 객체 생성 의 흐름

  • 만들 때 : @Aspect 사용 및 “스프링 빈 등록 필수”

  • 사용할 때 : @Around로 원하는 곳에 적용

    • @Around는 AOP의 어드바이스 타입 중 하나, 메서드 실행 전후 로직 실행 지원

    • proceed() 를 통해 원하는 시점 때 “실제 실행”을 지원 -> AOP 말고 실제 메서드

AOP 예시 코드 - 메소드 수행시간 기능


@Aspect // AOP
@Component // "빈" 등록
@Slf4j
public class TimeTraceAop {
  @Around("execution(* com.lepl..*(..))")
  public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    // 프록시 실행
    long start = System.currentTimeMillis();
    log.debug("START: {}", joinPoint.toString());
    try {
      return joinPoint.proceed(); // 실제 실행
    } finally {
      long finish = System.currentTimeMillis();
      long timeMs = finish - start;
      log.debug("END: {} {}ms", joinPoint.toString(), timeMs);
    }
  }
}


“서블릿 필터” 보다는 “Interceptor” 와 “ArgumentResolver” 활용 권장

  • ArgumentResolver 를 통해서 공통 작업할 때 컨트롤러를 더욱 편리하게 사용 가능 (컨트롤러 메서드의 매개변수에 적절한 값을 바인딩하는 과정을 담당)
    • 서블릿(=Dispatcher Servlet) 과 컨트롤러(핸들러) 사이에는 사실 핸들러 어댑터가 동작을 하며, 이때 “ArgumentResolver” 가 중간에 있어서 이를 거치고 “컨트롤러(핸들러)” 가 동작

    • ArgumentResolver 덕분에 수많은 “애노테이션” 속 데이터들을 컨트롤러(핸들러)로 정상 전달

      • EX) @Login 애노테이션 만들어서 “멤버 정보” 객체 반환 이런게 가능!

      • 기존 : @SessionAttribute(name = "loginMember", required = false) Member loginMember

      • 적용 : @Login Member loginMember

  • Interceptor” 는 “필터” 보다 더 많이 호출 (컨트롤러 도달 전과 후에 공통 수행)
    • “로그인 인증” 을 예시로 구현 가능
      • preHandle 에 “세션정보(쿠키)” 인증을 시도 및 실패시 다시 “로그인 창” 으로 이동
        • 이를 다양한 URL 패턴으로 적용 가능 -> 적용URL, 미적용URL 구분도 간편
      • 컨트롤러 도달 후인 postHandle() 도 있다는 것만 기억.

아래 코드들은 WebMvcConfigurer 를 구현한 Java Config 방식 포함

예시코드 - ArgumentResolver


Login.java(인터페이스) -> @Login 애노테이션 생성

@Target(ElementType.PARAMETER) // 파라미터에 사용
@Retention(RetentionPolicy.RUNTIME) // RUNTIME 까지 살아남게 설정
public @interface Login {
}

LoginMemberArgumentResolver.java -> 애노테이션이 @Login이고, 타입이 Long인지 체크 후 세션에 있는 memberId 반환해주는 로직

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
//
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    log.info("supportsParameter 실행");
//
    boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
    boolean hasLongType = Long.class.isAssignableFrom(parameter.getParameterType());
    // 어노테이션이 @Login 이고, 해당 파라미터 타입이 Long 라면 true 반환
    return hasLoginAnnotation && hasLongType;
  }
//
  // 위 supportsParameter 가 true 면 아래 함수가 실행
  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    log.info("resolveArgument 실행");
//
    HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
    HttpSession session = request.getSession(false); // false : 없으면 null
    if (session == null) {
      log.info("null");
      return null;
    }
    Long memberId = Long.valueOf(session.getAttribute(SESSION_NAME_LOGIN).toString());
    return memberId;
  }
}

ApiConfig.java -> ArgumentResolver 등록 방법

@Configuration // 설정 파일임을 알림
@Slf4j
public class ApiConfig implements WebMvcConfigurer {
//
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    resolvers.add(new LoginMemberArgumentResolver());
  }
}
예시코드 - Interceptor


MemberCheckInterceptor.java -> 로그인 인증 유무 체크 로직

@Slf4j // log
public class MemberCheckInterceptor implements HandlerInterceptor {
//
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    String requestURI = request.getRequestURI();
    log.info("인증 체크 인터셉터 실행 {}", requestURI);
//
    HttpSession session = request.getSession();
    if (session == null || session.getAttribute(SESSION_NAME_LOGIN) == null) {
      log.info("미인증 사용자 요청");
      // 회원 아님을 알림
      response.setStatus(HttpStatus.UNAUTHORIZED.value()); // status code : 401
      return false;
    }
    return true;
  }
}

ApiConfig.java -> Interceptor 적용법 + 인증 URL과 미인증 URL 설정 로직

@Configuration // 설정 파일임을 알림
@Slf4j
public class ApiConfig implements WebMvcConfigurer {
//
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MemberCheckInterceptor())
        .order(2)
        .addPathPatterns("/**") // 모든 경로 접근
        .excludePathPatterns("/", "/api/v1/members/login", "/api/v1/members/register",
            "/api/v1/members/logout", "/api/v1/members/*",
            "/image/**", "/css/**", "/*.ico", "/error"); // 제외 경로!
  }
}



댓글남기기