Spring AOP

dev_note·2022년 5월 15일
0
post-thumbnail

Proxy 란?

Proxy 는 사전적인 의미로 “대리인"이라는 뜻입니다. java 에서 프록시란 대리를 수행하는 클래스를 의미합니다. Proxy 는 Client 가 사용하려고 하는 실제 대상인 것 처럼 위장을 해서 클라이언트의 요청을 받아줍니다. 여기서 위장이란 "다형성"을 의미합니다.

Proxy 를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 target 또는 real subject(실체)라 부릅니다.

Proxy 는 실제로 타겟이 담당하는 역할 요청을 대신받아서 요청 이전, 이후에 대한 로직을 추가할 수 있는 객체입니다. 이렇게 하면 실제 타겟의 코드는 수정하지 않으면서 기능적인 추가를 할 수 있다는 장점이 있습니다.

Proxy 구현

proxy 를 구현하기 위해서는 인터페이스를 이용한 방법과 상속을 이용한 방법이 있습니다.

  1. target 과 같은 인터페이스를 구현
  2. proxy 가 target 을 제어할 수 있는 위치에 존재

(2번이 말이 조금 어려운데, 저는 "접근 가능한 package 위치에 있다는 의미"로 이해를 했습니다. 예를 들어 접근제어자가 default 인 메소드를 제어하고 싶으면 같은 패키지에 존재해야 한다의 의미정도로 생각합니다. 제 이해가 틀렸다면 지적해주시면 감사하겠습니다 😃 )

Proxy 구현 예제

서비스를 실행하는 시간을 구하는 로깅 Proxy 를 구현해보려고 합니다.

OrderService.java

public interface OrderService {
    void orderGoods(User buyer, List<Goods> goods);
}

Proxy 객체와 Target 이 공유할 수 있는 인터페이스 입니다.

OrderServiceImpl.java

public class OrderServiceImpl implements OrderService {

    private List<Order> orderRepository = new ArrayList<>();

    @Override
    public void orderGoods(User buyer, List<Goods> goods) {
        ...
    }
}

OrderServiceImpl 는 OrderService 인터페이스를 구현합니다. Proxy 의 Target 에 해당하는 클래스입니다.

LoggingProxy.java

public class LoggingProxy implements OrderService {

    private OrderService orderService;

    public LoggingProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void orderGoods(User buyer, List<Goods> goods) {
        long startTime = System.nanoTime(); // 시간측정 start
        orderService.orderGoods(buyer, goods); // 본래 클라이언트가 요청했던 동작 
        long endTime = System.nanoTime(); // 시간측정 end
        System.out.println((endTime - startTime) + " nanoseconds");
    }
}

LoggingProxy 클래스가 Target 클래스인 OrderServiceImpl 과 동일한 OrderService 인터페이스를 implements 했기 때문에 클라이언트 입장에서 Proxy 객체를 OrderService 변수로 참조할 수 있게 되었습니다. 예를 들면 아래와 같습니다.

private OrderService orderService = new LoggingProxy();

(LoggingProxy 객체는 클라이언트가 직접 생성하지 않습니다.)

이렇게 함으로써 OrderService 가 제공하는 메서드를 호출할 수 있습니다. 마치 OrderService 가 동작하는 것 처럼 보입니다.
이제 이 코드를 실행시켜보려고 합니다.

Runner.java

public static void main(String[] args) {
    OrderService orderService = new OrderServiceImpl();
    LoggingProxy proxy = new LoggingProxy(orderService);

    User buyer = new User();
    buyer.setUsername("주문지");
    buyer.setUserId(1);
    buyer.setAddress("구로");

    Goods goods = new Goods();
    goods.setName("치킨");
    goods.setPrice(10000d);
    goods.setGoodsId(1);

    proxy.orderGoods(buyer, Arrays.asList(goods));
}

Runner 는 클라이언트 코드를 의미합니다. 스프링을 사용해보신 분들은 아시겠지만 우리가 직접 orderService 생성하지 않습니다.
스프링 프레임워크에서 클라이언트는 아래과 같이 코드를 작성했을 것입니다.

