티스토리 뷰

AOP란?

Aspect Oriented Programming, 관점 지향 프로그래밍
횡단 관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임

 

횡단 관심사란

어플리케이션의 여러 부분에 걸쳐있는 기능을 의미

➡️로깅과 트랜잭션, 인증처리와 같은 시스템 공통 처리 영역을 말합니다.

 

👾 예를 들면 결제 시스템에서 결제 전과 후에 로깅이 이루어지는 경우,

로깅 작업은 결제 메서드에서 공통적으로 이용되는 부분이므로 횡단 관심사가 됩니다.

신용카드_결제(){
    // 결제 시작 전 로그
    // 로직
    // 결제 종료 후 로그
}

포인트_결제(){
    // 결제 시작 전 로그
    // 로직
    // 결제 종료 후 로그
}

쿠폰_결제(){
    // 결제 시작 전 로그
    // 로직
    // 결제 종료 후 로그
}

 

AOP를 이용해서 횡단관심사를 분리함으로, 핵심기능과 부가기능을 구분할 수 있습니다.

 

AOP 사용하면 얻을 수 있는 장점

  • 횡단관심사를 분리하면, 이후에 수정할때도 번거롭게 모든 메서드를 수정하지 않아도 된다
  • 반복되는 코드가 줄어든다.
  • 재사용성이 높아진다.
  • 비즈니스 로직에 집중할 수 있다.

 

대표적인 AOP 적용 방식 3가지

1️⃣ 컴파일 시점

  • .java 파일을 .class 파일로 변환하는 과정에서 AspectJ 컴파일러가 부가 기능 로직을 붙이는 방식
  • 특별한 컴파일러가 필요하여 복잡함..

 

2️⃣ 클래스 로딩 시점

  • .class 파일을 JVM에 저장하기 전에 코드 조작을 하는 것이다. 많은 모니터링 툴들이 사용하는 방식이다.
  • 클래스 로더 조작기를 지정해야 하는 부분이 번거롭고 운영하기 어렵다.

 

3️⃣ 런타임 시점

  • 메인 메서드가 실행된 다음으로, 자바 언어가 제공하는 범위 안에서 부가 기능을 적용해야 한다.
  • DI, BeanPostProcessor 같은 개념들을 사용하면 최종적으로 프록시를 통해 스프링 빈에 부가 기능을 적용할 수 있다.
  • 프록시 방식의 AOP
    • 프록시를 사용하기 때문에 AOP 기능에 일부 제약이 있다.
    • 프록시에서 target의 메서드를 호출하기 때문에 생성자 등의 조작이 불가능
  • 컴파일 시점처럼 특별한 컴파일러나 클래스 로딩시점처럼 클래스 로더 조작기를 설정하지 않아도 된다.
  • 스프링 AOP가 사용하는 방식이다.

 


Spring에서 AOP 프록시 구현하는 방법

JDK Proxy와 CGLib Proxy 두가지가 있다.

 

1️⃣ JDK Dynamic Proxy

  • Interface를 기반으로 Proxy를 생성해주는 방식
  • 인터페이스를 기반으로 프록시를 생성해주기 때문에 인터페이스의 존재가 필수적이다.
  • Java.lang.reflect.Proxy 클래스의 newProxyInstance() 메소드를 이용해 프록시 객체를 생성한다.

 

➕ Reflection 리플렉션

자바 코드 자체를 추상화하여 구체적인 객체정보를 알지 못하더라도 클래스정보들을 접근 할 수 있도록 하는 자바 API 입니다. 이를 통해 동적 객체 선언, 동적 메서드 호출 기능을 사용 할 수 있는데, Spring에서는 DI, Proxy등에서 리플렉션이 사용됩니다.

 

🧙자바의 Reflection API는 값비싼 API이기 때문에 Dynamic Proxy는 리플렉션을 하는 과정에서 성능이 떨어집니다.

 

1. 결제 인터페이스

public interface Payment {

    void payByCreditCard();

    void payByPoint();

    void payByCoupon();
}

 

2. 결제 인터페이스 구현

public class Movie implements Payment {

    @Override
    public void payByCreditCard() {
        System.out.println("신용카드로 결제합니다.");
    }

    @Override
    public void payByPoint() {
        System.out.println("포인트로 결제합니다.");
    }

    @Override
    public void payByCoupon() {
        System.out.println("쿠폰으로 결제합니다.");
    }
}

 

