티스토리 뷰

728x90
반응형

 

지난 글에서 자바 AOP를 구현하는 방법으로 AspectJ에 대해 알아보았다.

 

https://gnidinger.tistory.com/460

 

[Spring]AOP - AspectJ, Pointcut Expression, Join Point

AspectJ AspectJ는 미국의 팰로앨토 연구소(영어: Palo Alto Research Center, PARC)에서 개발한 자바 프로그래밍 언어용 AOP 확장 기능(라이브러리)이다. 조금 더 구체적으로 AspectJ는 애너테이션이 있는 일반.

gnidinger.tistory.com

 

위 그림처럼 스프링 AOP는 호출 시점에 IoC 컨테이너에 의해 AOP를 할 수 있는 프록시 빈을 생성한다.

 

동적으로 생성된 프록시 빈은 타겟의 메서드가 호출되는 시점에 부가기능을 추가할 메서드를 자체적으로 판단하고 가로채서

 

부가기능을 주입하게 되는데, 이것을 호출 시점에 동적으로 위빙을 한다 하여 런타임 위빙(Runtime Weaving)이라 한다.

 

스프링 AOP는 이 런타임 위빙을 기반으로 하고 있으며, 프록시 메커니즘을 기반으로 두 가지 AOP Proxy를 제공한다.

 

그것이 바로 JDK Dynamic Proxy와 CGLib이다.

 

각각 타겟의 인터페이스의 유무에 따라

 

  • 인터페이스 기반의 프록시 생성 시 Dynamic Proxy를,
  • 인터페이스 기반이 아닌 프록시 생성 시 CGLib

을 사용하는데, 현재 스프링 부트 AOP에서는 CGLib을 기본 프록시 패턴으로 사용하고 있다고 한다.

 

이 글에서는 스프링 없이 순수한 자바로 두 방식을 살펴봄으로써 스프링 AOP에 대해 이해를 더하고자 한다.

 

 

1. JDK Dynamic Proxy

 

JDK Dynamic Proxy란 리플렉션의 Proxy 클래스가 동적으로 프록시를 생성해준다고 해서 붙은 이름이다.

 

인터페이스를 기반으로 프록시를 생성해주기 때문에 인터페이스의 존재가 필수적이다.

 

구체적으로는 Java.lang.reflect.Proxy 클래스의 newProxyInstance() 메서드를 이용해 프록시 객체를 생성한다.

 

  • 리플렉션?

    • 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메서드, 타입, 필드 등)에 접근할 수 있게 해주는 자바 API
    • JVM이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 static 영역에 저장
    • 리플렉션 API는 이를 바탕으로 필요한 정보를 가져옴
    • 무거운 API라 성능이 저하된다는 단점이 있음

시작하기 전에 build.gradle에 다음과 같은 의존성을 추가해 주자.

dependencies {
	implementation 'junit:junit:4.13.2'
	implementation group: 'cglib', name: 'cglib', version: '3.3.0'
}

JUnit 테스트 도구와 CGLib 라이브러리를 불러오는 코드이다.

 

계속해서 가장 중요한 인터페이스로 시작하자.

package dynamicProxy;

public interface Animal {
    void eat();
    void drink();
}

이어서 인터페이스를 구현해 타겟을 만든다.

package dynamicProxy;

public class Cat implements Animal{
    @Override
    public void eat() {
        System.out.println("고양이가 사료를 먹습니다.");
    }

    @Override
    public void drink() {
        System.out.println("고양이가 물을 마십니다.");
    }
}
package dynamicProxy;

public class Dog implements Animal{
    @Override
    public void eat() {
        System.out.println("강아지가 사료를 먹습니다.");
    }

    @Override
    public void drink() {
        System.out.println("강아지가 물을 마십니다.");
    }
}

Anmail 인터페이스를 구현해 Cat, Dog 클래스를 타겟으로서 만들었으며,

 

메서드의 앞 뒤로 메세지를 찍고 실행 시간을 찍어보려고 한다.

 

계속해서 InvocationHandler를 상속받아 프록시 객체를 생성할 때 필요한 핸들러를 구현한다.

package dynamicProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class AnimalProxyHandler implements InvocationHandler {

    Object target;

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

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

        if(method.getName().equals("eat")) { // 메서드가 eat 이라면

            long begin = System.currentTimeMillis(); // 시간 측정 시작

            System.out.println("----사료 먹기 전----");
            result = method.invoke(target, args); // 메서드 호출
            System.out.println("----사료 먹기 끝----");

            System.out.println("메서드 실행에 걸린 시간(ms) = " + (System.currentTimeMillis() - begin)); // 시간 측정 끝

        } else if(method.getName().equals("drink")) { // 메서드가 drink 라면

            long begin = System.currentTimeMillis(); // 시간 측정 시작

            System.out.println("----물 마시기 전----");
            result = method.invoke(target, args); // 메서드 호출
            System.out.println("----물 마시기 끝----");

            System.out.println("메서드 실행에 걸린 시간(ms) = " + (System.currentTimeMillis() - begin)); // 시간 측정 끝
        }

        return result; // 호출결과 반환
    }
}

메서드 호출 시점을 가로채 와서 앞 뒤로 메세지를 찍고 소요시간을 측정했다.

 

계속해서 테스트코드의 작성이다.

 

위에 적었듯이 Java.lang.reflect.Proxy 클래스의 newProxyInstance() 메서드를 이용해 객체를 생성한다.

package dynamicProxy;

import org.junit.Test;
import java.lang.reflect.Proxy;

public class DynamicProxyTests {
    @Test
    public void dynamicProxyExample() {
        // 동적으로 프록시 생성
        // cat
        Animal cat = (Animal) Proxy.newProxyInstance(Animal.class.getClassLoader(), // 클래스 로더
                new Class[]{Animal.class}, // 타겟 인터페이스
                new AnimalProxyHandler(new Cat())); // 타겟의 정보가 포함된 핸들러
        // dog
        Animal dog = (Animal) Proxy.newProxyInstance(Animal.class.getClassLoader(),
                new Class[]{Animal.class},
                new AnimalProxyHandler(new Dog()));

        // 프록시를 통해 타겟 인스턴스 메서드 호출
        cat.eat();
        System.out.println();
        cat.drink();
        System.out.println();

        dog.eat();
        System.out.println();
        dog.drink();
        System.out.println();
    }
}

이때 구체적인 객체 생성 과정을 살피면 아래와 같다.

 

  1. 타겟의 인터페이스에 대해 자체적인 검증 로직 실행
  2. ProxyFactory에 의해 타겟의 인터페이스를 상속한 Proxy 객체 생성
  3. 프록시 객체에 핸들러를 포함시켜 하나의 객체로 반환

핵심은 인터페이스를 기준으로 프록시 객체를 생성한다는 것이다. 따라서 구현체는 인터페이스를 상속받아야 한다.

// 실행 결과
----사료 먹기 전----
고양이가 사료를 먹습니다.
----사료 먹기 끝----
메서드 실행에 걸린 시간(ms) = 0

----물 마시기 전----
고양이가 물을 마십니다.
----물 마시기 끝----
메서드 실행에 걸린 시간(ms) = 1

----사료 먹기 전----
강아지가 사료를 먹습니다.
----사료 먹기 끝----
메서드 실행에 걸린 시간(ms) = 0

----물 마시기 전----
강아지가 물을 마십니다.
----물 마시기 끝----
메서드 실행에 걸린 시간(ms) = 1

결과도 의도한대로 잘 나온 것을 확인할 수 있다.

 

끝으로 JDK Dynamic Proxy의 장점과 단점에 대해 짚고 CGLib으로 넘어가자.

 

  • 장점 - 개발자가 직접 프록시 객체를 만들 필요가 없다.
  • 단점 - 타겟은 반드시 인터페이스여야 한다. 리플렉션 API를 사용해 무겁다.

 

2. CGLib(Code Generator Library)

 

CGLib은 인터페이스가 아닌 클래스의 바이트코드를 조작하여 프록시 객체를 생성해주는 라이브러리를 말한다.

 

CGLib에선 인터페이스가 필요하지 않으니 클래스부터 작성한다.

package cglib;

public class Cat {
    public void eat() { System.out.println("고양이가 사료를 먹습니다."); }

    public void drink() { System.out.println("고양이가 물을 마십니다."); }
}

CGLib을 사용하여 프록시 객체를 생성하는 건 크게 두 단계로 나뉜다.

 

  • net.sf.cglib.proxy.Enhancer 클래스를 사용하여 프록시 객체 생성
  • net.sf.cglib.proxy.Callback을 조작

생성은 위에 적힌 대로 Enhancer 클래스를 사용해서 이루어지는데, 그 과정은 아래와 같다.

 

CGLib은 타겟의 클래스를 상속받아 프록시 객체를 생성한다.

 

이 과정에서 CGLib은 타겟 클래스에 포함된 모든 메서드를 재정의하여 프록시를 생성하는데, 

 

이 때문에 Final 메서드 또는 클래스를 이용해(재정의를 할 수 없으므로) 프록시를 생성할 수 없다는 단점이 생긴다.

 