@Controller
public class SpringController {
    @Autowired
    private OrderService orderService;

    @RequestMapping(value = "order", method = RequestMethod.POST)
    public void order(int userId, List<Integer> goodsId) {
        // ... id 로 객체를 find 한다
        orderService.orderGoods(buyer, goods);
    }
}

OrderService 인스턴스는 스프링을 프레임 워크를 통해서 주입받았을 것입니다. 따라서 스프링이 언제든 OrderService 인스턴스 대신에 LoggingProxy 인스턴스를 넘겨줄 수 있다는 것이 핵심입니다.
그리고 클라이언트는 프록시가 아닌 OrderService 인스턴스가 orderService 변수에 DI 되었을 것이라고 가정하고 코드를 작성합니다.

AOP

AOP 는 Aspect Oriented Programming 의 약자로 관점 지향 프로그래밍이라고 불립니다. 즉, 로직을 기준으로 관점 별로 로직을 묶어서 모듈화 할 수 있도록 돕겠다는 취지에서 나온 프로그래밍 기법이라고 할 수 있습니다.

예를 들어서 보안, 로깅, 데이터베이스 연결, 캐싱, 트랜잭션, 비즈니스 로직 등등 애플리케이션이 동작하기 위해서 해주어야할 처리가 있는데 이를 하나의 Service 클래스에서 관리한다면 어떻게 될까요?
애플리케이션 여러 목적을 하는 코드가 하나의 Service 클래스에 있기 때문에 코드를 파악하기 어려워질 것입니다.

반면 특정 위치에 있는 코드의 목적이 “보안"이라는 주제로 명확하게 정해져 있다면 코드를 파악하기가 쉬워집니다. “보안"이라는 카테고리 안에서 코드의 의도를 파악하기 시작할 수 있기 때문입니다.

이런 주제를 “관심사”라고 부릅니다. AOP 라는 기술을 통해서 클라이언트가, 관심사별로 코드를 모듈화 할 수 있도록 해줍니다.

AOP 가 왜 필요했을까?

객체지향 기술은 분명 성공적인 프로그래밍 방식이었지만, 복잡해져가는 애플리케이션의 요구조건과 기술적인 부분을 모두 해결하기에는 난해함을 가지고 있었습니다.

예를 들어 객체 지향 원칙을 지키면서 기술적인 트랜잭션 & 로깅 등등을 객체지향 기술만으로 처리하기에는 문제가 있었습니다.


출처 : https://gmoon92.github.io/spring/aop/2019/02/09/why-used-aop.html

위 그림을 보면 서비스 비즈니스 로직을 담은 this.userRepo.transferUserAmt(user); 코드 이외에 위아래로 트랜잭션을 처리하기 위한 코드가 존재합니다. 이는 비즈니스 로직과는 관련이 없지만 필수적인 부가 기능입니다. 비즈니스를 처리하기 위한 목적을 가지고 AccountService 클래스를 열었다면 읽기가 난해함을 느낄 것입니다.

AOP 는 이런 객체지향 기술의 한계와 단점을 극복하기 위해 나왔습니다. 한편 AOP 를 사용하면 그 결과로 OOP 를 더더욱 OOP 답게 만들 수 있습니다.

위빙

AOP 를 이해하려면 위빙이라는 단어를 이해해야 합니다. 위빙은 원본 로직에 부가 기능을 추가하는 것을 의미합니다. 위빙을 하는 방법은 다양하게 있을 것입니다. 코드를 위빙한다고 가정하면 어떻게 사용자 코드 사이에 위빙을 할 수 있을까요??
예를 들면 컴파일 하기 전에 .java 파일에 필요한 코드를 앞뒤로 삽입한 후 컴파일하는 방법이 있을 수 있겠네요.
이렇게 위빙을 하는 시점이 언제냐에 따라서 종류가 크게 3가지로 나누어 집니다.

  • 컴파일 시점 위빙
  • 클래스 로딩 시점 위빙
  • 런타임 시점 위빙


