트랜잭션의 동작과정이 궁금해서 살펴보려 하니 계속해서 AOP라는 개념이 등장해서 공부하게 됐다. 구체적인 예시를 설명하기보다는 AOP를 이해하기 위해 필요한 기본적인 개념과 간략한 활용 방법을 정리해보려 한다.
"관점 지향"이라는 말이 잘 와닿지 않는데, 분업을 떠올리면 좀 더 이해가 쉽다. 로깅과 트랜잭션, 보안 기능을 AOP의 대표적인 예시로 많이 등장하는데, 트랜잭션을 예로 들어보자. 비즈니스 로직 전과 후에 트랜잭션을 위한 처리를 해주어야 한다.
- 비즈니스 로직 전: 비즈니스 메서드가 현재 실행중인 데이터베이스 트랜잭션에서 실행되야 하는지 새로운 독립적인 트랜잭션에서 실행되어야하는지 결정한다.
- 비즈니스 로직 실행
- 비즈니스 로직 후: 예외가 발생했다면 트랜잭션을 롤백 처리하고, 그렇지 않다면 커밋한다.
Serive 계층에는 요구사항을 처리하기 위한 수많은 메서드가 있는데, 트랜잭션 처리가 필요한 메서드마다 로직 전 후로 코드를 작성해야 한다면 매우 번거로워진다. 또한 에러가 발생하기 쉽고 관리가 어려워진다.
가장 핵심적인 비즈니스 로직에 집중하고 싶다면? 관심사를 분리하자.
비즈니스를 위해 필요한 부분과 트랜잭션 처리를 위해 필요한 부분은 관심사가 다르다고 볼 수 있다. 이를 분리한다면 각각의 관심사에 집중할 수 있다. 여러 메서드에 반복적으로 사용되며 흩어져있던 코드가 한 곳으로 모아지게 되기 때문에, 아래와 같은 장점이 있다.
위빙은 대상 객체의 생애 중 수행되는 시점에 따라 아래 3가지로 나뉠 수 있다.
1. 컴파일 타임 위빙(Compile time weaving = CTW)
타겟 클래스가 컴파일될 때 aspect가 위빙되며 별도의 컴파일러가 필요하다. AspectJ의 위빙 컴파일러는 이러한 목적으로 사용된다.
2. 로드 타임 위빙(Load time weaving = LTW)
클래스가 JVM에 적재될 때 aspect가 위빙된다. 이를 위해서는 애플리케이션에서 사용되기 전에 타켓 클래스의 바이트코드를 enhance하는 특별한 ClassLoader가 필요하다. AspectJ의 로드시간 위빙 기능을 사용하면 클래스로드시간에 위빙된다.
3. 실행 시간 위빙(Runtime Weaving = RTW)
애플리케이션 실행 중에 aspect가 위빙된다. 보통 타겟 객체에 호출을 위임하는 구조의 프록시 객체를, 위빙 중에 AOP 컨테이너가 동적으로 만들어낸다. 스프링 자체 AOP의 aspect는 실시간 위빙을 사용한다.
이제 AOP가 뭔지는 알겠는데, 그래서 이걸 어떻게 사용할 수 있다는 걸까? Spring에서 AOP를 구현하는 방법에 대해 알아보자!
AOP는 패러다임이기 때문에, 각 언어마다 구현체가 있다. Java 프로젝트에서 AOP를 구현하기 위해 지원하는 프레임워크는 AspectJ, JBoss AOP, Spring AOP가 있다고 한다.
Spring에서는 다음 3가지 방법 중 한 가지를 선택하여 AOP를 구현할 수 있다. Spring 공식문서에서는 일반적인 애플리케이션의 경우 AspectJ pointcut을 활용하여 AOP를 사용할 것을 권장하고 있다.
1. @AspectJ 어노테이션 활용
2. 스키마 기반 접근법(schema-based approach)
3. Spring AOP API 활용
의존성이 아래와 같이 구성되어 있기 때문에, spring-boot-starter-data-jpa 나 spring-boot-starter-data-aop를 의존성으로 추가하면, Spring AOP와 AspectJ를 모두 사용할 수 있다.
구현하는 방법에 어떤 것들이 있는지 알았으니, 이제 대략적으로 어떻게 사용하는지 알아보려한다.
@AsjpectJ의 지원을 허용하는 것이 가장 첫 번째 단계이다. 설정 파일에 다음과 같이 @EnableAspectJAutoProxy
어노테이션을 활용하여 허용할 수 있다.
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
Aspect는 앞서 용어 정리에서 언급한 듯이 Advice와 Pointcut으로 구성된다. 우선 Advice의 경우 아래처럼 5개의 어노테이션을 지원한다.
어노테이션 | 설명 |
---|---|
@Before | 비즈니스 로직 실행 전 |
@AfterReturning | 비즈니스 로직 실행 결과가 정상적으로 반환된 후 |
@AfterThrowing | 비즈니스 로직 실행 시 예외가 발생된 후 |
@After | 비즈니스 로직 실행 후 |
@Around | 비즈니스 로직 실행 전과 후 |
@Pointcut
어노테이션은 반복적으로 사용되는 pointcut을 지정할 수 있다. 해당 어노테이션을 활용하지 않고, advice 어노테이션 안에 pointcut을 지정해도 된다.
아래는 com.example.aop.annotation.service
에 있는 모든 메서드에 Before Advice와 After Advice를 적용하는 예시인데, @Pointcut 어노테이션을 활용해 반복되는 pointcut을 변수로 만들어 활용하고 있다.
import lombok.extern.slf4j.Slf4j;
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;
@Component
@Slf4j
@Aspect
public class TimeLog {
@Pointcut("within(com.example.aop.annotation.service..*)")
private void annotationServicePointcut() {}
@Before("annotationServicePointcut()")
public void start() {
log.info("시작: {}ms", System.currentTimeMillis());
}
@After("annotationServicePointcut()")
public void end() {
log.info("종료: {}ms", System.currentTimeMillis());
}
}
Spring AOP에서는 Proxy를 이용하여 AOP를 구현하기 때문에, 위에서 AspectJ를 활용한 방식과는 조금 다르다. 우선 동작과정에 대해 간단하게라도 살펴보고 넘어가자.
여기서도 Advice를 정의해줘야한다. Spring AOP에서는 Advice 유형에 따른 인터페이스를 아래와 같이 지원한다.
어드바이스 유형 | 인터페이스 |
---|---|
Before | org.springframework.aop.MethodBeforeAdvice |
After-returning | org.springframework.aop.AfterReturningAdvice |
After-throwing | org.springframework.aop.ThrowsAdvice |
Around | org.aopalliance.intercept.MethodInterceptor |
Introduction | org.springframework.aop.IntroductionInterceptor |
MethodInterceptor
인터페이스를 활용하여 Around Advice를 생성하고 Target에 적용했다.
public class DynamicProxyTest {
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
// 타깃 설정
pfBean.setTarget(new HelloTarget());
// 부가기능을 담은 어드바이스 추가
// 여러개 추가도 가능
pfBean.addAdvice(new UppercaseAdvice());
// FactoryBean 이므로 getObject()로 생성된 프록시를 가져온다.
Hello proxiedHello = (Hello) pfBean.getObject();
System.out.println(proxiedHello.sayHello("toby"));
}
static class UppercaseAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("시작");
// 타깃 오브젝트의 메서드 실행
String ret = (String)invocation.proceed();
System.out.println("종료");
return ret.toUpperCase();
}
}
// 타깃과 프록시가 구현할 인터페이스
static interface Hello {
String sayHello(String name);
String sayThankYou(String name);
}
// 타깃 클래스
static class HelloTarget implements Hello {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
@Override
public String sayThankYou(String name) {
return "Thank You " + name;
}
}
}
아직 어떤 방법이 좋은 방법인지, 실무에서는 어떻게 활용하고 있는지 잘 감이 오지 않는다. AOP가 뭔지는 알았는데, Proxy에 대해서는 온전히 이해하지 못한 것 같다. 런타임시에 Aspect를 적용하기 위해 만들어지는 객체라는 정도만 이해했다.
트랜잭션의 처리 과정을 보면서 실제로 Spring에서 AOP는 어떤식으로 활용되었는지 살펴보면서 Proxy에 대한 부분도 추가로 공부하는 게 좋을 것 같다.
@PointCut
활용 방법