3. 프록시 핸들러 구현

import java.lang.reflect.InvocationHandler;

public class PaymentProxyHandler implements InvocationHandler {

    private static final Logger logger = LoggerFactory.getLogger(PaymentProxyHandler.class);
    Object target;

    public PaymentProxyHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;

        logger.info("{} 호출 전", method.getName());
        result = method.invoke(target, args);
        logger.info("{} 호출 후", method.getName());

        return result;
    }
}

 

다이내믹 프록시의 invoke 메소드의 매개변수

  • proxy: Object - 다이내믹 프록시 객체
  • method: Method - 현재 처리 중인 메소드
  • args: Object[] - 메소드에 전달된 인자들

 

테스트

public class DynamicProxyTest {

    @Test
    public void pay() {
        Payment movie = (Payment) Proxy.newProxyInstance(Payment.class.getClassLoader(),
                new Class[]{Payment.class},
                new PaymentProxyHandler(new Movie()));
        // 각 매개변수의 역할은
        // 1. ClassLoader: 동적으로 생성된 프록시 클래스의 로딩에 사용될 클래스 로더
        // 2. Interface array: 생성될 프록시 클래스가 구현할 인터페이스들
        // 3. InvocationHandler: 프록시가 호출되었을 때 실제로 수행될 로직을 정의한 핸들러

        movie.payByCreditCard();
        System.out.println();
        movie.payByPoint();
        System.out.println();
        movie.payByCoupon();
    }
}

 

 

➡️ Payment 인터페이스를 구현한 객체에 대한 동적 프록시를 생성하고,

프록시에는 PaymentProxyHandler라는 핸들러가 추가되어 호출될 때의 동작을 정의합니다.

이 핸들러는 Movie 객체를 사용하여 실제 메소드 호출을 처리합니다.

 


 

다이나믹 프록시는 인터페이스가 필수라는 특징이 있습니다.

인터페이스가 없는 경우에 프록시 패턴을 사용하고 싶다면 CGLib를 통해 사용할 수 있습니다.

 

2️⃣ CGLib Proxy

  • 인터페이스가 아닌 클래스 기반으로 바이트코드를 조작하여 프록시를 생성하는 방식입니다.

 

1. CGLib 의존성 추가

implementation 'net.sourceforge.cglib:com.springsource.net.sf.cglib:2.1.3'

 

2. 클래스 작성

public class Movie2 {

    public void payByCreditCard() {
        System.out.println("신용카드로 결제합니다.");
    }

    public void payByPoint() {
        System.out.println("포인트로 결제합니다.");
    }

    public void payByCoupon() {
        System.out.println("쿠폰으로 결제합니다.");
    }

}

 

3. 프록시 핸들러 구현

CGLib를 사용하여 프록시 생성 시에는 아래 두 가지 작업을 필요로 합니다.

1️⃣ Enhancer 클래스를 사용하여 원하는 프록시 객체 만들기

2️⃣ Callback을 사용하여 프록시 객체 조작하기

 

JDK Dynamic Proxy에서 사용한 InvocationHandler 방식과 MethodInterceptor 방식을 통해 핸들러를 적용할 수 있습니다.
InvocationHandler 방식을 사용하면 바이트코드 조작이 아니라 JDK Dynamic Proxy와 유사하게 리플렉션을 활용하기 때문에, CGLib의 성능을 활용하기 위해서는 일반적으로 MethodInterceptor를 사용한다고 합니다.

 

1. InvocationHandler 방식으로 핸들러 구현

import org.springframework.cglib.proxy.InvocationHandler;

public class PaymentProxyCGLibHandler implements InvocationHandler {

    private static final Logger logger = LoggerFactory.getLogger(PaymentProxyCGLibHandler.class);
    Object target;

    public PaymentProxyCGLibHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;

        logger.info("{} 호출 전", method.getName());
        result = method.invoke(target, args);
        logger.info("{} 호출 후", method.getName());

        return result;
    }
}

 

테스트

CGLib에서는 Enhancer를 통해 프록시를 생성합니다.

@Test
public void cglibProxyInvocationHandlerTest() {
    Enhancer movieEnhancer = new Enhancer();
    // 프록시 할 클래스 지정
    movieEnhancer.setSuperclass(Movie2.class);
    // 핸들러 지정
    movieEnhancer.setCallback(new PaymentProxyCGLibHandler(new Movie2()));
    // 프록시 생성
    Movie2 movie2 = (Movie2) movieEnhancer.create();

    movie2.payByCreditCard();
    System.out.println();
    movie2.payByPoint();
    System.out.println();
    movie2.payByCoupon();
}

 

