[실습] Spring AOP, Interceptor, Filter

Jerry·2025년 8월 19일

Dependency 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

AOP @Pointcut 내부 문법 정리

Spring AOP는 패키지 + 클래스의 메서드 단위로 컨트롤할 수 있다.

표현식의미
execution(public Integer com.edu.aop..*(*) )com.edu.aop 패키지 이하, 파라미터 1개, 리턴 타입 Integer인 모든 메서드
execution(* com.edu..get*(..))com.edu 패키지 이하, 이름이 get으로 시작, 파라미터 0개 이상
execution(* com.edu.aop..*Service.*(..))com.edu.aop 패키지 이하, 클래스명이 Service로 끝나는 모든 클래스의 메서드
execution(* com.edu.aop.BoardService.*(..))BoardService 클래스의 모든 메서드
execution(* some(*, *))이름이 some이고 파라미터가 2개인 메서드
*와일드카드: 모든 리턴 타입, 모든 이름 등

자바 코드

package com.codeit.aop.advice;

import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect // AOP Aspect임을 알리는 어노테이션
@Component // Bean 생성 어노테이션
public class BasicAdvice {

  @Pointcut("execution(* com.codeit.aop.service.UserService.*(..))")
  public void servicePointcut() {
    // 동작하지 않는 부분으로, 빈칸으로 남길 것!
  }

  // @Before : 대상 메서드가 호출되기 전에 실행되는 advice=메서드
  @Before("servicePointcut()")
  public void printBeforeLog(JoinPoint jp) {
    System.out.println("Before : " + jp.getSignature().getName() + "() 호출됨"); // 대상 메서드
    System.out.println("Before args : " + Arrays.toString(jp.getArgs()));
  }

  // @After : 대상 메서드가 호출된 후에 실행되는 advice
  // → 대상 메서드의 인자나 리턴 값을 가져올 수 없음
  @After("servicePointcut()")
  public void printAfterLog(JoinPoint jp) {
    System.out.println("@After : " + jp.getSignature().getName() + "() 호출됨");
  }

  // @AfterReturning : 메서드가 호출 이후에 리턴될 때 호출되는 advice, 대상 메서드가 종료되기 전에 호출
  // → After보다 빠른 호출, 리턴 값을 확인할 수 있다. // 예외가 발생한 경우 호출되지 않음
  @AfterReturning(pointcut = "servicePointcut()", returning = "result")
  public void printAfterReturningLog(JoinPoint jp, Object result) {
    System.out.println("@AfterReturning : " + jp.getSignature().getName() + "() 호출됨");

    if (result instanceof User u) {
      System.out.println("User : " + u);
    } else if (result instanceof List<?> list) {
      System.out.println("list : " + list);
    } else {
      System.out.println("result : " + result);
    }
  }

  // @AfterThrowing : 메소드에서 예외가 발생하여 에러가 던져졌을때 호출되는 메소드
  @AfterThrowing(pointcut = "servicePointcut()", throwing = "ex")
  public void printErrorLog(JoinPoint jp, Exception ex) {
    System.out.println("@AfterThrowing : " + jp.getSignature().getName() + "() 호출됨");
    ex.printStackTrace();
  }

  // @Around : 대상 메서드가 호출되기 전과 후에 처리하는 advice
  @Around("servicePointcut()")
  public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("@Around 실행 전 : " + pjp.getSignature().getName() + "() 호출됨");

    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    // 대상 메서드 실행
    Object result = pjp.proceed();

    stopWatch.stop();

    System.out.println("@Around 실행 후 : " + pjp.getSignature().getName() + "() 호출됨");
    System.out.println("@Around 메소드 실행 시간 : " + stopWatch.getTotalTimeMillis() + "ms");
    System.out.println("@Around 메소드 실행 시간 : " + stopWatch.getTotalTimeNanos() + "ns");

    return result;
  }
}

포인트컷

항목내용
선언@Pointcut("execution(* com.codeit.aop.service.UserService.*(..))")
의미com.codeit.aop.service.UserService모든 메서드(파라미터 무관) 대상
메서드servicePointcut() – 본문은 비워 둠(표식용)

어드바이스 요약

어노테이션/메서드실행 시점목적/설명리턴/예외 접근비고
@BeforeprintBeforeLog(JoinPoint jp)대상 메서드 호출 전메서드명/인자 로깅 등 사전 처리반환값 접근 불가, 예외 없음jp.getSignature().getName(), jp.getArgs() 사용
@AfterprintAfterLog(JoinPoint jp)정상/예외 여부와 무관하게 메서드 종료 후공통 정리/로깅반환값 접근 불가, 예외 객체 직접 제공 안 됨finally 성격
@AfterReturning(pointcut, returning="result")printAfterReturningLog(JoinPoint jp, Object result)정상 반환 직후반환값 기반 로깅/후처리반환값 접근 가능, 예외 시 호출 안 됨instanceof로 타입 분기(User, List<?> 등)
@AfterThrowing(pointcut, throwing="ex")printErrorLog(JoinPoint jp, Exception ex)예외 발생 시에러 로깅/모니터링반환값 없음, 예외 객체 접근 가능스택트레이스 출력 등
@AroundaroundAdvice(ProceedingJoinPoint pjp)호출 전/후 모두실행 시간 측정, 트랜잭션·권한 등 경계 처리pjp.proceed()로 실제 호출 제어, 반환값 가로채기 가능, 예외 재전파 가능StopWatch로 ms/ns 측정, 전/후 로깅

JoinPoint / ProceedingJoinPoint 사용 포인트

타입주요 사용 메서드설명
JoinPointgetSignature().getName(), getArgs()메서드명/인자 조회(비-어라운드에서 사용)
ProceedingJoinPointproceed()실제 대상 메서드 호출(오직 @Around에서만 가능)

Filter

package com.codeit.aop.config;

import com.codeit.aop.filter.LoggingFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<LoggingFilter> loggingFilter() {
        FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LoggingFilter());
        registrationBean.addUrlPatterns("/api/*"); // 적용할 URL 패턴
        registrationBean.setOrder(1); // 필터 실행 순서 (낮을수록 먼저 실행)
        return registrationBean;
    }
}
package com.codeit.aop.filter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;

public class LoggingFilter implements Filter {

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    System.out.println(">> LoggingFilter 초기화");
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    System.out.println("LoggingFilter >> 요청 들어옴: " + request.getRemoteAddr());
    chain.doFilter(request, response); // 다음 필터/서블릿으로 전달
    System.out.println("LoggingFilter << 응답 완료");
  }

  @Override
  public void destroy() {
    System.out.println("LoggingFilter >> LoggingFilter 종료");
  }
}

Interceptor

package com.codeit.aop.config;

import com.codeit.aop.interceptor.LoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor())
                .addPathPatterns("/api/**")           // 적용할 URL 패턴
                .excludePathPatterns("/api/auth/**"); // 제외할 URL 패턴
    }
}
package com.codeit.aop.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class LoggingInterceptor implements HandlerInterceptor {

    // 컨트롤러 실행 전
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("LoggingInterceptor >> 요청 URI: " + request.getRequestURI());
        return true; // true: 진행, false: 요청 차단
    }

    // 컨트롤러 실행 후 (뷰 렌더링 전)
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           org.springframework.web.servlet.ModelAndView modelAndView) throws Exception {
        System.out.println("LoggingInterceptor >> 컨트롤러 실행 후 처리");
    }

    // 뷰 렌더링까지 완료된 후 (예외도 포함)
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("LoggingInterceptor >> 요청 완료 후 처리");
    }
}
profile
Backend engineer

0개의 댓글