출처 : https://bepoz-study-diary.tistory.com/408

java 코드를 추가하는 방법입니다. 컴파일 시점에 .java 파일을 .class 파일로 변환하는 과정에서 AspectJ 컴파일러가 부가 기능 로직을 붙이는 방식입니다. 컴파일된 .class 를 디컴파일 해보면 AspectJ 관련 호출 코드가 들어감을 알 수 있습니다.

컴파일 시점 위빙의 단점은 특별한 컴파일러가 필요하고 복잡하다는 점입니다. 기본 컴파일러는 java 코드를 변경할 수 없기 때문에 특별한 컴파일러를 사용해야 합니다.

애플리케이션 구동을 위해서는 클래스로더라는 것을 통해서 JVM 에 클래스 정보를 올리는 과정을 거쳐야 합니다. 이 때 JVM 에 올라간 .class 정보를 토대로 코드가 실행됩니다.

클래스 로딩 시점 위빙은 .class 파일을 JVM 에 저장하기 전(클래스로드 타임 전)에 코드를 조작합니다. 그리고 많은 JVM 모니터링 툴들이 사용하는 방법이다.

로드타임 위빙의 단점은 클래스 로더 조작기를 직접 조작해야 한다는 점입니다. 자바 실행 시 특별한 옵션 (java -javaagent)을 통해 클래스 로더 조작기를 지정해야하는데 이 부분이 번거롭고 운영하기 어렵게 만드는 요인입니다.

위에서 배웠던, 프록시 방식의 AOP 를 의미합니다. 이 방법을 사용하면 “컴파일타임 위빙”이나 “로드타임 위빙"의 불편한 사항들이 개선됩니다. 컴파일 시점 처럼 특별한 컴파일러를 사용한다던가 클래스 로더 조작기를 설정하지 않아도 됩니다.

런타임에 스프링으로 부터 필요한 객체를 전달 받아서 “빈포스트프로세서"라는 곳에서 객체를 프록시 객체로 변경 해줍니다.

Spring 에서 AOP 적용 기법

AOP 는 위에서 언급했던 프록시라는 기술을 사용해서 구현됩니다. Spring AOP 는 Proxy 메커니즘을 기반으로 AOP Proxy 를 제공하고 있다. 그리고 IoC 즉, 스프링이 관리하는 컨테이너에서 관리하는 빈 객체만 Proxy 를 적용할 수 있다는 말이기도 합니다.

스프링에서 프록시를 적용하는 방법에는 2가지가 있습니다.

  1. JDK Dynamic Proxy
  2. CGLIB 라이브러리를 이용한 Proxy

자바에서 제공하는 다이나믹 프록시를 이용하는 방법

jdk runtime proxy 라고도 부르는 이 기술은 다음과 같이 사용합니다.

Object proxy = Proxy.newProxyInstance(ClassLoader       // 클래스로더
                                    , Class<?>[]        // 타깃의 인터페이스
                                    , InvocationHandler // 타깃의 정보가 포함된 Handler
              										  );

리플랙션을 지원하는 Proxy 클래스의 newProxyInstance 메소드를 사용하면 됩니다. JDK Dynamic Proxy 의 newProxyInstance 메소드가 Proxy 객체를 생성해주는 과정은 다음과 같습니다.


출처 : https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html

  1. Target 의 인터페이스를 자체적인 검증 로직을 통해 검증 (access level, 인터페이스가 맞는지 등)
  2. ProxyFactory 에 의해 타깃의 인터페이스를 상속한 Proxy 객체 생성
  3. Proxy 객체에 InvocationHandler 를 포함시킨 하나의 객체로 반환

JDK Runtime Proxy 는 인터페이스를 기준으로 Proxy 객체를 생성해줍니다. 그렇기 때문에 인터페이스가 아닌 타입의 변수에 JDK Runtime Proxy 를 DI 하려고 하면 예외를 발생시킬 수 있습니다.