2. MethodInterceptor 방식으로 핸들러 구현

public class PaymentInterceptor implements MethodInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(PaymentInterceptor.class);


    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        Object result = null;
        logger.info("{} 호출 전", method.getName());
        result = proxy.invokeSuper(obj, args);
        logger.info("{} 호출 후", method.getName());
        return result;
    }
}

 

PaymentInterceptor의 intercept 메소드의 매개변수

  • obj: Object - 프록시된 객체
  • method: Method - 현재 처리 중인 메소드
  • args: Object[] - 메소드에 전달된 인자들
  • proxy: MethodProxy - CGLIB의 메소드 프록시 객체

 

테스트

@Test
public void cglibProxyMethodInterceptorTest() {
    Enhancer movieEnhancer = new Enhancer();
    // 프록시 할 클래스 지정
    movieEnhancer.setSuperclass(Movie2.class);
    // 핸들러 지정
    movieEnhancer.setCallback(new PaymentInterceptor());
    // 프록시 생성
    Movie2 movie2 = (Movie2) movieEnhancer.create();

    movie2.payByCreditCard();
    System.out.println();
    movie2.payByPoint();
    System.out.println();
    movie2.payByCoupon();
}

 


필터 조건에 따라 다른 인터셉터 적용하기

setCallback() 메서드를 이용해 인터셉터를 적용하는 대신, 필터 조건에 따라 다른 인터셉터를 적용하고 싶다면 Callback Filter을 구현하면 됩니다.

 

CallbackFilter를 구현 시에는 accept() 메서드를 재정의합니다.

 

accept() 메서드는 정수 인덱스값을 반환하므로 이를 이용해 callback 배열에 있는 인터셉터를 적용할 수 있습니다.

 

사실 해당 예제는 필터 조건을 걸지 않아도 되지만, 코드로 살펴보기 위해 굳이 한번 적용해봤습니다~

 

1. CallbackFilter의 accept 메서드 재정의

public class PaymentMethodCallbackFilter implements CallbackFilter {

    @Override
    public int accept(Method method) {
        if(method.getName().equals("payByCreditCard")) return 0;
        if(method.getName().equals("payByPoint")) return 1;
        if(method.getName().equals("payByCoupon")) return 2;
        return 0;
    }
}

 

2. 각 인덱스에 대해 인터셉터를 생성

public class PayByCreditCardInterceptor implements MethodInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(PayByCreditCardInterceptor.class);

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        Object result = null;

        logger.info("{} 호출 전", method.getName());
        result = proxy.invokeSuper(obj, args);
        logger.info("{} 호출 후", method.getName());

        return result;
    }
}

public class PayByPointCardInterceptor implements MethodInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(PayByPointCardInterceptor.class);

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        Object result = null;

        logger.info("{} 호출 전", method.getName());
        result = proxy.invokeSuper(obj, args);
        logger.info("{} 호출 후", method.getName());

        return result;
    }
}

public class PayByCouponCardInterceptor implements MethodInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(PayByCouponCardInterceptor.class);

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        Object result = null;

        logger.info("{} 호출 전", method.getName());
        result = proxy.invokeSuper(obj, args);
        logger.info("{} 호출 후", method.getName());

        return result;
    }
}

 

테스트

@Test
public void callbackFilterTest() {
    Enhancer movieEnhancer = new Enhancer();
    // 프록시 할 클래스 지정
    movieEnhancer.setSuperclass(MoviePay2.class);
    // 메서드 이름에 따라 인덱스 반환 해주는 콜백 필터 지정
    movieEnhancer.setCallbackFilter(new PaymentMethodCallbackFilter());
    // 콜백 배열 지정
    movieEnhancer.setCallbacks(new Callback[]{
            new PayByCreditCardInterceptor(), // 0
            new PayByPointCardInterceptor(), // 1
            new PayByCouponCardInterceptor() // 2
    });
    // 프록시 생성
    MoviePay2 moviePay2 = (MoviePay2) movieEnhancer.create();

    moviePay2.payByCreditCard();
    System.out.println();
    moviePay2.payByPoint();
    System.out.println();
    moviePay2.payByCoupon();
}

 


스프링에서의 AOP는 어떻게 적용될까?

