[Spring] AOP 어떻게 동작할까?

may_yun·2023년 4월 27일
1

Spring

목록 보기
2/4

AOP (Aspect Oriented Programming)

관점 지향 프로그래밍으로 핵심 기능과 부가 기능으로 나누고 그 관점을 기준으로 각각을 모듈화 하는 것 (여기서 Aspect(관점)이란 흩어진 관심사들을 하나로 모듈화 한 것을 의미)

  • Spring AOP는 기본적으로 프록시 방식으로 동작한다.
    프록시 패턴이란 어떤 객체를 사용하고자 할 때,
    객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 말한다.

객체 지항 프로그래밍(OOP)에서는 주요 관심사에 따라 클래스를 분할한다.
이 클래스들은 보통 SRP(Single Responsibility Principle)에 따라 하나의 책임만을 갖게 설계된다. 하지만 클래스를 설계하다보면 로깅, 보안, 트랜잭션 등 여러 클래스에서 공통적으로 사용하는 부가 기능들이 생긴다.
이들은 주요 비즈니스 로직은 아니지만, 반복적으로 여러 곳에서 쓰이는 데 이를 흩어진 관심사(Cross Cutting Concerns)라고 한다.

AOP의 주요 개념

  • Aspect: Advice + PointCut로 AOP의 기본 모듈 (묶어 놓은 모듈)
  • Advice: Target에 제공할 부가 기능을 담고 있는 모듈 (Aspect 안의 기능)
  • Target: Advice이 부가 기능을 제공할 대상 (Advice가 적용될 비즈니스 로직) (적용이 되는 대상)
  • JointPoint: Advice가 적용될 위치 (적용 시점)
    • 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용 가능
  • PointCut: Target을 지정하는 정규 표현식 (적용해야 할 위치)

AOP 코드 예시

주문하는 로직이 있을 때, 프로젝트의 요구 사항으로 모든 메소드의 실행 시간을 로그로 남겨야 한다면 아래와 같이 로그를 남기는 코드를 메소드에 포함시키는 방법이 있을 수 있다.
하지만 이건 주문을 하는 핵심기능시간을 측정하는 부가 기능이 하나의 메서드 안에 공존하게 된다. 또한 모든 로직에 적용하려면 로그를 남기는 코드가 중복으로 작성될 것이다
(수정을 할 경우가 생긴다면 로그 코드를 전부 수정해야한다)

  • AOP 없이 관심사를 처리할 경우의 문제
    • 여러 곳에서 반복적인 코드 작성
    • 코드가 변경될 경우 여러 곳에서 수정
    • 주요 비즈니스와 부가 기능이 한 곳에 섞여 가독성이 떨어진다

이러한 문제점인흩어진 관심사를 별도의 클래스로 모듈화하여 위의 문제들을 해결하고,
결과적으로 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;
    }
}

1) 방법: 클라이언트 -> 프록시에 요청

이 방법은 클라이언트가 프록시에 요청하는 모양이 된다.
클라이언트가 프록시의 존재를 알 필요 없이 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);
    }
}

스프링이 AOP를 만드는 2가지 방법

1) 인터페이스 구현 방식 (DI)

: 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();
    }
}

최종적으로 빈으로 등록하는 것은 프록시 객체이다.
클라이언트 입장에서는 주입받은 빈이 프록시인지 아닌지 모르고 단순히 인터페이스에만 의존하여 요청을 보내지만 런타임시에는 프록시가 먼저 요청을 받아서 실행 시간과 관련된 작업을 한 뒤에 타겟에서 핵심 기능을 요청하는 흐름이다.

2) 구체클래스 상속 방식

구체 클래스 상속도 인터페이스 방식과 마찬가지로 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인 것

📌 스프링이 제공하는 AOP 사용법

스프링은 프록시를 만들고 빈으로 등록하는 작업을 모두 대신해준다.
개발자는 어떤 부가기능을 수행할 것인지 정하고 (어드바이스),
그 부가기능을 어떤 클래스 혹은 메서드에 적용할 것인지 필터링하는 것(포인트컷)만 해주면 된다.

  • @Aspect 어노테이션을 붙여 이 클래스 Aspect를 나타내는 클래스라는 것을 명시
  • @Component로 스프링 빈으로 등록
  • @Around : 타켓 메서드를 감싸서 특정 어드바이스를 실행한다는 의미로 아래 코드에서는 hello.aop 패키지와 그 하위 패키지에 있는 모든 클래스와 모든 메서드가 이 Aspect를 적용하겠다는 것
@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;
    }
}
  • 실행 순서
      1. 포인트컷 표현식을 보고 일치하는 클래스들은 프록시를 만들어서 빈으로 등록
      1. 런타임 시 포인트컷에 일치하는 메서드들은 애스펙트에 정의해 놓은 어드바이스 로직을 실행
      1. 타겟의 메서드를 호출

📌Spring AOP 동작 원리

Spring에서 AOP는 프록시 방식으로 동작한다. (프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 Wrapping 오브젝트이다)

  • Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다

  • Spring AOP에서는 JDK Dynamic Proxy 와 CGlib 을 통해 Proxy화 한다

  • JDK Dynamic Proxy는 Reflection을 기반으로 이루어지고, CGlib 은 상속을 기반으로 이루어진다

    1. 클라이언트에서 타겟을 감싸고 있는 프록시 호출 (이때 프록시는 타겟 메소드 실행 전 후로 부가 기능을 실행하도록 구성되어 있다.)
    • 프록시 패턴은 타겟 하나 하나마다 프록시 객체를 정의해야하므로 번거롭고 코드의 중복이 생긴다는 점
    1. 런타임 위빙(Runtime Weaving): 런타임 시 JDK Dynamic Proxy 또는 CGLIB를 활용하여 프록시를 생성(타겟 객체를 새로운 프록시 객체로 적용하는 과정을 의미)

프록시 방식 (런타임 위빙)

  • 프록시 방식에는 두가지가 있는데
    • JDK Dynamic Proxy: 동적 프록시
      • spring AOP 기본 동작 방식
      • Java의 Reflection을 활용해 동적으로 생성: 동적 프록시는 Reflection을 활용해 인터페이스들을 탐색하고, 그만큼 동적으로 프록시를 생성하는 것
      • 인터페이스를 기준으로 Proxy 인스턴스 생성
      • Reflection은 aop를 적용할 메서드가 아니라고 해도 인터페이스에 있는 메서드는 일단 전부 프록시 객체 생성에 불러오기 때문에,상대적으로 CGLib보다 성능이 낮다
    • CGLIB
      • spring boot AOP 기본 동작 방식

참고

profile
개발 일지

0개의 댓글