인터페이스 타입이 아닌 클래스 타입으로 DI

@RequiredArgsConstructor
@Service
public class CardServiceImpl implements CardService {
		...
}

@RestController
public class CardController {

		private CardServiceImpl cardService;

    public CardController(CardServiceImpl cardService) {
        this.cardService = cardService;
    }
}

예외 로그

The bean is of type 'com.sun.proxy.$Proxy169' and implements:
	com.slack.slack.domain.service.CardService
	org.springframework.aop.SpringProxy
	org.springframework.aop.framework.Advised
	org.springframework.core.DecoratingProxy

Expected a bean of type 'com.slack.slack.domain.service.impl.CardServiceImpl' which implements:
	com.slack.slack.domain.service.CardService

Action:

Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.

Jdk Dynamic Proxy 기반으로 동작하지 않아요!

테스트를 위해서 Spring 에서 인터페이스가 아닌 타입으로 DI 를 받으려고 시도했지만 예외가 발생하지 않았습니다. 이유를 찾기 위해 디버깅 해보니 Jdk Dynamic 프록시가 아닌 GCLIB 을 기반으로 동작하고 있었습니다.

왜 그럴까 검색하다가 다음과 같은 글을 발견했습니다.

spring boot aop 에서 JDK dynamic proxy 이용하는 법

스프링을 Proxy 기반으로 AOP 를 동작시키려면 @EnableAspectJAutoProxy 어노테이션을 설정해야 하는데 이를 아래와 같은 내용을 설정파일에 추가해야 JDK Runtime Proxy 기반으로 동작시킬 수 있다고 합니다.

spring:
  aop:
    auto: false
    proxy-target-class: false

스프링에서 제공하는 CGLIB 을 사용하는 방법

CGLib 은 Code Generator Library 의 약자로 클래스의 바이트코드를 조작하여 Proxy 객체를 생성해주는 라이브러리입니다.
Spring CGLib 을 사용하여 인터페이스가 아닌 타깃의 클래스에 대해서도 Proxy 를 생성해줄 수 있습니다.

CGLib 은 Enhancer 라는 클래스를 통해 Proxy 를 생성할 수 있습니다.

Enhancer enhancer = new Enhancer();
         enhancer.setSuperclass(MemberService.class); // 타깃 클래스
         enhancer.setCallback(MethodInterceptor);     // Handler
Object proxy = enhancer.create(); // Proxy 생성

CGLib 은 타깃의 클래스를 상속받아 다음 그림과 같이 프록시를 생성해줍니다.

  1. Enhance 가 Target 의 Class 를 상속 받는다.
  2. 해당 메소드를 Handler 로 재정의하여 Proxy 를 생성해줍니다.

이런 특성 때문에 Final 메소드 또는 클래스에 대해서 재정의를 할 수 없기 때문에 Proxy 를 생성할 수 없다는 단점이 있지만 GCLib 은 바이트코드를 조작해서 Proxy 를 생성해주기 때문에 JDK Dynamic Proxy 보다 성능이 좋습니다.

예제

public static OrderServiceImpl getOrderServiceProxy(OrderServiceImpl target) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(OrderServiceImpl.class); // 타깃 클래스
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

            long startTime = System.nanoTime();
            method.invoke(target, args);
            long endTime = System.nanoTime();
            System.out.println((endTime - startTime) + " nanoseconds");

            return null;
        }
    });     // Handler
    Object proxy = enhancer.create(); // Proxy 생성

    return (OrderServiceImpl)proxy;
}

Jdk runtime Proxy 와 CGLib 성능비교

AOP Benchmark

스프링이 CGLib 을 권장하지 않았던 이유는 뭘까?

다음과 같은 3가지 한계가 존재했기 때문입니다.

  • net.sf.cglib.proxy.Enhancer 의존성 추가
  • default 생성자
  • 타깃의 생성자 두 번 호출

