Tools/Spring

스프링 AOP 기본 동작 원리

칼쵸쵸 2021. 2. 27. 20:47

스프링 AOP

- OOP를 보완하기 위해 ASPECT를 모듈화 할 수 있는 프로그래밍 기법

구성요소

1. Aspect : 묶어 놓은 모듈

2. Target : 적용이 되는 대상

3. Advice : Aspect 안의 기능

4. Join Point : 적용 시점

5. Point cut : 적용해야 할 위치

 

AOP 적용 방식

1) 컴파일 : 컴파일 시 이미 코드 내용대로 조작이된 바이트코드를 만들어냄

2) 로딩 타임 : 컴파일에서는 저굥되지 않고 각 Aspect를 따로 바이트 코드로 가지고 있지만 로드시 설정한 대로 로드 후 생성

3) 런타임 : 스프링 안에서 사용

A라는 클래스 타입의 Bean을 만들 때 A*인 "프록시 빈" 을 생성 후 Aspect를 호출

 

PROXY 패턴

프록시와 리얼 서브젝트가 공유하는 인터페이스가 있고 실제 서비스 사용자는 본래의 동작 ( 리얼 프로젝트의 목적 ) 을 프록시 인터페이스를 통해 실행한다.

실제 사용자는 프록시를 거쳐서 리얼 프로젝트에 접근하게 되므로 클라이언트가 해당 동작을 수행하기 위해서는 인터페이스로 접근 -> 프록시 동작 -> 실제 동작 순으로 동작하게 된다.

리얼 프로젝트는 SRP원칙을 지키면서 프록시가 부가적인 일을 담당하게 한다 ( 접근 제한 , 로깅 ,트랜잭션 )

 

Interface

public interface ProxyInterface {
    void SayHello();
}

Proxy

public class Proxy implements ProxyInterface{

    ProxyInterface realsubject;

    //실제 객체를 생성자로 넘겨 받음
    public Proxy(ProxyInterface realsubject) {
        this.realsubject = realsubject;
    }

    //실제 객체 동작을 포함함
    @Override
    public void SayHello() {
        System.out.println("Proxy Hello");
        realsubject.SayHello();
    }
}

Real Subject

public class Real implements ProxyInterface{
    @Override
    public void SayHello() {
        System.out.println("Real Hello");
    }
}

Main

public class Main {
    public static void main(String args[])
    {
    	//프록시 객체에 생성자로 리얼 서브젝트를 넘겨서 실행
        //인터페이스를 구현한 객체는 모두 프록시 적용 가능
        ProxyInterface proxy = new Proxy(new Real());
        proxy.SayHello();
    }
}

 

실제 동작인 Real Subject를 동작하기 위해서는 Interface를 통해 구현된 Proxy를 시켜서 실행한다.

Real Subject는 수정되지 않고 추가 기능을 넣을 수 있다.

인터페이스를 통해 받으므로 여러가지 ProxyInterface를 구현한 객체에 대해서 Proxy패턴을 적용 할 수 있게 된다.

 

(단점)

1. 인터페이스를 만들기 번거롭다.

2. 리얼 서브젝트에 더 많은 메서드에 대해서 적용하려고 하면 똑같은 일을 하는 프록시 객체에 메서드를 계속 구현 해야한다.

//문제가 있는 프록시 객체
public class Proxy implements ProxyInterface{

    ProxyInterface realsubject;

    public Proxy(ProxyInterface realsubject) {
        this.realsubject = realsubject;
    }

//Hello 출력
    @Override
    public void SayHello() {
        System.out.println("Proxy Hello");
        realsubject.SayHello();
    }
    
//Hello를 출력하는 똑같은 기능의 메서드이지만 리얼 서브젝트의 기능이 달라 같은 코드를 반복해야함
    @Override
    public void Sayhi() {
        System.out.println("Proxy Hello");
        realsubject.Sayhi();
    }
    
}

 

다이나믹 프록시 인스턴스

위의 문제를 해결하기 위해 런타임시점에서 프록시 객체 인스턴스를 구현함