하지만 CGlib은 바이트 코드를 조작하여 프록시를 생성해주기 때문에 성능이 JDK Dynamic Proxy보다 뛰어나다고 한다.

 

계속해서 생성한 프록시 객체 조작은 다시 두 방법으로 나뉘는데,

 

  • InvocationHandler
  • MethodInterceptor

가 그것이다. 일반적으로는 MethodInterceptor가 쓰인다고 한다. 두 방법에 대해 하나씩 알아보자.

 

 

2-1. InvocationHandler

 

InvocationHandler 방식은 바이트코드 조작이 아니라 JDK Dynamic Proxy와 마찬가지로 리플렉션을 사용한다.

 

따라서 코드는 한 줄만 빼고 위의 JDK Dynamic Proxy에서 사용했던 InvocationHandler코드와 일치한다.

package cglib;

import net.sf.cglib.proxy.InvocationHandler;
import java.lang.reflect.Method;

//CGLib 에서 InvocationHandler 사용하기
public class AnimalProxyCGLibHandler implements InvocationHandler {

    Object target;

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

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

        long begin = System.currentTimeMillis(); // 시간 측정 시작

        if(method.getName().equals("eat")) { // 메서드가 eat 이라면

            System.out.println("----사료 먹기 전----");
            result = method.invoke(target, args); // 메서드 호출
            System.out.println("----사료 먹기 끝----");

        } else if(method.getName().equals("drink")) { // 메서드가 drink 라면

            System.out.println("----물 마시기 전----");
            result = method.invoke(target, args); // 메서드 호출
            System.out.println("----물 마시기 끝----");

        }

        System.out.println("메서드 실행에 걸린 시간(ms) = " + (System.currentTimeMillis() - begin)); // 시간 측정 끝

        return result; // 호출결과 반환
    }
}

import 시 java.lang.reflect.InvocationHandler를 net.sf.cglib.proxy.InvocationHandler로 교체한 것을 볼 수 있다.

 

계속해서 Enhancer 클래스를 사용해 프록시 객체를 생성한다.

 

이어서 setCallback() 메서드를 이용해 핸들러를 지정하게 된다.

package cglib;

import net.sf.cglib.proxy.Enhancer;
import org.junit.Test;

public class byHandlerTests {
    @Test
    public void byHandlerExample() {

        Enhancer catEnhancer = new Enhancer(); // Enhancer 객체 생성
        catEnhancer.setSuperclass(Cat.class); // 타겟 클래스
        catEnhancer.setCallback(new AnimalProxyCGLibHandler(new Cat())); // 핸들러 지정
        Cat cat = (Cat) catEnhancer.create(); // 프록시 생성

        cat.eat();
        System.out.println();
        cat.drink();
    }
}
// 실행 결과
----사료 먹기 전----
고양이가 사료를 먹습니다.
----사료 먹기 끝----
메서드 실행에 걸린 시간(ms) = 1

----물 마시기 전----
고양이가 물을 마십니다.
----물 마시기 끝----
메서드 실행에 걸린 시간(ms) = 0

결과는 동일하게 나온 것을 확인할 수 있다.

 

 

2-2. MethodInterceptor

 

MethodInterceptor를 상속받아 같은 기능을 하는 인터셉터를 작성해 보겠다.

 

클래스는 위와 같은 Cat 하나만 가지고 진행한다.

package interceptor;

public class Cat {
    public void eat() {
        System.out.println("고양이가 사료를 먹습니다.");
    }

    public void drink() {
        System.out.println("고양이가 물을 마십니다.");
    }
}
package interceptor;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;


public class PrintLogInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = null;

        long begin = System.currentTimeMillis();  // 시간 측정 시작
        System.out.println("----메서드 실행 전----");
        result = methodProxy.invokeSuper(o, objects);
        System.out.println("----메서드 실행 후----");
        System.out.println("메서드 실행에 걸린 시간(ms) = " + (System.currentTimeMillis() - begin)); // 시간 측정 끝

        return result;
    }
}

이어서 테스트 코드를 작성해 실행 결과를 확인하자.

package interceptor;

import org.junit.Test;
import net.sf.cglib.proxy.Enhancer;
public class byInterceptorTests {
    @Test
    public void byInterceptorExample() {

        Enhancer catEnhancer = new Enhancer(); // Enhancer 객체 생성
        catEnhancer.setSuperclass(Cat.class); // 타겟 클래스
        catEnhancer.setCallback(new PrintLogInterceptor()); // 로그 출력해주는 인터셉터 지정
        Cat cat = (Cat) catEnhancer.create(); // 프록시 생성

        cat.eat();
        System.out.println();
        cat.drink();
    }
}
// 실행 결과
----메서드 실행 전----
고양이가 사료를 먹습니다.
----메서드 실행 후----
메서드 실행에 걸린 시간(ms) = 19

