
Spring의 핵심 기술인 Proxy에 대해 알아봤고,
Proxy의 단점을 어떻게 극복하여 사용하는지 살펴봤다.
스프링 컨테이너에 등록된 모든 Bean에 대하여 로직을 수행하는 Spring 프록시는
클래스의 핵심 기능에 부가 기능을 쉽게 추가할 수 있어
획기적으로 반복되는 코드의 사용을 막을 수 있다.
결론적으로 개발자는 프록시 로직의 Advice와 적용 대상을 필터링하는 Pointcut을 담아
Advisor을 반환하는 메서드를 Bean으로 등록해주기만 하면 됐다.
이번에는 Advisor을 어떻게 만들면 좋을지 살펴보자.

Pointcut을 직접 구현하는 것은 어렵기 때문에,
보통 Spring에서 제공하는 Pointcut을 사용한다고 했다.
그 중에서도 주로 AspectJExpressionPointcut을 사용하는데
AspectJExpressionPointcut을 사용하기 위해서는 AspectJ 표현식을 알아야 한다.
마치 하나의 언어를 학습하는 것 같아 힘들지만, 열심히 알아보자.
Pointcut 지시자(PCD, Pointcut Designator)란,
Advice를 적용할 대상 메서드를 선별하는 표현식이다.
포인트컷 지시자를 하나씩 알아보자.

execution은 Advice를 적용할 특정 메서드를 선별하는 포인트컷 지시자이다.
가장 많이 사용되는 지시자이며 여러 요소를 가지고 있어 기능이 복잡하다.
요소를 하나씩 살펴보겠다.
// ** {} : 생략 가능, [] : 필수 **
execution({접근제어자} [반환타입] {패키지경로.클래스이름}.[메서드이름]([파라미터타입]) throws {예외})
execution(public String com.example.service.Service.serviceMethod(String))
예외 부분은 생략하고 모든 요소가 작성된 예시이다.
com.example.service 패키지 경로의 Service 클래스의
public String serviceMethod(String)에 Advice를 적용한다는 의미이다.
여기서 접근제어자와 패키지경로.클래스이름은 생략할 수 있다.
execution(String serviceMethod(String))
각 요소에 구체적인 문자를 입력하는 대신 대체 문자를 작성할 수 있다.
파라미터에는 다음과 같은 방식을 확장하여 대체문자를 사용할 수 있다.
execution(* *(..))
접근제어자, 반환타입, 패키지경로와 클래스이름, 메서드이름, 파라미터.
모든 요소에 관계 없이 Advice를 적용하는 표현식은 위와 같이 작성할 수 있다.
execution(public String com.example.service.Service.*(..))
execution은 상속을 적용하여 위 경우에는
Service를 상속하는 자식클래스 역시 AOP 적용 대상이 된다.
파라미터는 정확히 매칭되어야 하며, 파라미터의 자식클래스는 적용 대상이 아니다.

execution에서 [패키지경로.클래스이름] 부분만을 사용해서 메서드를 선별한다.
// ** com.example.service 패키지 경로의 Service 클래스의 모든 메서드에 적용 **
within(com.example.service.Service)
within의 경우 execution과 다르게 자식 클래스는 적용 대상이 아니다.
파라미터 타입으로 Advice를 적용할 메서드를 선별한다.
// ** 파라미터를 String으로 하는 모든 메서드에 적용**
args(String)
파라미터의 자식클래스 타입도 적용 대상이다.
args는 파라미터의 정보만으로 Advice 적용 대상 메서드를 선별하는데,
개발자가 등록하는 Bean 외에 Spring이 등록하는 Bean도 적용될 수 있기 때문에
args는 단독 사용이 금지된다.

