퇴근까지 30분!! 퇴근 후 친구와의 약속을 떠올리며 다음의 핵심 비지니스 로직을 가지는 계산용 빈을 만들었다.
@Component
public class CalculatorBean {
public double divide(int a, int b){
return a/b;
}
public int add(int... args){
return Arrays.stream(args).sum();
}
}
이제 위 빈이 잘 동작하는지 테스트 해보자. 테스트만 통과하면 퇴근 각이다.!
@SpringBootTest
@Slf4j
@ActiveProfiles("dev")
public class CalculatorBeanTest {
@Autowired
CalculatorBean bean;
@Test
public void divideTest(){
double result =bean.divide(100, 2);
Assertions.assertEquals(50, result);
}
@Test
public void addTest(){
int result = bean.add(1,2,3);
Assertions.assertEquals(6, result);
}
@Test
public void beanTypeTest(){
Class<?> clazz = bean.getClass();
Assertions.assertEquals(CalculatorBean.class, clazz);
log.debug("bean type: {}", clazz.getName());
}
}
단위테스트는 잘 통과할 것이다. 여기서 주의 깊게 살펴볼 부분은 testBean에서 출력한 log 부분이다. 물론 실제 구현체가 CalculatorBean이었기 때문에 이상한 점은 하나도 없다.
Aspect 작성이 끝났다면 다시 한번 다시 한번 단위테스트를 실행해 보고 바뀐 점을 살펴보자.
07:23:17 [TRACE] c.d.a.a.CommonAspect.argumentLogging.15:
method call: int com.doding.aop.beans.CalculatorBean.divide(int,int), [100, 2]
07:26:15 [TRACE] c.d.a.a.CommonAspect.argumentLogging.15:
method call: int com.doding.aop.beans.CalculatorBean.add(int[]), [[1, 2, 3]]
테스트 결과를 살펴보면 놀랍게도 divide, add가 호출될 때마다 우리가 설정해 놓은 AOP가 잘 동작함을 알 수 있다. 어떻게 이런 마법 같은 일이 가능할까? 아주 신기해 하며 3번째 테스트를 실행하는 순간 좌절에 빠지고 만다. 테스트는 실패다!
org.opentest4j.AssertionFailedError:
Expected :class com.doding.aop.beans.CalculatorBean
Actual :class com.doding.aop.beans.CalculatorBean$$SpringCGLIB$$0
하지만 실패의 원인도 가늠하기 어렵다. CalculatorBean$$SpringCGLIB$$0은 도대체 뭐지?
AOP의 비밀 proxy
proxy란?
AOP의 비밀은 proxy 객체가 가지고 있다.
proxy란 '대리/대신' 이런 뜻을 가지고 있는데 겉에서 봤을 때는 원본(target)과 똑같이 생겨서 구분할 수 없고 런타임에 target 메서드 호출 전/후 등에 끼어들어서 무언가 부가적인 작업을 하는 객체를 의미한다.
이 proxy는 빈이 interface 기반인지 또는 class 기반인지에 따라서 생성 방식이 약간 다르다.
interface 기반 빈에 대한 Proxy
class 기반 빈에 대한 Proxy
종류
JDK의 동적 프록시
CGLIB(Code Generator Library)의 프록시
구성
장/단점
생성 시간이 빠르지만 인터페이스가 반드시 필요
클래스 바이트 코드를 조작하므로 생성 시간이 길지만 언제나 적용 가능
결과적으로 어떤 방식으로 Proxy를 만들던지 동작은 동일하며 proxy클래스는target 빈의 메서드를 재정의하기 때문에 외부에서는 proxy의 메서드를 부르는지 target 빈의 메서드를 부르는지 알지 못한다.
참고로 SpringBoot에서는 CGLIB를 기본으로 설정할 것인지에 대한 옵션으로 spring.aop.proxy-target-class 속성이 있는데 기본값은 true이다. 아마 생성되는 proxy에 대해 일관성을 갖기 위한 설정이 아닌가 한다.
proxy의 동작 방식
스프링은 빈 생성 후 pointcut에 기반해서 AOP 적용 대상인지 검토하고 대상이라면 proxy 클래스를 생성한 후 기존의 빈 즉 target 대신 proxy를 노출한다.
따라서 ApplicationContext에 빈을 요청하면 proxy가 반환된다. 이제 빈의 메서드를 호출하면 proxy가 target으로 가는 요청을 가로챈 후 @Before, @After 등 호출 시점에 맞춰 target의 메서드 호출 전/후에 advice 코드를 실행한다.