CGLib의 개선

  • Enhancer 의존성은 Spring 에서 기본적으로 제공하지 않았었는데 Spring Core 패키지에 이를 추가하여 개선했습니다. 더이상 의존성을 별도로 추가하지 않아도 CGlib 을 이용해서 개발할 수 있게 되었습니다.
  • CGLib 을 구현하기 위해서는 default 생성자가 필요했던 문제를 Objensis 라이브러리의 도움을 받아 default 생성자 없이도 Proxy 를 생성할 수 있도록 개선했습니다.
  • Objensis 라이브러리의 도움을 받아 생성자를 2번 호출하던 문제도 개선했습니다.

⇒ Spring 은 JDK Dynamic Proxy 를 사용해서 인터페이스 방식의 DI 를 지원해주고 인터페이스가 아닌 타입에 대해서 CGlib 으로 Proxy 를 DI 해줍니다.

그러나 상속을 기반으로 하여, 인터페이스가 아닌 타입의 DI가 가능하다는 점, CGlib 이 가지고 있던 한계를 극복했습니다.
그래서 현재 Spring 은 Proxy 를 생성하는 방법으로 CGlib 을 사용합니다. 기본 설정이 CGlib 를 이용한 Proxy 생성입니다.

AOP 구현

스프링이 제공하는 aop 기능을 구현해보고 싶었습니다. AspectJ 가 아닌, 어노테이션을 이용해서 Aspect 코드를 삽입하는 방법으로 AOP 를 구현하려고 합니다.

즉, 스프링 프레임워크의 일부 기능을 구현해보려고 합니다. 이 프레임워크의 이름을 임시로 Look 이라고 부르겠습니다.

코드는 아래 저장소에 있습니다.
https://github.com/donghyeon0725/study/tree/master/src/main/java/com/studyall/study/proxy/aop

스토리

아무런 스토리도 없이 구현하면 왜 이런 코드가 나왔는지 이해가 안될 수 있기 때문에 스토리를 추가하려고 합니다. 클라이언트 입장입니다. 현재 클라이언트는 Look 프레임워크를 이용해서 다음과 같은 코드를 작성하고 싶은 상황입니다.

  • Service 를 호출하기 전과 후에 profile 을 측정하고 싶다. 이 때 Look 프레임워크가 제공하는 AOP 기능을 이용해서 profile 을 측정하는 코드를 구현하고 싶다.

구현 패키지

client 패키지는 사용자 입장에서 사용자가 작성한 코드를 모아놓은 패키지이고 Look 패키지는 프레임워크가 제공하는 클래스를 모아놓은 패키지입니다.

클라이언트가 알아야할 어노테이션?

클라이언트가 사용하기 어려운 프레임워크는 대체적으로 인기가 없습니다. 제가 만든 프레임워크는 세계정복(?)을 목표로 하고 있기 때문에 쉬워야 합니다. 그렇기 때문에 사용자가 AOP 사용을 위해서 알아야 할 클래스가 적으면 적을 수록 좋습니다.

클라이언트 입장에서 알아야 할 클래스와 어노테이션은 다음과 같습니다.

  • @Service
  • @Aspect
  • AspectI

AOP 의 목적은 코드의 분리입니다.

나(클라이언트)는 코드를 분리해서 로직을 작성할 테니, 이 코드를 프레임워크가 알아서 다른 코드 사이에 끼워 넣어주기를 바래 ~

이점을 고려해서 AOP 코드를 삽입해줄 수 있도록 설계했습니다. 여기서 클라이언트가 위빙 하려는 로직을 Aspect 라고 부릅니다. 마찬가지로 Look 프레임워크도 이를 Aspect 라고 부르겠습니다.

@Aspect 는 Aspect 클래스를 만들기 위해서 사용하는 어노테이션이고, AspectI 는 Aspect 클래스가 따라야할 표준을 정의한 인터페이스입니다.

어노테이션