[패키지경로.애노테이션이름]을 입력하여 사용한다.
@target(com.example.service.annotation.RealAnnotation)
// ** 부모 클래스도 적용 메서드 선별 대상에 포함된다 **
class Parent {
// 코드
}
@RealAnnotation
class Child extends Parent {
// 코드
}
해당 애노테이션이 붙은 클래스와 부모 클래스까지 포함하여 적용할 메서드를 선별한다.
Spring이 등록하는 Bean에도 적용되어 단독 사용이 금지된다.
[패키지경로.애노테이션이름]을 입력하여 사용한다.
@within(com.example.service.annotation.RealAnnotation)
해당 애노테이션이 붙은 클래스만 적용할 메서드를 선별한다.
@target과는 달리 부모 클래스는 선별 대상에 포함되지 않는다.
[패키지경로.애노테이션이름]을 입력하여 사용한다.
@annotation(com.example.service.annotation.RealAnnotation)
@target, @within과 달리 애노테이션이 붙은 클래스가 아닌 메서드에 적용된다.
[패키지경로. 애노테이션이름]을 입력하여 사용한다.
@args(com.example.service.annotation.RealAnnotation)
// ** Service가 @RealAnnotation이 붙어있다면 Advice를 적용한다. **
public void method(Service service) {
// 메서드 바디
}
메서드의 파라미터가 해당 애노테이션을 가지고 있는 경우 Advice를 적용한다.

bean 이름으로 Advice 적용 대상을 선별한다.
bean(service)
[패키지경로.클래스이름]을 입력하여 사용한다.
해당 클래스의 프록시 객체가 있으면 해당 객체에 Advice를 적용한다.
하위 클래스를 포함하여 적용한다.
// ** Service의 Proxy 객체가 있는 경우 해당 Proxy 객체에 Advice 적용 **
this(com.example.service.Service)
JDK 동적 프록시의 경우, 인터페이스를 기반으로 프록시를 생성하기 때문에
구체클래스에 this를 적용하면 프록시 객체가 생성되지 않은 상태이다.
이 부분만 유의하면 된다.
[패키지경로.클래스이름]을 입력하여 사용한다.
해당 클래스에 Advice를 적용하며, 하위 클래스를 포함하여 Advice를 적용한다.
// ** Service의 Proxy 객체가 있는 경우 해당 Proxy 객체에 Advice 적용 **
this(com.example.service.Service)

메서드 호출 시, 해당 메서드가 Advice 적용 대상 메서드라면
프록시 로직이 있는 Advice 메서드 호출 후 그 내부에서 원본 객체 메서드가 호출된다.
이 때 AspectJ 표현식을 사용하면 메서드에 전달한 파라미터의 값을
프록시 객체에서 확인이 가능하다.
AspectJExpression = pointcutDesignator(parameter)
public void aspectJExpression(Object parameter) {
Object value = parameter // ** 메서드 파라미터 값 획득 **
}
포인트컷 지시자에 작성하는 이름과
메서드 파라미터의 이름을 동일하게 작성하면
원본 객체를 호출할 때 작성한 메서드 파라미터 값을 전달받을 수 있다.
args(parameter)
public void aspectJExpression(Object parameter) {
Object value = parameter // ** 메서드 파라미터 값 획득 **
}
이 부분에서 표현식인 args(parameter)은
args(Object)로 풀이할 수 있다.
다음 포인트컷 지시자를 사용할 때 적용 가능하다.
AspectJExpression = @pointcutDesignator(annotation)
public void aspectJExpression(RealAnnotation annotation) {
RealAnnotation value = annotation // ** 애노테이션 획득 **
}
포인트컷 지시자의 애노테이션과
메서드 파라미터의 이름을 동일하게 작성하면
해당 애노테이션 정보를 획득할 수 있다.
@annotation(annotation)
public void aspectJExpression(RealAnnotation annotation) {
RealAnnotation value = annotation // ** 애노테이션 획득 **
}
이 부분에서 표현식인 @annotation(annotation)은
@annotation(패키지경로.RealAnnotation)으로 해석할 수 있다.
다음 포인트컷 지시자를 사용할 때 적용 가능하다.