스프링부트에서는 성능이 좋은 CGLib으로 Proxy를 구현한다.

 

🧙용어 정리

타겟(Target)

  • 핵심 기능을 담고 있는 모듈로서 부가기능을 부여할 대상, 즉 AOP를 적용할 대상

조인포인트(Join Point)

  • 어드바이스가 적용될 수 있는 위치
  • 부가기능을 적용할 수 있는 지점

포인트컷(PointCut)

  • 어드바이스를 적용할 타겟의 메서드를 선별하는 정규표현식

애스펙트(Aspect)

  • 어드바이스 + 포인트 컷
  • 스프링에서는 Aspect를 빈으로 등록해서 사용한다.

어드바이스(Advice)

  • 타겟의 특정 조인포인트에 제공할 부가기능

 

타겟 메서드의 Aspect 실행 시점을 지정할 수 있는 어노테이션

@Before, @After, @Around, @AfterReturning, @AfterThrowing 등이 있습니다.

➡️ 이는 어느 시점에 부가기능을 적용시킬지를 의미합니다.

 

 

 

위빙(Weaving)

  • 타겟의 조인 포인트에 어드바이즈를 적용하는 과정

 

코드로 살펴보기

결제 전 후에 로그를 출력한다.

방법1️⃣ @Before과 @After를 이용해 로그를 출력한다.

방법2️⃣ @Around를 이용해 타겟 메서드 실행 전 후에 로그를 출력한다.

 

1. AOP 의존성 추가 

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

2. Aspect 빈으로 등록하기

@Aspect 어노테이션을 통해, 해당 클래스가 Aspect를 나타내는 클래스 임을 명시하고

@Component 어노테이션을 통해, 스프링 빈으로 등록합니다.

@Component
@Aspect
public class PayLoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(PayLoggingAspect.class);

    @Pointcut("execution(* com.example.aop.SpringAop.MoviePay3.*(..))")
    private void allPay(){}

    @Before("execution(* com.example.aop.SpringAop.MoviePay3.*(..))")
    public void logBeforePay(JoinPoint joinPoint) throws Throwable {
        logger.info("{} 호출 전", joinPoint.getTarget().getClass().getName());
    }

    @After("allPay()")
    public void logAfterPay(JoinPoint joinPoint) throws Throwable {
        logger.info("{} 호출 후", joinPoint.getTarget().getClass().getName());
    }

//    @Around("com.example.aop.SpringAop.Movie3..*()")
//    public Object logAroundPay(ProceedingJoinPoint joinPoint) throws Throwable {
//        logger.info("{} 호출 전", joinPoint.getTarget().getClass().getName());
//
//        Object result = joinPoint.proceed();
//
//        logger.info("{} 호출 전", joinPoint.getTarget().getClass().getName());
//
//        return result;
//    }
}

 

테스트

@SpringBootTest
public class ProxyTest {

    @Autowired
    MoviePay3 moviePay3;

    @Test
    public void springAopTestWithBean() {
        moviePay3.payByCreditCard();
        System.out.println();
        moviePay3.payByPoint();
        System.out.println();
        moviePay3.payByCoupon();
    }
}

 

 

⚠️주의. Spring AOP는 등록된 Bean에 대해서만 프록시 객체가 만들어져 적용됩니다.

아래와 같이 일반 객체 생성 시에는 AOP가 적용되지 않는 것을 확인할 수 있습니다.

@Test
public void springAopTestWithoutBean() {
    MoviePay3 moviePay3 = new MoviePay3();

    moviePay3.payByCreditCard();
    System.out.println();
    moviePay3.payByPoint();
    System.out.println();
    moviePay3.payByCoupon();
}

 

 


참고

https://velog.io/@suhongkim98/JDK-Dynamic-Proxy%EC%99%80-CGLib

 

JDK Dynamic Proxy와 CGLib를 알아보자 #2

Dynamic Proxy와 CGLib을 실습해보며 AOP를 이해해보았습니다.

velog.io

 

https://jiwondev.tistory.com/152

 

자바 AOP의 모든 것(Spring AOP & AspectJ)

이 글은 사전지식이 없다면 읽기 어려울 수 있다. 바이트코드와 리플렉션을 모른다면 아래의 글을 꼭 읽어보도록하자. 2021.08.17 - [기본 지식/Java 기본지식] - 바이트코드 조작(리플렉션, 다이나믹

jiwondev.tistory.com