// A class
public void request(RequestBodyDTO body) {
requestService.request(body);
}
// B class
public Object findById(Long id) {
findService.findById(id);
}
// C class
public void payAll() {
payService.payAll();
}
// D class
public void saveItem(Item item) {
repository.save(item);
}
만약 위와 같이 각각의 클래스에 있는 메서드가 호출될 때, 로그를 찍고 싶다면 어떻게 해야할까?
// A class
public void request(RequestBodyDTO body) {
log.info("[A.request]");
requestService.request(body);
}
// B class
public Object findById(Long id) {
log.info("[B.findById]");
findService.findById(id);
}
// C class
public void payAll() {
log.info("[C.payAll]");
payService.payAll();
}
// D class
public void saveItem(Item item) {
log.info("[D.saveItem]");
repository.save(item);
}
이렇게 모든 메서드에 로그를 찍는 로직을 추가해줄 수 있다. 매우 간단하다. 끝...
이 아니라 문제는 이런 메서드가 너무 많을 때이다. 극단적으로 수백개의 메서드에 로그를 찍는 로직을 추가해야한다고 해보자. 감당가능한가..?
우리는 이럴 때 감사하게도 스프링이 제공해주는 AOP 를 사용하면 된다.
AOP를 번역하면 관점 지향 프로그래밍이다. OOP가 객체지향 프로그래밍인 것 처럼 어떤 프로그래밍 기법인 것처럼 보여서, OOP와 AOP는 공존하지 못할 것처럼 보인다. 하지만 아니다. AOP는 우리가 OOP를 사용하면서 불편한 점을 해소해주는 것이다.
어떤 불편인 지는 위의 간단한 예제로 이미 알아보았다. 위 예제처럼 로그를 찍거나 트랜잭션으로 핵심 로직을 감싸는 등의 행위는 메서드의 부가 기능이다. 이러한 부가 기능은 특정 메서드 뿐만아니라 애플리케이션 전역에 걸쳐 공통적으로 적용할 필요가 있다.
이런 부가 기능을 횡단 관심사(cross cutting concerns)라고 하는데 AOP를 사용하면 횡단 관심사를 원하는 경로에 있는 메서드에 횡단에 걸쳐 부가 기능을 추가할 수 있다.
스프링 AOP는 프록시를 사용한다. AspectJ에서 제공하는 여러 방법을 사용하면 프록시가 아니라 실제 객체에 컴파일 시점에 코드를 추가한다던가, 클래스 로딩 시점에 추가할 수도 있다. 이들은 확실한 관점 지향 프로그래밍이 가능하나 설정이 복잡하여 잘 사용하지 않는다.
스프링 AOP는 AOP를 적용하겠다고 해놓은 객체의 프록시를 생성한다. 그 프록시에는 우리가 추가하고자 하는 부가 기능과 실제 객체의 주소와 메서드(joinPoint)가 들어있다. 프록시를 만들 때, 해당 객체를 상속받거나(CGLIB) 인터페이스를 구현(JDK 동적프록시)하기 때문에 Proxy인 지 아닌 지 몰라도 우리가 아는 그 객체의 타입을 사용하여 호출 실제로는 프록시가 호출된다. 프록시는 부가 기능을 수행하고 실제 메서드를 호출해준다.
위 예시는 트랜잭션을 부가 기능으로 하는 AOP를 적용시킨 예이다.
AOP를 적용하기로 한 메서드가 있는 객체 A(스프링 빈)를 의존성 주입받았다. 이 객체를 이용해 해당 메서드를 호출하게 되면 이렇게 실제 A 객체가 아닌 A 객체의 프록시 객체를 호출하게 된다. 그러면 프록시 내부에 있는 메서드가 트랜잭션을 시작해주고, target(실제 대상 객체)의 실제 메서드(핵심 기능)를 호출해준다.
근데 무슨 기준으로 메서드를 고를까? 게다가 나는 트랜잭션이 아니라 로그를 찍고 싶은데 어떻게 바꿔?
AOP에는 Advisor
, Advice
, PointCut
이라는 용어가 있는데 하나씩 알아보겠다.
Advice
는 부가 기능 그 자체를 의미한다. 로그를 추적한다거나, 트랜잭션으로 감싼다거나 하는 메서드 코드를 말한다.
PointCut
은 Advice
를 어디에 적용할 지를 말한다.
Advisor
는 Advice
와 PointCut
을 쌍으로 가지고 있는 것을 말한다. Advisor
만 알고 있으면 적용 위치와 행위를 모두 알고있기 때문에 AOP를 적용할 수 있다.
스프링은 Advisor
를 빈으로 등록하면 자동 프록시 생성기가 모두 자동으로 프록시를 생성해준다. 이 때, 기준은 PointCut
에 해당하는 메서드가 단 하나라도 있으면 해당 객체는 프록시가 생성된다.
스프링 AOP에서는 @Aspect
애노테이션을 사용하여 간단하게 Advisor
를 만들 수 있다.
@Aspect
public class LogTraceAspect {
private final LogTrace logTrace;
public LogTraceAspect(LogTrace logTrace) {
this.logTrace = logTrace;
}
/* 포인트 컷만 생성 가능 */
@Pointcut("execution(* hello.proxy.app..*(..))")
private void allApp(){} // pointcut signature
@Around("execution(* hello.proxy.app..*(..))") // 포인트컷
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
//어드바이스
TraceStatus status = null;
try {
String message = joinPoint.getSignature().toShortString();
status = logTrace.begin(message);
// 로직 호출
Object result = joinPoint.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
execute()
에서 @Around
가 PonitCut
, 해당 메서드가 Advice
가 된다.
@Around
는 해당 메서드(조인포인트)의 앞 뒤로 모두 부가 기능을 수행할 수 있는 애노테이션이다.@Around
를 사용하면ProceedingJoinPoint
를 매개변수로 받아야하는데, 여기에는 실제 메서드를 수행하는proceed()
가 있다.
자동 프록시 생성기는 기본적으로 스프링 빈으로 등록되어있는데, 자동 프록시 생성기가 @Aspect
애노테이션을 찾아서 Advisor
로 변환하여 프록시를 생성해준다.