AspectJ의 표현식에 대해 알아봤다.
AspectJ 표현식을 작성하여 AspectJExpressionPointcut을 사용할 수 있다.
이 Pointcut에는 AspectJ 표현식을 setExpression() 메서드로 등록해야
프록시 로직을 적용할 Bean을 필터링할 수 있다.
@Bean
public Advisor advisor() {
// ** Pointcut 생성 **
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(aspectJExpression); // ** AspectJ 표현식 작성 **
// ** Advice 생성 **
RealAdvice advice = new RealAdvice();
return new DefaultPointcutAdvisor(pointcut, advice);
결국 AspectJ 표현식만 잘 작성하면 된다.
AspectJ 표현식을 설정하고 Advisor에 등록만 하는 과정은 단순 작업일 뿐이다.
이러한 단순 작업은 Spring 형님께서 해결해주신다.

Pointcut을 생성하는 것의 핵심은 AspectJ 표현식을 작성하는 것이다.
@Around 애노테이션을 사용하면 AspectJ 표현식만을 작성하고
Pointcut을 생성하고 설정하는 과정을 생략할 수 있다.
@Around(aspectJExpression)
public void aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
// 프록시 로직
proceedingJoinPoint.proceed();
// 프록시 로직
}
ProceedingJoinPoint는 Advice의 MethodInvocation과 유사하다.
실제 객체를 호출하는 메서드는 proceed()로 MethodInvocation과 이름이 동일하다.
실제 객체를 호출할 때 발생하는 정보가 여기에 담겨져 있다.
실제 호출 객체, 파라미터, 호출 메서드 등이 포함된다.
Around 애노테이션은 Pointcut을 생성할 때 가장 많이 사용하는 애노테이션으로,
ProceedingJoinPoint를 사용하여 메서드 실행을 제어하고
실제 객체 메서드 호출 전, 후로 로직을 추가할 수 있으며
proceed() 메서드를 통해 메서드 파라미터를 변경할 수도 있다.
@Around 애노테이션에 AspectJ 표현식만 작성하면 모든 것이 해결된다.
이제 AspectJ 표현식을 잘 작성하는 데에 신경을 몰두하자!

@Around 외에 실제 객체 메서드가 호출되지 않는 것을 방지하고
메서드 실행 순서를 명확히 하는 등 다양한 옵션을 제공하는 대체 애노테이션들이 있다.
@Before(aspectJExpression)
public void beforeMethod(Joinpoint joinPoint) {
// 실제 객체 메서드 호출 전 동작하는 프록시 로직
}
@Before에서는 실제 객체 메서드가 호출되기 전에 동작하는 프록시 로직을 정의한다.
실제 객체 메서드 호출을 명시하지 않아도 자동으로 실행되며,
프록시 로직 호출 시점을 실제 객체 메서드 호출 전으로 한정하여
메서드 제어가 불가하다는 특징이 있다.
실제 객체 메서드를 호출하지 않기 때문에 ProceedingJoinPoint 대신
상위 타입인 JoinPoint를 사용하는 것으로 충분하다.

@After(aspectJExpression)
public void afterMethod(Joinpoint joinPoint) {
// 실제 객체 메서드 호출 후 동작하는 프록시 로직
}
@After는 실제 객체 메서드가 호출 된 이후에 동작하는 프록시 로직을 정의한다.
마찬가지로 실제 객체 메서드는 자동 실행되어 제어가 불가능하며 JoinPoint를 사용한다.
보통 자원을 해제하는 데에 사용된다.
@AfterReturning(value = aspectJExpression, returning = "result")
public void afterReturningMethod(Joinpoint joinPoint, Object result) {
// 실제 객체 메서드 호출 후 동작하는 프록시 로직
Object value = result;
}
@AfterReturning도 실제 객체 메서드가 호출 된 이후에 동작하는 프록시 로직을 정의한다.
@After과 특징은 같으나 차이점은 메서드 실행 결과를 받아볼 수 있다는 점이다.
returning element 이름과 메서드 파라미터 이름을 일치시켜 결과 조회가 가능하다.
@AfterThrowing(value = aspectJExpression, throwing = "e")
public void afterReturningMethod(Joinpoint joinPoint, Exception e) {
// 예외 발생 후 동작하는 프록시 로직
Object value = result;
}
@AfterThrowing은 예외가 발생한 후 동작하는 프록시 로직을 정의한다.
throwing element 이름과 파라미터 예외의 이름을 일치시켜 예외 조회가 가능하다.

@Around 애노테이션에 AspectJ 표현식을 작성하는 대신,
@Pointcut 애노테이션을 활용하면 AspectJ 표현식을 메서드로 분리해낼 수 있다.
// 기존
@Around(aspectJExpression)
// 변경 후
@Pointcut(aspectJExpression)
public void pointcutMethod(){}
public void pointcutMethod2(int value){} // ** 파라미터 전달 가능 **
// ** 사용 **
@Around("pointcutMethod()")
// ** 파라미터 전달 가능 **
@Pointcut을 사용하기 위해 지켜야할 규칙들이 있다.
메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처라고 부른다.
이렇게 Pointcut을 메서드로 분리할 경우,
Pointcut만 관리하는 클래스를 설계하여 별도로 관리하기 용이하다.