1. Real Subject를 생성하는 순간에 프록시 인스턴스를 구현함

2. Proxy Interface의 클래스 로더를 조작해서 프록시 인스턴스 생성

3. 해당 인스턴스가 어떤 인터페이스의 구현체인지 정의 ( 클래스 배열 )

4. Invocation Handler를 통해서 생성되는 순간에 프록시 객체의 동작 정의

5. 구현된 클래스는 Object 타입을 리턴함 (타입 캐스팅 필요)

6. 동적 생성함으로써 객체 구조를 바꾸지 않을 수 있으며 프록시 객체 구현이 필요 없다.

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

public class Main {
    public static void main(String args[])
    {
        //다이나믹 프록시 인스턴스
        ProxyInterface dinamicProxy = (ProxyInterface) Proxy.newProxyInstance(
                //프록시 인터페이스의 클래스로더
                // 클래스 인터페이스 내용을 배열로 전달
                //InvocationHandler를 통한 프록시 동작
                ProxyInterface.class.getClassLoader(),
                new Class[]{ProxyInterface.class},
                new InvocationHandler()
                {
                    ProxyInterface real = new Real(); //실제 객체 생성
                    @Override
                    public Object invoke(
                            Object proxy,
                            Method method,
                            Object[] args) throws Throwable {
                        // 프록시 동작 정의
                        System.out.println("Proxy Hello");

                        // 리플렉션 Method 기능을 통해 real 에서
                        // 인터페이스에 정의된 메서드 실행시 프록시 동작
                        // 특정 메서드에서만 동작하게 하려면 if문으로 제어가능
                        Object object = method.invoke(real);

                        return object;
                    }
                });

        //동적 프록시 객체에서 SayHello (리얼 서브젝트의 동작)
        dinamicProxy.SayHello();
    }
}

 

단점)

유연한 구조가 아니며 동적 프록시 생성 단계가 복잡함

인터페이스가 필요함

 

CGLIB를 활용한 클래스의 프록시

스프링 AOP가 사용하는 기본 동작 방식

인터페이스의 구현 없이 프록시 생성

동적 프록시를 생성하는 가장 유연한 방식

 

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

import java.lang.reflect.Method;

public class main {
    public static void main(String[] args) {

        //핸들러 정의
        MethodInterceptor handler = new MethodInterceptor() {
            RealSubject real = new RealSubject();
            @Override
            public Object intercept(Object o,
                                    Method method,
                                    Object[] args,
                                    MethodProxy methodProxy) throws Throwable {
                //프록시 정의
                //특정 메서드 실행시 프록시 동작
                if(method.getName().equals("SayHello")) {
                    //프록시 동작 정의
                    System.out.println("Proxy before Hello");
                    //인터페이스가 아니라 인스턴스 자체를 전달
                    Object object = method.invoke(real);
                    System.out.println("Proxy after Hello");
                    return object;
                }
                return method.invoke(real);
            }
        };
        //cglib의 Enhancer를 사용하여 객체 생성
        RealSubject real = (RealSubject) Enhancer.create(RealSubject.class,handler);
        real.SayHello();
    }
}

 

스프링 AOP는 위의 내용을 내장하고 있으며 인터페이스 없이 프록시를 동작시킬 수 있다.

또한 어떤 클래스에 대해서도 프록시를 정의 할 수 있으며 동적이고 유연한 객체 관계를 정의 할 수 있다.

다만 CGlib가 상속을 기반으로 동작하기 때문에

상속이 불가능한 객체에 대해서는 AOP를 적용할 수 없음 (final 클래스 , private default 생성자 사용)

인터페이스가 존재한다면 인터페이스의 프록시를 만들어 사용하는 것이 안전하다.

'Tools > Spring' 카테고리의 다른 글

스프링 AOP 적용 예제  (0) 2021.02.28
Component와 Component Scan  (0) 2020.08.09
ApplicationContext-2 (Autowired)  (0) 2020.08.06
ApplicationContext-1 (Bean을 등록하는 방법)  (0) 2020.08.04
Spring Ioc 컨테이너와 Bean  (0) 2020.08.04