어노테이션설명
@Service서비스 빈 생성을 위한 어노테이션
@AspectAspect 클래스를 만들 때 사용하는 어노테이션. 이 어노테이션과 type 타입만 명시해주면 ApplicationContext 에 저장된 해당 Bean 을 찾아 Aspect 로직이 들어있는 Proxy 로 만들어준다.

스프링 프레임워크에서 @Service 와 @Aspect 어노테이션이 붙은 경우 모두 Bean 으로 관리를 하고 있습니다. 마찬가지로 Look을 구현할 때 두 객체 모두 빈으로 관리할 예정입니다.

어플리케이션 구동을 위한 주요 클래스 & 인터페이스

인터페이스클래스 (or 구현체)설명
ApplicationContextLookApplicationContext생성한 빈을 관리하는 Context 객체입니다.
ApplicationRunner프레임워크의 컨테이너에 해당하는 ApplicationContainer 클래스를 초기화 하고 애플리케이션을 로드합니다
ApplicationContainer프레임워크를 구동시키는 컨테이너 입니다. 필요한 모듈을 new 생성자를 통해서 생성합니다. 빈 프로세서가 빈을 ApplicationContext 에 주입하고 어노테이션 핸들러를 체인에 추가(add) 합니다. 이 후 체인에 들어있는 핸들러들을 꺼내 어노테이션을 핸들링합니다.
BeanProcessorDefaultBeanProcessor빈을 인스턴스화 합니다. BeanDefinitionFinder 를 통해서 로드한 package 의 클래스정보를 로드한 후 Bean 대상이되는 클래스를 찾아 인스턴스화 합니다. 이 후 ApplicationContext 에 추가(add)합니다.
AnnotationHandlerChainAnnotationHandler 구현체를 담아두는 chain 입니다.
chain 에서 AnnotationHandler 를 꺼내어 어노테이션을 핸들링힙니다. 커스텀으로 추가가 가능합니다.AnnotationHandler
ProxyCreatorCGlib 을 기반으로 사용자가 작성한 Aspect 클래스를 빈에 위빙 해준다.

어플리케이션을 구동하기 위해서 Look은 빈 객체를 생성하고 이를 프록시로 만들어줄 것 입니다. 구동 흐름은 다음과 같습니다.

애플리케이션 구동흐름

ApplicationRunner → ApplicationContainer → BeanDefinitionFinder → BeanProcessor → AnnotationHandlerChain → AspectHandler → ProxyCreator

ApplicationRunner.java

public static void main(String[] args) {
    ApplicationContainer applicationContainer = new ApplicationContainer();
    applicationContainer.init(args);

    ApplicationContext context = applicationContainer.getContext();
    MemberService service = context.getBean(MemberServiceImpl.class.getName(), MemberService.class);
    service.createMember("유저1");
}

애플리케이션의 시작점입니다. 마치 SpringBoot 에서 @SpringBootApplication 어노테이션이 붙은 클래스와 유사합니다.

ApplicationContainer 의 init 메소드를 호출함으로서 애플리케이션을 시작합니다. 그리고, 아직은 구현하지 않았지만 applicationContainer 에서 ApplicationContext 를 꺼내어 MemberService 의 createMember 메소드를 호출하는 과정은 나중에 스프링에서 @Autowired 어노테이션을 통해서 빈을 주입받는 DI 로 구현할 예정입니다.

ApplicationContainer.java

public class ApplicationContainer {
    private String path;
    private ApplicationContext context;
    private BeanDefinitionFinder finder;
    private BeanProcessor processor;
    private AnnotationHandlerChain annotationHandlerChain;

    public void init(String[] args) {
        // 필요한 인스턴스 로드 => 구현체 로드
        createInstance();

        // bean processing
        processor.process(finder, context);

				// add annotation handlers
        addAllAnnotationHandler();

        // annotation handling
        annotationHandlerChain.handle(context);
    }

		...
}