Pointcut은 총 2번 이루어진다.
프록시 생성 단계에서 실제 객체가 프록시 객체를 생성해야 하는지 확인하기 위해 1회,
메서드 호출 단계에서 객체의 메서드 중 해당 메서드가
프록시 객체를 통해 호출되어야 하는지 확인하기 위해 1회 이루어진다.

Pointcut을 생성하고 설정하는 것은 @Around를 사용하여 해결했다.
그렇다면 어떻게 이 Pointcut을 Advisor에 등록하는 과정도 자동화되는걸까?
그 비밀은 @Aspect에 있다.
@Aspect는 Proxy를 적용하기 위해 Spring 기능 중에서 가장 권장되는 방법이다.
이제 프록시 로직을 작성하던 Advice에 @Aspect 애노테이션을 선언한다.
이 애노테이션이 있으면 Spring의 BeanFactoryAspectJAdvisorBuilder가
내부 로직을 Advice로 생성하고, @Around는 Pointcut으로 생성하여
자동으로 Advisor을 생성하고 이후에 Bean으로 등록된다.
@Aspect // ** Advisor 자동 생성 **
class AspectClass {
@Around(aspectJExpression) // ** Pointcut 생성 **
public Object aspectMethod(ProceedingJoinPoint proceedingJoinPoint) {
// ** 내부 로직으로 Advice 생성 **
// 프록시 로직
Object result = proceedingJoinpoint.proceed();
// 프록시 로직
return result;
}
@Aspect 애노테이션을 선언한 클래스는 반드시 Bean으로 등록 후 사용 가능하다.
실행 순서는 먼저 Advisor로 직접 등록한 Bean이 프록시 객체를 생성한 뒤에
Aspect 애노테이션을 참고하여 프록시 객체가 생성된다.

하나의 프록시 객체에는 N개의 Advisor를 등록할 수 있다고 했다.
등록된 여러개의 Advisor을 특정 순서대로 작동하고 싶다면 어떻게 해야할까?
@Order을 사용하면 쉽게 해결이 가능하다.
@Order은 메서드가 아닌 클래스 단위로 설정이 가능하다.
따라서 순서를 적용하고 싶다면 하나의 클래스에서 Aspect를 관리하는 것이 아닌,
각각 별도의 클래스를 생성하여 @Aspect를 선언해야 한다.
마찬가지로 Bean으로 등록해야 사용 가능하다.
@Aspect
@Order(1)
class Aspect1 {
// 코드
}
@Aspect
@Order(2)
class Aspect2 {
// 코드
}
Order의 숫자가 작을수록 먼저 실행된다.

이렇게 Proxy를 사용하면 실제 객체 메서드인 원본에 로직을 추가할 수 있다.
Spring이 내가 작성한 코드를 웹에서 동작하기 위해 사용하는 것도 있지만,
개발 단계에서 부가 기능 로직이나 접근 제어, 기능을 추가하는 경우에도 사용할 수 있다.
이렇게 원본 로직에 다른 로직이 추가되는 것을 Weaving(위빙)이라고 한다.
원본 로직에 다른 로직을 추가하는 방법에는 크게 3가지 방법이 있다.

.class 파일을 생성하는 컴파일 시점에 로직을 추가하는 방법이다.
프록시를 생성하는 것이 아닌 실제 대상 코드에 적용되어 컴파일된다.
AspectJ가 제공하는 특별한 컴파일러를 사용해야 하며 복잡하다는 단점이 있다.
자바 실행 후 JVM에 .class 파일을 저장하기 전에 로직을 추가하는 방법이다.
이 시점에 Aspect를 적용하는 것을 로드 타임 위빙이라고 한다.
마찬가지로 실제 대상 코드에 적용된다.
JVM에 저장하기 전에 원본 로직을 조작하는 것을 java Instrumentation이라고 한다.
많은 모니터링 툴들이 이 방식을 사용한다.
자바 실행 시 java -javaagent를 통해 로더 조작기를 지정해야 하는 단점이 있다.
.class 파일을 생성하여 컴파일하고 클래스 로더에 올라가 자바가 실행되고 난 뒤,
즉 main() 메서드가 실행되고난 뒤 로직을 추가하는 방법이다.
Proxy를 사용하는 방법이 바로 이 방법이며 Spring 기술이 동반되어야 한다.
프록시 객체를 생성해야 추가 로직을 사용해야 한다는 제약이 있지만,
실제 대상 코드가 유지되며 특별한 컴파일러나 복잡한 과정이 없고
대부분의 문제가 프록시로 해결이 가능해서 현실적인 활용 방법이다.
생성자 호출, 필드 값에 접근할 때, static 메서드에 접근할 때 등
추가 로직을 더할 수 있는 다양한 시점이 존재한다.
런타임 시점에 추가하는 Proxy 방식은 메서드를 호출할 때로 한정된다.
객체의 메서드를 호출할 때 동적으로 프록시 객체가 메서드를 가로채는 방식의 배경이자
AspectJ 표현식에 메서드 이름이 필수적으로 작성되어야 하는 이유이다.

Proxy 로직을 추가하려면 반드시 Proxy 객체를 거쳐야 한다.
실제 객체 메서드를 직접 호출할 경우 Proxy 로직이 적용되지 않는다.
당연한 이야기인데 뭘 조심해야 한다는 걸까?
객체 메서드 내부에 또 다른 객체 내부 메서드를 호출한다면,
첫번째 메서드를 호출할 때에는 Proxy 객체에서 호출하지만
내부 메서드는 해당 객체인 this의 메서드를 호출하는 구조가 된다.
즉 메서드 내부에서 또 다른 내부 메서드를 호출하는 순간을 조심해야 한다!
Proxy는 실제 호출 객체의 메서드를 가로채는 방식으로 동작한다.
즉 호출이 가능한 public 메서드에 한정하여 적용할 수 있다.
즉 public 메서드 내부에서 다른 public 메서드를 호출할 때 문제가 발생하는 것이다.

@Component
class Service {
private Service service;
@Autowired // 수정자 주입
public void setService(Service service) {
this.service = service
}
public void outerMethod() {
// 메서드 바디
service.innerMethod(); // ** 수정자 주입으로 받은 프록시 객체 메서드 사용 **
}
public void innerMethod() {
// 메서드 바디
}
}
이 경우 내부의 메서드를 호출해도 자기 자신인 this 객체가 아닌
수정자 주입을 통해 의존관계를 주입 받은 프록시 객체의 메서드를 사용하여
추가 로직을 수행하지 않는 문제를 해결할 수 있다.
스프링은 순환 참조를 금지하여 위 코드는 사실 예외가 발생하는데
application properties에 다음을 추가하면 예외 없이 문제를 해결할 수 있다.
spring.main.allow-circular-references=true

Spring Bean에 등록된 프록시 객체를 조회하여 사용하는 방식이다.
ObjectProvider 또는 Provider를 사용하면 된다.
객체를 실제 사용하는 시점에 조회한다고 해서 지연조회라고 부른다.
@Component
class Service {
private final ObjectProvider<Service> provider;
@Autowired
public Service(ObjectProvider<Service> provider) {
this.provider = provider
}
public void outerMethod() {
// 메서드 바디
Service service = provider.getObject();
service.innerMethod();
}
public void innerMethod() {
// 메서드 바디
}
}

가장 권장하는 방식이다.
호출한 메서드에서 호출하는 내부 메서드를 보유하는 클래스를 추출하여
내부 메서드를 기존의 클래스와 분리하는 것이다.
@Component
class Service {
private OtherService otherService;
@Autowired
public Service(OtherService otherService) {
this.otherService = otherService
}
public void serviceMethod() {
// 메서드 바디
otherService.otherServiceMethod(); // ** 분리한 클래스에서 호출 **
}
}
@Componenet // ** 호출하는 클래스를 다른 클래스로 분리 **
class OtherService {
public void otherServiceMethod() {
// 메서드 바디
}
}

Spring AOP는 대부분의 클래스에 적용해야 하는 특정 기능인
횡단 관심사를 적용할 때에 매우 유용하게 사용할 수 있다.
최종적으로 Advisor을 구현하여 AOP 기능을 사용할 수 있었는데
@Around와 함께 AspectJ 표현식을 작성하고
@Aspect에 프록시 로직을 구현하여 슬기롭게 AOP를 사용해보자.