----메서드 실행 전----
고양이가 물을 마십니다.
----메서드 실행 후----
메서드 실행에 걸린 시간(ms) = 0

setCallback() 메서드를 이용해 인터셉터를 적용했기 때문에 eat과 drink에 같은 로그가 나오는 걸 확인할 수 있다.

 

메서드에 따라 다른 인터셉터를 적용할 수는 없을까? 이때 사용하는 것이 CallbackFilter이다.

 

 

2-2-1. CallbackFilter 구현

 

CallbackFilter를 구현한 클래스를 만들어 accept() 메서드를 재정의해 사용한다.

 

accept() 메서드는 int형의 index값을 반환하는데, 이 값을 이용해 setCallbacks 배열에 있는 인터셉터를 적용한다.

 

먼저 구현 클래스를 만들자.

package interceptor;

import net.sf.cglib.proxy.CallbackFilter;
import java.lang.reflect.Method;

// 메서드가 eat 이냐 drink 냐에 따라 해당하는 인덱스 반환
public class AnimalMethodCallbackFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
        if(method.getName().equals("eat")) return 0;
        if(method.getName().equals("drink")) return 1;
        return 0; // 기본 값 = 0
    }
}

계속해서 적용하고자 하는 인터셉터를 구현한다.

package interceptor;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class EatInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = null;

        long begin = System.currentTimeMillis(); // 시간 측정 시작
        System.out.println("----사료 먹기 전----");
        result = methodProxy.invokeSuper(o, objects);
        System.out.println("----사료 먹기 끝----");
        System.out.println("메서드 실행에 걸린 시간(ms) = " + (System.currentTimeMillis() - begin)); // 시간 측정 끝

        return result;
    }
}
package interceptor;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class DrinkInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = null;

        long begin = System.currentTimeMillis(); // 시간 측정 시작
        System.out.println("----물 마시기 전----");
        result = methodProxy.invokeSuper(o, objects);
        System.out.println("----물 마시기 끝----");
        System.out.println("메서드 실행에 걸린 시간(ms) = " + (System.currentTimeMillis() - begin)); // 시간 측정 끝

        return result;
    }
}

마지막으로 테스트코드를 작성한다.

 

setCallbackFilter() 메서드를 통해 CallbackFilter를 적용하고,

 

setCallbacks() 메서드를 통해 인터셉터 배열을 적용한다.

package interceptor;

import org.junit.Test;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;

public class callbackFilterTests {
    @Test
    public void callbackFilterExample() { // 콜백 필터를 이용해 필터 조건(메서드)에 따라 다른 인터셉터 적용

        Enhancer catEnhancer = new Enhancer(); // Enhancer 객체 생성
        catEnhancer.setSuperclass(Cat.class); // 타겟 클래스
        // 메서드 이름에 따라 인덱스 반환해주는 콜백 필터 지정, 0 반환 시 EatInterceptor, 1 반환 시 DrinkInterceptor
        catEnhancer.setCallbackFilter(new AnimalMethodCallbackFilter());
        catEnhancer.setCallbacks(new Callback[]{ // 콜백 배열 지정
                new EatInterceptor(), // 0
                new DrinkInterceptor() // 1
        });
        Cat cat = (Cat) catEnhancer.create(); // 프록시 생성

        cat.eat();
        System.out.println();
        cat.drink();
    }
}
----사료 먹기 전----
고양이가 사료를 먹습니다.
----사료 먹기 끝----
메서드 실행에 걸린 시간(ms) = 25

----물 마시기 전----
고양이가 물을 마십니다.
----물 마시기 끝----
메서드 실행에 걸린 시간(ms) = 1

메서드를 호출할 때마다 이름에 따라 해당하는 인터셉터를 적용하는 것을 확인할 수 있다.

 

끝으로 CGLib의 장점과 단점에 대해 짚고 글을 마치자.

 

  • 장점 - JDK Dynamic Proxy에 비해 성능이 좋다.
  • 단점 - Final 메서드/클래스를 이용해 프록시를 생성할 수 없다.

전체 실습 코드는 아래 주소에 있다.

 

https://github.com/gnidinger/JDK-Dynamic-Proxy-And-CGLib

 

GitHub - gnidinger/JDK-Dynamic-Proxy-And-CGLib

Contribute to gnidinger/JDK-Dynamic-Proxy-And-CGLib development by creating an account on GitHub.

github.com

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함