관점 지향 프로그래밍으로 핵심 기능과 부가 기능으로 나누고 그 관점을 기준으로 각각을 모듈화 하는 것 (여기서
Aspect(관점)
이란 흩어진 관심사들을 하나로 모듈화 한 것을 의미)
- Spring AOP는 기본적으로
프록시 방식
으로 동작한다.
프록시 패턴이란 어떤 객체를 사용하고자 할 때,
객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 말한다.
객체 지항 프로그래밍(OOP)에서는 주요 관심사에 따라 클래스를 분할한다.
이 클래스들은 보통 SRP(Single Responsibility Principle)에 따라 하나의 책임만을 갖게 설계된다. 하지만 클래스를 설계하다보면 로깅, 보안, 트랜잭션 등 여러 클래스에서 공통적으로 사용하는 부가 기능들이 생긴다.
이들은 주요 비즈니스 로직은 아니지만, 반복적으로 여러 곳에서 쓰이는 데 이를 흩어진 관심사(Cross Cutting Concerns)
라고 한다.
주문하는 로직이 있을 때, 프로젝트의 요구 사항으로 모든 메소드의 실행 시간을 로그로 남겨야 한다면 아래와 같이 로그를 남기는 코드를 메소드에 포함시키는 방법이 있을 수 있다.
하지만 이건 주문을 하는 핵심기능
과 시간을 측정하는 부가 기능
이 하나의 메서드 안에 공존하게 된다. 또한 모든 로직에 적용하려면 로그를 남기는 코드가 중복으로 작성될 것이다
(수정을 할 경우가 생긴다면 로그 코드를 전부 수정해야한다)
이러한 문제점인흩어진 관심사를 별도의 클래스로 모듈화하여 위의 문제들을 해결하고,
결과적으로 OOP를 더욱 잘 지킬 수 있도록 도움을 주는 것이 AOP이다.
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
public Order orderItem(String itemId) {
long startTime = System.currentTimeMillis();
Order order = new Order(itemId);
Order result = orderRepository.save(order);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("orderItem(). resultTime = {}", resultTime);
return result;
}
}
이 방법은 클라이언트가 프록시에 요청하는 모양이 된다.
클라이언트가 프록시의 존재를 알 필요 없이 AOP를 요청하는 방법은 다음 스프링이 AOP를 만드는 2가지 방법을 참고
@Slf4j
@RequiredArgsConstructor
public class OrderServiceProxy{
private final OrderService target;
public Order orderItem(String itemId) {
long startTime = System.currentTimeMillis();
Order result = target.orderItem(itemId); // 핵심기능은 타겟에게 요청
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("orderItem(). resultTime = {}", resultTime);
return result;
}
}
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public Order orderItem(String itemId) {
Order order = new Order(itemId);
return orderRepository.save(order);
}
}
: Spring에서는 Target 클래스 혹은 그의 상위 인터페이스를 상속하는 프록시 클래스를 생성하고, 프록시 클래스에서 부가 기능에 관련된 처리를 한다. 이렇게 하면 Target에서 Aspect을 알 필요 없이 순수한 비즈니스 로직에 집중할 수 있다.
예를 들어 다음 코드의 logic() 메서드가 Target이라면,
public interface TargetService{
void logic();
}
@Service
public class TargetServiceImpl implements TargetService{
@Override
public void logic() {
...
}}
@Service
public class TargetServiceProxy implements TargetService{
// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
TargetService targetService = new TargetServiceImpl();
...
@Override
public void logic() {
// Target 호출 이전에 처리해야하는 부가 기능
// Target 호출
targetService.logic();
// Target 호출 이후에 처리해야하는 부가 기능
}
}
@Service
public class UseService{
// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
TargetService targetService = new TargetServiceProxy();
...
public void useLogic() {
// Target 호출하는 것처럼 부가 기능이 추가된 Proxy를 호출한다.
targetService.logic();
}
}
최종적으로 빈으로 등록하는 것은 프록시 객체이다.
클라이언트 입장에서는 주입받은 빈이 프록시인지 아닌지 모르고 단순히 인터페이스에만 의존하여 요청을 보내지만 런타임시에는 프록시가 먼저 요청을 받아서 실행 시간과 관련된 작업을 한 뒤에 타겟에서 핵심 기능을 요청하는 흐름이다.
구체 클래스 상속도 인터페이스 방식과 마찬가지로 DI를 이용한 방식입니다.
@Slf4j
public class OrderServiceProxy extends OrderService{
private final OrderService target;
public OrderServiceProxy(OrderRepository orderRepository, OrderService target) {
super(null);
this.target = target;
}
public Order orderItem(String itemId) {
long startTime = System.currentTimeMillis();
Order result = target.orderItem(itemId); // target 호출
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("orderItem(). resultTime = {}", resultTime);
return result;
}
}
@Configuration
public class OrderConfig {
@Bean
public OrderService orderService() {
OrderService target = new OrderService(orderRepository());
return new OrderServiceProxy(null, target);
}
@Bean
public OrderRepository orderRepository() {
OrderRepository target = new OrderRepository();
return new OrderRepositoryProxy(target);
}
}
위 코드에서 Service와 Repository 모두 프록시를 빈으로 등록하였습니다. 따라서, 클라이언트는 코드 상으로는 OrderService만 알고 있지만 실제로 주입되는 빈은 OrderServiceProxy인 것
스프링은 프록시를 만들고 빈으로 등록하는 작업을 모두 대신해준다.
개발자는 어떤 부가기능을 수행할 것인지 정하고 (어드바이스),
그 부가기능을 어떤 클래스 혹은 메서드에 적용할 것인지 필터링하는 것(포인트컷)만 해주면 된다.
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("execution(* hello.aop..*.*(..))")
public Object logAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 핵심 기능은 타겟에게 위임
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("orderItem(). resultTime = {}", resultTime);
return result;
}
}
Spring에서 AOP는 프록시 방식으로 동작한다. (프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 Wrapping 오브젝트이다)
Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다
Spring AOP에서는 JDK Dynamic Proxy 와 CGlib 을 통해 Proxy화 한다
JDK Dynamic Proxy는 Reflection을 기반으로 이루어지고, CGlib 은 상속을 기반으로 이루어진다
참고
- https://pingpongdev.tistory.com/24
- https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC
- https://chalchichi.tistory.com/41
- https://riverblue.tistory.com/64?category=753253
- https://m.blog.naver.com/whdgml1996/222023198373
- https://steady-coding.tistory.com/608
- https://hyungyu-lee.github.io/articles/2019-10/how-spring-aop-works
- https://velog.io/@ha0kim/2020-12-28-AOP-%ED%8A%B9%EC%A7%95-%EB%B0%8F-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC