관점지향 프로그래밍이란 Spring의 3대 요소중 하나로, 특정 관점을 기준으로 로직을 보고 그 로직을 모듈화하는 프로그래밍 방식을 말한다. 애플리케이션의 각 부분에 공통적으로 들어가는 내용을 독립적으로 모듈화하여 책임을 분리하고 코드를 깔끔하게 유지하기 위해 사용된다.
코드를 작성하다보면 여러 부분에서 반복적으로 사용되는 비즈니스 로직이 있는데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다. 그리고 이 흩어진 관심사를 모아서 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.
Aspect
위에서 설명한 흩어진 관심사를 모듈화 한 것으로 주로 부가기능을 모듈화하기 위해 사용된다.
Target
클래스, 메서드 등 모듈화된
Aspect를 적용하는 곳Advice
실질적으로 어떤 일을 해야할 지에 대한 것으로 실질적인 부가기능을 담은 구현체이다.
JointPoint
Advice가 적용될 위치와 적용되는 시점이다. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능하다.PointCut
JointPoint의 상세한 스펙을 정의한 것으로 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있다.
Spring에서 사용하는 AOP는 프록시 패턴 기반의 AOP 구현체이다. 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서이다. 스프링 AOP는 스프링 빈에만 적용이 가능하며, 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 등 엔터프라이즈 애플리케이션에서 가장 흔한 문제에 대한 해결책을 지원하는 것이 목적이다.
여기서 Aspect를 실행시키는 타이밍은 어노테이션을 통해 설정할 수 있다.
@Before (이전) : 어드바이스 타겟 메소드가 호출되기 전에 어드바이스 기능을 수행
@After (이후) : 타겟 메소드의 결과에 관계없이(즉 성공, 예외 관계없이) 타겟 메소드가 완료 되면 어드바이스 기능을 수행
@AfterReturning (정상적 반환 이후)타겟 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
@AfterThrowing (예외 발생 이후) : 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
@Around (메소드 실행 전후) : 어드바이스가 타겟 메소드를 감싸서 타겟 메소드 호출전과 후에 어드바이스 기능을 수행
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void onGetRequest() { }
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void onPostRequest() { }
@Pointcut("execution(* com.test..*.SomeService.*(..))")
public void onSomeServiceExecution() { }
/**
* 특정 서비스 호출시 Aspect 실행
*/
@Before("onSomeServiceExecution()")
public void beforeSomeServiceAction(JoinPoint joinPoint) {
// 메서드 정보 받아오기
Method method = getMethod(joinPoint);
log.info("======= method name = {} =======", method.getName());
// 파라미터 받아오기
Object[] args = joinPoint.getArgs();
if (args.length <= 0) log.info("no parameter");
for (Object arg : args) {
log.info("parameter type = {}", arg.getClass().getSimpleName());
log.info("parameter value = {}", arg);
}
}
/**
* Pointcut 과 매칭되는 메서드의 실행 전, 후에 실행
* @Around advice 는 꼭 proceed()가 필요하다.
*/
@Around("onGetRequest() || onPostRequest()")
public Object logAction(ProceedingJoinPoint joinPoint) throws Throwable{
Object result = null;
try {
result = joinPoint.proceed(joinPoint.getArgs());
return result;
} finally {
// HttpServletRequest 로 URI 및 파라미터 목록 출력
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
log.info("{} {}", request.getMethod(), request.getRequestURI());
Set<String> keySet = request.getParameterMap().keySet();
log.info("================== parameter scan starting ==================");
for(String key: keySet) {
log.info("parameter key = {}", key);
log.info("parameter value = {}", request.getParameter(key));
}
log.info("================== parameter scan finished ==================");
}
}
}
beforeSomeServiceAction과 logAction 두가지 예시를 들었다. 이에서 설명 했듯이 @Around 어노테이션은 타겟 메소드를 감싸서 호출 전후로 실행되는 메소드이기 때문에 반드시 proceed()를 메소드 내에서 실행하여야 한다.
@Before 어노테이션에서 사용된 onSomeServiceExecution() Pointcut처럼 특정 클래스의 모든 메소드가 실행될 때 Advice를 실행할 수도 있고, 특정 어노테이션이 실행될 때, 또는 위 예시에서 사용되지는 않았지만 특정 Bean을 타겟으로 설정할 수도 있다.