AOP는 관점 지향 프로그래밍. Spring의 핵심 개념중 하나인 DI가 애플리케이션 모듈들 간의 결합도를 낮춰준다면, AOP는 애플리케이션 전체에 걸쳐 사용되는 기능을 재사용하도록 지원하는 것
쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다.
예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다.
위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.
@Service
@RequiredArgsConstructor
public class UserService {
private final PlatformTransactionManager transactionManager;
public void someSevice() {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 부가 기능 - 로깅, 보안 등등
// 핵심 기능
someServiceMethod();
// 부가 기능
transactionManager.commit(transaction);
} catch (RuntimeException runtimeException) {
transactionManager.rollback(transaction);
throw runtimeException;
}
}
}
서비스 로직의 원자성 보장을 위해 내부적으로 트랜잭션을 적용한 코드입니다. 문제는 UserService의 클래스에는 someServiceMethod()
핵심 비즈니스 로직 이외에도 트랜잭션 경계 설정이라는 부가 기능
관심사들이 존재한다.
현재 예제 코드는 부가 기능 관심사가 트랜잭션 하나 뿐이지만, 또 다른 부가 기능의 관심사가 추가된다면 부가 기능이 필요한 메서드마다 비슷한 코드를 중복해서 작성해야 한다.
가장 큰 문제는 UserService와 비슷하게 수행해야 하는 클래스가 100개가 더 있을 수 있기 때문에 필요한 클래스 마다 UserService와 같이 중복되는 코드를 반복해서 작성해야 함을 의미한다.
만약 코드를 변경한다면 클래스를 변경하는 이유는 비즈니스 로직의 변경 및 부가 기능 코드 또한 변경해야 하기 때문에 서비스 클래스의 응집도가 떨어지고 가독성 또한 나빠지며, 변경할 부분이 명확하게 드러나지 않게 되는등 유지보수 측면에서 아쉬운 점이 많아진다.
프록시 객체에 트랜잭션 등 부가 기능 관련 로직을 위치시키고, 클라이언트 요청이 발생하면 실제 타깃 객체는 프록시로부터 요청을 위임받아 핵심 비즈니스 로직을 실행합니다. 이를 데코레이터 패턴이라고 한다.
public interface UserService {
void someSevice();
}
@Service
@Primary
@RequiredArgsConstructor
public class SimpleUserService implements UserService {
@Override
public void someSevice() {
...
}
}
@Service
@RequiredArgsConstructor
public class UserServiceProxy implements UserService {
private final UserService target;
private final PlatformTransactionManager transactionManager;
@Override
public void someSevice() {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 부가 기능 - 로깅, 보안 등등
// 핵심 기능
someServiceMethod();
// 부가 기능
transactionManager.commit(transaction);
} catch (RuntimeException runtimeException) {
transactionManager.rollback(transaction);
throw runtimeException;
}
}
}
프록시 객체를 이용하여 핵심 메소드가 호출되기 이전에 부가 기능을 적용하였다.
프록시 객체를 이용하여 핵심 비즈니스 로직과 부가 기능 관심사를 분리할 수 있었지만 여전히 한계가 존재한다.. 100개의 클래스가 이와 비슷한 기능을 요구한다면, 100개의 프록시 클래스를 생성하고 인터페이스 메서드를 일일이 구현해야 합니다.
다행히 이러한 별도의 프록시를 번거롭게 생성하는 작업을 생략하는 방법이 존재합니다. Java의 Reflection API를 이용하거나, Spring의 ProxyFactoryBean 등을 사용하는 방법이 존재한다.
Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다
스프링이 사용하는 다이나믹 프록시에는 2가지 방법이 있다.
JDK Dynamic Proxy
CGLib Proxy
스프링에서는 기본적으로 jdk dynamic proxy, 스프링 부트에서는 CGLib Proxy 방식으로 AOP를 사용한다.
JDK Dynamin Proxy, CGLib Proxy의 차이를 알아보자.
JDK Dynamic Proxy는 Proxy Factory에 의해 런타임시 동적으로 만들어지는 오브젝트이다. JDK Dynamic Proxy는 반드시 인터페이스가 정의되어있고, 인터페이스에 대한 명세를 기준으로 Proxy를 생성한다. 즉, 인터페이스 선언에 대한 강제성이 있다는 단점이 있다.
내부적으로 JDK Dynamic Proxy에서는 InvationHandler라는 인터페이스를 구현해 만들어지는데, invoke 함수를 오버라이딩하여 Proxy의 위임 기능을 수행한다. 이 과정에서 객체에 대한 Reflection
기능을 사용해 구현하기 때문에 퍼포먼스 하락의 원인이 되기도 한다.
CGLIB Proxy는 순수 Java JDK 라이브러리를 이용하는 것이 아닌 CGLIB
라는 외부 라이브러리를 추가해야만 사용할 수 있다. CGLIB의 Enhancer 클래스를 바탕으로 Proxy를 생성하며, 인터페이스가 없어도 Proxy를 생성할 수 있다. CGBLIB Proxy는 타겟 클래스를 상속
받아 생성하기 때문에 Proxy를 생성하기 위해 인터페이스를 만들어야하는 수고를 덜 수 있다.
하지만, 상속을 이용하므로 final이나 private와 같이 상속에 대해 오버라이딩을 지원하지 않는 경우에는 Aspect를 적용할 수 없다는 단점이 있다.
CGLIB Proxy는 바이트 코드를 조작해서 프록시 객체를 생성하므로 JDK Dynamic Proxy보다 퍼포먼스가 빠른 장점이 있다.
참고 출처 : https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html