ApplicationContainer 애플리케이션이 구동되기 위해서 필요한 의존성을 정의합니다. 느슨한 결합으로 구현된 클래스들에 구체적인 객체를 주입해줄 수 있도록 의존성을 정의하고 해당 객체를 생성하는 과정을 createInstance 메소드가 담당합니다.

createInstance 메소드 호출이 끝나면 빈을 로드하는 processor 가 동작합니다. 별도의 메소드로 만들어 loadBean 이라는 이름을 붙여주면 더 좋았겠지만, 시간상 그렇게 하지 못했습니다.

addAllAnnotationHandler 에서는 사용자가 커스텀으로 추가한 어노테이션 핸들러와 Look 프레임워크에서 제공하는 어노테이션 핸들러를 annotationHandlerChain 에 추가합니다.

왜 AnnotationHandlerChain 를 만들었나요?

왜 굳이 annotationHandlerChain 를 사용했으냐면 새로운 어노테이션이 추가되었을 때 기존의 코드를 건드리지 않고, 어노테이션 핸들러를 체인에 추가하고 동작시킬 수 있는 구조로 설계하고 싶었습니다.

addAllAnnotationHandler 메소드 코드를 보면 아래와 같습니다.

// 모든 어노테이션 핸들러 추가
private void addAllAnnotationHandler() {
		AnnotationHandler aopHandler = new AspectHandler(new ProxyCreator());
    this.annotationHandlerChain.add(aopHandler);
    this.addAllCustomAnnotationHandler();
}

지금은 new 키워드를 통해서 AspectHandler 객체를 생성했기 때문에, Look 프레임웤이 제공하는 새로운 어노테이션 핸들러를 추가하기 위해서는 이 코드를 수정해야합니다.

하지만, 추후 스프링이 제공하는 @Scan 어노테이션과 유사하게 어노테이션 스캔을 이용해서 핸들러를 추가하는 코드로 변경을 한다면 새로운 핸들러를 추가하더라도 이 메소드를 변경할 일이 없습니다. 즉 변경에는 닫혀 있고 확장에는 열려 있어야 한다는 OCP 원칙 을 지킬 수 있습니다.

이를 염두해두고 Chain 역할을 할 클래스를 만들었습니다.

AspectHandler.java

target 이 되는 빈을 proxy 객체로 만듭니다. 이 후 Aspect 클래스 빈을 찾아서 target 의 proxy 빈에 위빙(사용자 코드를 끼워 넣음)합니다.

현재는 하나의 Aspect 클래스만 지원합니다. 추 후 기회가 되면 여러 Aspect 클래스를 지원할 수 있도록 확장하려고 합니다.

@Override
public void handle(ApplicationContext applicationContext) {

    // aop 빈을 찾아서 service 빈에 넣어주기
    AspectI aspectI = findAspect(applicationContext);

    Object bean = findTargetBean(applicationContext, aspectI);

    Object proxy = proxyCreator.getProxy(bean, aspectI);

    applicationContext.setBean(bean.getClass().getName(), proxy);
}

ProxyCreator.java

public class ProxyCreator {
    public <T> T getProxy(T target, AspectI aspectI) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass()); // 타깃 클래스
        enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> {
            aspectI.handle(target, method, args);
            return null;
        });     // Handler
        Object proxy = enhancer.create(); // Proxy 생성

        return (T)proxy;
    }
}

마지막으로 프록시 생성기 클래스입니다.

CGlib 라이브러리를 사용해서 이를 구현 하였는데, JDK Runtime Proxy 대신 이를 사용한 주요 이유는 인터페이스 기반이 아닌 객체를 프록시로 만들 수 있기 때문입니다.

AOP 구현을 위해 Client 가 알아야 할 클래스 or 인터페이스

대상타입설명
AspectI인터페이스aop class 가 지켜야할 표준을 정의한 인터페이스 입니다.

사용자는 @Aspect 어노테이션을 제외하고 오직 AspectI 인터페이스만 알고 있으면 Aspect 를 만들 수 있습니다.

출처 & 참고

profile
having a better day than i did yesterday

0개의 댓글