공부해봅시당

[Spring] 흐름으로 이해해보는 AOP와 프록시 본문

STUDY/Spring

[Spring] 흐름으로 이해해보는 AOP와 프록시

tngus 2024. 3. 2. 03:09

AOP의 개념에 대해 쉽게 이해하고 싶은 사람은 링크를 참조하길 바람

 


목차

1. AOP(Aspect Oriented Programming)란?

2. AOP와 프록시 패턴

3. 프록시 패턴의 단점 해결: 다이내믹 프록시

4. 다이내믹 프록시 단점 해결: 프록시 펙토리(Proxy Factory)

5. 아직도 귀찮다. 더 자동화해보자: 자동 프록시 생성기(Automatic Proxy Creator)


1. AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)란?

전통적인 객체지향 기술의 설계 방법(추상화와 메서드 추출 등)으로는

독립적인 모듈화가 불가능한 부가기능을 모듈화 하기 위해 Aspect(관점)라는 개념을 도입함

 

1-1. Aspect를 통한 부가기능 분리

아래의 그림을 살펴보면 Aspect를 통해 부가기능을 분리하는 방식을 살펴볼 수 있음

왼쪽을 보면 부가기능이 핵심기능의 모듈에 침투해 들어가면서 설계와 코드가 모두 지저분해진 것을 볼 수 있음

오른쪽과 같이 핵심기능과 부가기능을 Aspect를 통해 분리하여

설계와 개발 시에는 다른 특성을 띈 Aspect들을 독립적인 관점으로 작성할 수 있고,

런타임 시에는 왼쪽과 같이 각 부가기능들이 필요한 위치에 다이내믹하게 참여하게 됨

 

이렇게 Aspect를 만들어 설계하고 개발하는 방법을 Aspect 지향 프로그래밍 즉, AOP라고 부름

 

1-2. AOP 관련 용어

아래 본격적인 설명에 들어가기에 앞서, 용어에 대한 정의부터 하고 진행하도록 하겠다

 

1) Joinpoint [조인포인트]

    - 애플리케이션 실행 중에 특정 작업(예: 메소드 호출, 필드 접근 등)이 수행되는 지점

    - 포인트컷의 후보가 되는, 애플리케이션의 핵심 기능을 구성하는 메소드들을 의미

2) Pointcut [포인트컷]

     - 어드바이스가 적용될 조인포인트의 집합을 정의하는 표현식

     - 특정 조건에 해당하는 조인포인트들을 선별하여 어드바이스의 적용 범위를 결정하는 핵심적인 메커니즘

     - 공통 기능을 적용할 특정 조건을 만족하는 조인포인트를 선정

3) Advice [어드바이스]

     - 조인포인트에서 실행될 추가적인 코드(공통 기능)를 담고 있음

     - 로깅, 보안, 트랜잭션 관리 등과 같은 공통적인 작업을 구현

4) Weaving [위빙]

     - 컴파일 타임, 로드 타임, 또는 런타임에 어드바이스를 애플리케이션의 지정된 조인포인트에 삽입하는 과정

     - 포인트컷에 의해 지정된 핵심 메소드가 호출될 때 어드바이스를 적절히 삽입하는 과정

5) Aspect [에스펙트]

     - Pointcut + Advice. 어떤 Pointcut에 어떤 Advice를 실행할지 결정

     - 애플리케이션의 핵심 기능 자체를 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 요소로서

        핵심 기능에 부가적인 의미나 기능을 제공하는 특별한 모듈

 

 

2. AOP와 프록시 패턴

앞서 설명했지만 AOP는 기존의 OOP 방식으로는 부가기능의 모듈화가 어렵다는 단점을 극복하기 위해 등장하였음

AOP의 구현 방법 중 하나로 프록시 패턴이 사용됨

 

2-1. AOP와 프록시 패턴은 무슨 관계가 있을까?

AOP와 프록시의 상관관계

AOP를 사용하여 애플리케이션의 특정 조인 포인트(Join Point, 예: 메소드 호출)에 Aspect를 적용할 때,

프록시 객체를 생성하여 원본 객체를 감싸는 방식으로 Aspect의 코드를 실행하게 됨

 

이 프록시 객체는 원본 객체의 호출을 가로채 Aspect에 정의된 공통 기능(어드바이스, Advice)을 실행한 다음,

원본 객체의 메소드를 호출함

 

이 과정을 통해, 개발자는 핵심 비즈니스 로직을 수정하지 않고도 공통 관심사를 효율적으로 관리할 수 있음

 

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)와 프록시(Proxy)는 자바(Spring Framework 등)와 같은 프로그래밍 언어에서 객체 지향 프로그래밍(OOP)을 보완하는 중요한 개념으로, 모듈성을 향상시키고 코드의 재사용성을 높이는 데 도움을 줍니다. 이 두 개념은 밀접한 상관관계를 가지고 있으며, AOP 구현에 프록시 패턴이 자주 사용됩니다.
AOP (Aspect-Oriented Programming)
AOP는 애플리케이션의 공통적인 관심사(Cross-cutting Concerns)를 분리하여 모듈화하는 프로그래밍 패러다임입니다. 로깅, 보안, 트랜잭션 관리와 같은 기능은 여러 모듈에서 공통적으로 사용되지만, 각 모듈의 핵심 로직과는 별개의 관심사를 나타냅니다. AOP는 이러한 관심사를 별도의 모듈(Aspect)로 분리하여, 핵심 로직 코드에 영향을 주지 않으면서도 필요한 곳에 적용할 수 있도록 합니다.
프록시 (Proxy)
프록시는 특정 객체에 대한 접근을 제어하거나 기능을 추가하기 위해 사용되는 디자인 패턴입니다. 클라이언트가 객체에 직접 접근하는 대신, 프록시 객체를 통해 간접적으로 접근하도록 하여 추가적인 처리를 할 수 있습니다. 이러한 처리에는 접근 제어, 로깅, 지연 로딩, 원격 객체 접근 등이 포함될 수 있습니다.

 

예를 들어, Spring Framework에서 AOP는 대부분 프록시 기반으로 구현됨

Spring AOP는 런타임에 프록시를 생성하여 특정 메소드 호출 전후에 추가적인 작업(예: 로깅, 트랜잭션 관리)을 수행할 수 있도록 함

요약하자면, AOP와 프록시는 소프트웨어 개발에서 중요한 역할을 하며, AOP 구현에 프록시 패턴이 광범위하게 사용됨

프록시는 AOP가 애플리케이션의 공통 관심사를 효과적으로 분리하고 적용할 수 있도록 하는 메커니즘을 제공함

 

2-2. 왜 프록시 패턴을 쓰는 걸까?

주된 이유는 프록시를 통해 메소드 호출과 같은 다양한 조인 포인트(Join Points)에 투명하게 관심사(Aspects)를 삽입할 수 있기 때문

 

1) 코드 침투성 감소
     - 프록시 패턴을 사용하면, 비즈니스 로직 코드에 아무런 변경 없이도 공통 기능을 적용할 수 있음

     - 예를 들어, 로깅, 보안 검사, 트랜잭션 관리 등의 공통 관심사를 비즈니스 로직 코드와 분리하여 관리할 수 있으므로,

        코드의 침투성(coupling)이 줄어들고, 모듈성이 향상됨

2) 재사용성 및 유지보수성 향상
     - 공통 관심사를 별도의 Aspect로 모듈화함으로써, 해당 기능을 애플리케이션의 다른 부분에서 재사용할 수 있음

     - 이는 코드의 중복을 줄이고, 유지보수성을 향상시킴

     - 프록시를 사용하면, 이러한 Aspect를 필요한 곳에 쉽게 적용할 수 있으며,

        변경 사항이 발생할 경우 Aspect 코드만 수정하면 되므로 유지보수가 용이해짐

3) 동적인 관점의 적용
     - 프록시를 통해 런타임에 동적으로 Aspect를 적용할 수 있음

     - 즉, 애플리케이션 실행 중에도 특정 조건에 따라 Aspect를 적용하거나 해제할 수 있음

     - 이는 프로그램의 유연성을 높이며, 다양한 실행 환경과 요구 사항에 적응할 수 있도록 함

 

4) 투명성과 추상화
     - 프록시를 사용하는 AOP 구현은 개발자로 하여금 구현 세부 사항에 대해 걱정할 필요 없이,

        비즈니스 로직에 집중할 수 있게 해줌

     - 프록시는 메소드 호출과 같은 조인 포인트에서 자동으로 Aspect의 코드를 실행하므로,

        개발자는 Aspect의 존재를 명시적으로 알 필요가 없음

     - 이는 코드의 추상화 수준을 높이고, 개발 과정을 단순화함

5) 기술적 호환성
     - 많은 프로그래밍 언어와 프레임워크, 특히 Java 및 Spring Framework에서 프록시 패턴은 이미 잘 지원되고 있음

     - 이러한 환경에서 AOP를 구현하기 위해 프록시 패턴을 사용하면,

        기존의 기술적 인프라와의 호환성을 유지하면서 공통 관심사를 효과적으로 관리할 수 있음

 


프록시 패턴을 사용한 AOP 구현은 이러한 이유로 인해 널리 사용되며,

소프트웨어 설계에서 모듈성, 재사용성, 유지보수성을 향상시키는 중요한 방법으로 인식되고 있음

 

프록시에 대해 더 자세히 알고 싶다면 링크 참조

 

[Spring] 프록시와 디자인패턴

1. 프록시(Proxy)란? 용어 프록시(Proxy) 정의 대리자 라는 뜻으로, 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 역할 특징 - 실제 대상인 것처럼 위장

study0304.tistory.com

 

2-3. 프록시 패턴의 단점과 해결방안

프록시를 사용하여 기존코드를 수정하지 않고, 타깃의 기능을 추가하거나 접근을 제한할 수 있었음

그러나 이런 프록시에도 아직 여러 단점이 존재함

 

바로 프록시를 만드는 것이 번거롭다는 것

 

프록시 패턴의 문제를 해결하는 방법이 바로 다이내믹(동적)프록시

이름 그대로 프록시를 개발자가 직접 생성하는 것이 아닌 런타임에 동적으로 생성해줌

 

1) 인터페이스 구현과 위임 코드 작성의 번거로움

     - 프록시를 만들기 위해서는 타깃의 인터페이스를 구현해야 하며,

        각 메소드에 대해 부가기능이 필요 없는 경우에도 타깃 객체로의 위임 코드를 일일이 작성해야 함

     - 이 과정은 매우 번거롭고 시간이 많이 소모됨

 

해결 방법

- 다이내믹 프록시는 런타임에 프록시 객체를 생성하므로,

   개발자가 수동으로 인터페이스를 구현하거나 위임 코드를 작성할 필요가 없음

- InvocationHandler 인터페이스를 사용하여 모든 메소드 호출을 중앙에서 처리할 수 있으며,

   이를 통해 자동으로 타깃 객체로의 위임이 처리됨

- 이 과정은 자동화되어 있어 번거로움을 크게 줄여줌

 

2) 부가기능 코드의 중복

     - 부가기능을 적용해야 하는 모든 메소드에 동일한 코드를 추가해야 하므로, 코드 중복이 발생함

     - 예를 들어, 모든 메소드에서 실행 시간을 로깅해야 하는 경우,

        이러한 실행 시간 계산을 위한 코드가 모든 관련 메소드에 중복되어 나타남

 

해결 방법

- InvocationHandler의 구현을 통해 모든 메소드 호출을 가로채고, 이를 중앙에서 관리할 수 있음

- 이로 인해 부가기능(예: 실행 시간 로깅)을 한 곳에 작성하고

   모든 메소드에 일관되게 적용할 수 있어, 코드 중복을 방지함

- 메소드 호출마다 개별적으로 부가기능을 코딩할 필요가 없어짐

 

3) 타깃 오브젝트에 대한 종속성

     - 프록시는 멤버 변수로 타깃 오브젝트를 가지고 있기 때문에, 타깃 오브젝트에 종속적

     - 동일한 기능을 수행하는 프록시라 하더라도, 다양한 타깃에 적용하려면 타깃의 개수만큼 프록시를 별도로 생성해야 함

     - 이는 타깃의 변경이 있을 때마다 프록시도 함께 수정해야 하는 유지보수의 어려움을 초래

 

해결 방법

- 다이내믹 프록시는 생성 시점에 타깃 객체를 지정하기만 하면 되므로,

   하나의 프록시 구현으로 다양한 타깃 객체에 대응할 수 있음

- 이는 타깃 객체의 변경이나 다양한 타깃에 대한 적용이 필요할 때 유연성을 제공함

- 타깃의 종속성 문제를 완화하여, 유지보수성을 향상시킴

 

4) 타깃 인터페이스 변경에 대한 영향

     - 타깃 인터페이스의 메서드가 변경되면, 프록시를 사용하는 메서드 뿐만 아니라,

        타깃으로 위임하는 모든 메서드를 수정해야 함

     - 이는 프록시 클래스의 유지보수를 더욱 어렵게 함

 

해결 방법

- 다이내믹 프록시는 타깃 인터페이스의 구체적인 메소드 구현 대신,

   메소드 호출을 InvocationHandler로 전달함

- 따라서, 타깃 인터페이스에 변경이 있어도,

   대부분의 경우 InvocationHandler 구현을 수정하지 않고도 대응할 수 있음

- 이는 타깃 인터페이스 변경 시 필요한 수정 범위를 최소화하여, 프록시 클래스의 유지보수를 용이하게 함

 

 

3. 프록시 패턴의 단점 해결: 다이내믹 프록시

다이내믹 프록시는 개발자가 직접 프록시 클래스를 작성하는 대신, 런타임에 자동으로 프록시 객체를 생성해줌


인터페이스 구현과 위임 코드의 번거로움을 줄이고, 코드 중복을 방지하며,

타깃 오브젝트에 대한 종속성 문제와 인터페이스 변경에 따른 유지보수 문제를 해결할 수 있음

 

3-1. 다이내믹 프록시란

다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트

다이내믹 프록시는 자바의 리플랙션 기능을 이용하여 프록시를 만들어 줌

리플렉션(reflection)
구체적인 클래스 타입을 알지 못해도 그 클래스의 변수, 메서드, 타입에 접근할 수 있도록 해주는 자바 API

 

3-2. 다이내믹 프록시 분류 - 인터페이스 유무에 따라

- 인터페이스가 존재하는 경우 -> JDK 동적 프록시

- 인터페이스가 없는 경우 -> CGLIB

1) JDK 동적 프록시 

JDK 동적 프록시는 구현클래스가 아닌 인터페이스를 기반으로 프록시를 생성하기 때문에 인터페이스가 반드시 필요

JDK 동적 프록시를 만드는 방법

java.lang.reflect의 InvocationHandler를 구현하면 됨
InvocationHandler는 invoke()메서드 한개만 가진 인터페이스

public Object invoke(Object proxy, Method method, Object[] args)

 

리플렉트의 invoke()와 비슷한 구조

- proxy : 자기 자신
- method : 호출된 메서드
- args : 호출된 메서드의 매개변수

 

 

예시

public class TestInvocationHandler implements InvocationHandler {

    private TargetInterface target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.

    public TestInvocationHandler(TargetInterface target) {
        this.target = target;
    }

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

        log.info("프록시 부가기능 시작");  //여기에 부가기능 로직을 추가
        Object result = method.invoke(target, args);  //실제 타깃 오브젝트로 요청을 위임합니다.
        log.info("프록시 부가기능 종료");  //여기에 부가기능 로직을 추가

        return result;
    }
}

- class TargetInterface

   run()단일 메서드를 가지고 있는 인터페이스

- private TargetInterface target

   다이내믹 프록시로부터 전달받은 요청을 실제 타깃에게 위임해야 하기 때문에 멤버변수로 주입을 받음
- public Object invoke()

   다이내믹 프록시로부터 요청을 받으면 자동으로 실행되는 메서드

   매게변수에 method 나 args 등이 자동으로 주입이 되기 때문에, 부가 기능과 실제타깃으로 요청을 위임하는 코드만 작성할 수 있음

 

테스트 코드

@Test
void testInvocationHandler(){
    TargetInterface target = new TargetImpl();
    TestInvocationHandler invocationHandler = new TestInvocationHandler(target);

    //프록시 생성
    TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(TargetInterface.class.getClassLoader(), new Class[]{TargetInterface.class}, invocationHandler);

    //프록시 실행
    proxy.run();
    log.info("proxy = {}", proxy.getClass());

}

 

테스트 코드 결과

 

결과 분석

proxy.run() 메서드가 실행되면 그림과 같은 순서로 호출됨

 

다른 타깃에 같은 부가기능을 수행하는 프록시를 적용하려면

기존에는 적용하려는 타깃의 인터페이스와 일치하는 프록시 인터페이스를 만들어야 했음

 

그러나 JDK 동적 프록시, 즉 InvocationHandler를 사용하여 타깃에 종속적인 프록시 인터페이스 생성없이 Proxy.newProxyInstance(...)를 사용하여 동적으로 생성할수 있게 됨

 

 

JDK 동적 프록시 제약

JDK 동적 프록시는 인터페이스 기반으로 동작하기 때문에 몇가지 제약이 있음

- 인터페이스를 구현하지 않은 클래스에는 적용할 수 없음
   == 인터페이스가 없는 클래스를 프록시해야 하는 경우에는 사용할 수 없음

 

 

2) CGLIB(Code Generator Library) 

평소에 스프링을 사용하던 중 빈을 출력했을 때 다음과 같이 EnhancerByCGLIB 를 본 경험이 있을 것!


이는 바로 CGLIB를 사용하여 프록시를 생성했기 때문에 생기는 마크

CGLIB는 JDK동적 프록시와 달리 바이트 코드를 조작해서 동적으로 클래스를 생성해주는 라이브러리

인터페이스 없이 구체 클래스만 존재하더라도 프록시를 생성할 수 있음

 

CGLIB를 만드는 방법

CGLIB는 MethodInterceptor를 통해 구현

public interface MethodInterceptor extends Callback{
    Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

 

MethodInterceptor는 단일 메서드 인터페이스

JDK 동적 프록시의 InvocationHandler와 굉장히 유사한 구조와 동작 방식을 가지고 있음

- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 호출된 메서드의 매개변수
- proxy : 실제 타깃의 메서드를 invoke 할 때 사용. method를 이용하여 실행이 가능하지만 성능상 proxy.invoke()를 권장.

 

예시

public class TestMethodInterceptor implements MethodInterceptor {

    private TargetImpl target; //타깃 오브젝트를 멤버 변수로 유지해야 합니다.

    public TestMethodInterceptor(TargetImpl target) {
        this.target = target;
    }

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

        log.info("프록시 부가기능 시작");  //여기에 부가기능 로직을 추가
        Object result = proxy.invoke(target, args);  //실제 타깃 오브젝트로 요청을 위임합니다.
        log.info("프록시 부가기능 종료");  //여기에 부가기능 로직을 추가

        return result;

    }
}

 

테스트 코드

@Test
public void testMethodInterceptor(){

    TargetImpl target = new TargetImpl();

    //프록시 생성
    Enhancer enhancer = new Enhancer();  //Enhancer 오브젝트를 생성
    enhancer.setSuperclass(TargetImpl.class);  //생성할 프록시의 타입을 지정합니다(TargetImpl을 상속합니다.)
    enhancer.setCallback(new TestMethodInterceptor(target));  //MethodInterceptor를 설정합니다.
    TargetImpl proxy = (TargetImpl) enhancer.create();  //프록시를 생성한 후 타입 캐스팅

    proxy.run();
    log.info("proxy = {}", proxy.getClass());
}

 

테스트 코드 결과화면

 

 

CGLIB 제약

클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있음

- 부모 클래스의 생성자를 체크해야 함
   CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요
- 클래스에 final 키워드가 붙으면 상속이 불가능
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음

(기본 생성자 및 생성자 2번 호출 문제는 'objenesis' 라는 특별한 라이브러리를 사용함으로써 해결되었다고 함)

 

3-3. 다이내믹 프록시 정리

다이내믹 프록시

- 런타임시 동적으로 프록시를 생성해주는 기술
- 다이내믹 프록시는 리플렉션이라는 기술로 구현

JDK 동적 프록시

- 인터페이스 기반의 프록시 생성이 필요할 때 적합하며, 자바의 표준 메커니즘을 사용하여 상대적으로 구현이 간단

- InvocationHandler 사용
CGLIB

- 인터페이스가 없는 클래스를 프록시해야 하거나, 상속을 통한 더 복잡한 프록시 로직을 구현하고자 할 때 유용

- 추가 라이브러리가 필요하지만, 그만큼 더 많은 유연성을 제공

- MethodInterceptor 사용

기준 JDK 동적 프록시 CGLIB(Code Generation Library)
구현 방식 인터페이스 기반 서브클래싱(Subclassing) 방식
사용 조건 대상 객체가 하나 이상의 인터페이스를 구현해야 함. 인터페이스 또는 클래스 모두 가능. 상속을 통해 프록시 생성.
성능 일반적으로 빠름
(인터페이스를 통한 동적 프록시는 비교적 오버헤드가 적음)
JDK 동적 프록시보다 약간 느릴 수 있음
(하지만 최적화로 성능 차이 최소화)
사용 용이성 사용하기 쉬움 (자바 표준 API 사용). 별도의 라이브러리 (CGLIB)가 필요.
(스프링에서는 스프링 프레임워크가
스프링 내부 소스 코드에 포함 -> 별도 라이브러리 필요X)
활용성 인터페이스가 있는 경우에만 사용 가능. 인터페이스가 없는 클래스에도 사용할 수 있어 더 유연함.
확장성과 유연성 인터페이스를 통해 메소드 호출을 관리하기 때문에,
인터페이스 설계에 따라 유연성이 결정됨.
클래스의 구현을 직접 상속받기 때문에
더 많은 제어와 유연성을 제공할 수 있음.
주요 사용 사례 Spring AOP 등에서 널리 사용. Spring AOP의 백업 옵션으로 사용되며,
인터페이스가 없는 클래스를 프록시해야 할 때 사용.

 

 

3-4. 다이내믹 프록시 단점과 해결방안

다이내믹 프록시를 적용함으로써 많은 코드를 개선하며 클래스 수와 중복을 줄일 수 있었지만,

아직 불편함이 존재


1) 인터페이스와 구현 클래스에 따른 프록시 생성 방식의 복잡성

     - 다이내믹 프록시를 직접 사용할 때,

        대상 객체가 인터페이스를 구현했는지 여부에 따라

        JDK 동적 프록시와 CGLIB 중 적절한 방식을 선택해야 하는 복잡성이 있음

     - 이는 개발자가 프록시 생성 로직을 각 경우에 맞춰 구현해야 하므로,

        코드의 복잡도가 증가하고 유지보수가 어려워질 수 있음


해결 방안

- 프록시 팩토리를 사용하면, 이러한 복잡성을 추상화 계층 뒤로 숨길 수 있음

- 프록시 팩토리는 대상 객체의 타입을 자동으로 분석하여

   인터페이스가 있으면 JDK 동적 프록시를, 없으면 CGLIB를 사용해 프록시 객체를 생성

- 따라서 개발자는 프록시 생성 방식의 복잡성을 신경 쓰지 않고 일관된 방식으로 프록시 객체를 생성할 수 있음

 

2) 프록시 생성 방식의 선택에 따른 유연성 부족

     - 특정 프록시 생성 방식(JDK 동적 프록시 또는 CGLIB)에 의존하게 되면,

        해당 방식만을 사용해야 하는 제한이 있음

     - JDK 동적 프록시는 인터페이스가 필수적이기 때문에, 인터페이스 없이는 사용할 수 없음

        반면, CGLIB는 구체 클래스를 상속받아 프록시를 생성하기 때문에, 최종 클래스나 생성자가 private인 클래스에는 사용할 수 없음


해결 방안

- 프록시 팩토리는 이러한 제한을 극복하고 유연성을 제공

- 사용자가 프록시 팩토리에 대상 객체만 제공하면, 내부적으로 가장 적합한 프록시 생성 방식을 선택하여 사용

- 이로 인해 어떤 타입의 객체에도 유연하게 프록시를 적용할 수 있으며, 프록시 생성 방식의 제한에서 벗어날 수 있음

 

 

4. 다이내믹 프록시 단점 해결: 프록시 펙토리(Proxy Factory)

4-1. 프록시 팩토리의 역할

프록시 팩토리는 JDK 동적 프록시와 CGLIB 사이의 선택을 추상화하여,

개발자가 인터페이스의 유무에 관계없이 투명하게 프록시 객체를 생성할 수 있도록 도와주는 역할을 함

 

 

즉, 프록시 팩토리는 대상 객체의 특성을 분석하고,

인터페이스가 있다면 JDK 동적 프록시를, 없다면 CGLIB를 사용하여

프록시 객체를 자동으로 생성해줌

 

이 과정은 내부적으로 처리되기 때문에,

개발자는 프록시 생성 방식의 복잡성을 신경 쓰지 않고 프록시를 사용할 수 있음

스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용한다.
따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.

 

4-2. 프록시 팩토리의 장점

- 유연성

   인터페이스의 유무와 상관없이 동일한 방식으로 프록시 객체를 생성할 수 있어, 코드의 유연성이 향상됨
- 간결성

   복잡한 프록시 생성 로직을 프록시 팩토리 내부에 숨길 수 있어, 코드가 더 간결하고 이해하기 쉬워짐
- 재사용성

   프록시 팩토리를 통해 생성된 프록시 객체는 재사용이 가능하여, 코드 중복을 줄일 수 있음


프록시 팩토리를 사용함으로써, 다이내믹 프록시의 생성과 관리가 더욱 편리해지고,

애플리케이션의 구조가 단순화되며, 유지보수성이 향상됨

 

 

5. 아직도 귀찮다. 더 자동화해보자: 자동 프록시 생성기(Automatic Proxy Creator)

자동 프록시 생성기(Automatic Proxy Creator)는

애플리케이션 내에서 프록시 생성 과정을 더욱 자동화하고, 편리하게 만들기 위해 두둥등장

 

프록시 팩토리는 이미 프록시 생성 과정을 추상화하고,

인터페이스 유무에 따라 적절한 프록시 생성 방식(JDK 동적 프록시 또는 CGLIB)을 선택하는 유연성을 제공하고 있음

 

그러나, 프록시 팩토리를 사용하더라도

개발자는 여전히 프록시를 필요로 하는 각각의 빈(Bean)에 대해 명시적으로 프록시 팩토리 빈을 구성해야 하는 수고로움이 있음

 

5-1. 자동 프록시 생성기의 역할

자동 프록시 생성기는 이러한 수동 작업을 최소화하며, 

특정 조건을 만족하는 빈에 대해 자동으로 프록시를 적용할 수 있도록 해줌

 

예를 들어, Spring Framework에서는 

@Transactional 어노테이션이 붙은 클래스나 메소드에 대해 

트랜잭션 관리를 자동으로 적용하기 위해 내부적으로 자동 프록시 생성기를 사용함

이렇게 함으로써, 개발자는 비즈니스 로직에 더 집중할 수 있고, 

프록시 관련 설정이나 코드를 직접 작성하는 번거로움 없이 공통 기능을 적용할 수 있게 됨

 

5-2. 자동 프록시 생성기의 장점

- 설정의 단순화

   애플리케이션 전반에 걸쳐 일관된 방식으로 공통 기능을 적용할 수 있도록 해주며, 복잡한 프록시 설정을 줄여줌
- 개발 생산성 향상

   공통 기능의 적용을 위한 부가적인 코드 작성 없이, 어노테이션 등의 간단한 설정으로 자동 프록시 생성이 가능함
- 유지보수성 개선

   프록시 관련 코드가 분산되지 않고, 중앙에서 관리되므로 애플리케이션의 유지보수성이 향상됨

 

5-3. 자동 프록시 생성기의 작동 과정

 

1) 생성

스프링은 애플리케이션 실행 중에 

@Bean 어노테이션이 붙은 메소드 또는 컴포넌트 스캔을 통해 발견된 클래스로부터 빈 객체를 생성


2) 전달

생성된 객체는 빈 저장소에 등록되기 직전,

빈 후처리기(BeanPostProcessor)에 전달됨

 

이 과정에서 객체가 프록시 적용 대상인지 여부를 판단할 수 있음

 

3) Advisor 빈 조회

스프링 컨테이너는 등록된 모든 Advisor 빈을 조회함

Advisor는 포인트컷과 어드바이스(부가 기능)를 가지고 있어,

어떤 빈에 어떤 부가 기능을 적용할지 결정하는 역할을 함

 

4) 프록시 적용 대상 체크

조회된 Advisor에 포함된 포인트컷을 사용하여,

생성된 객체가 프록시를 적용할 대상인지를 판단함

 

객체의 모든 메서드가 포인트컷 조건과 매칭되어 검사되며,

하나라도 조건에 만족한다면 해당 객체는 프록시 적용 대상이 됨

 

5) 프록시 생성

객체가 프록시 적용 대상이면, 

해당 객체를 대신하여 실행될 프록시 객체가 생성됨

 

이 프록시 객체는 원본 객체의 메서드 호출을 가로채어, 

필요한 부가 기능을 수행한 뒤 원본 메서드를 호출함

 

프록시 객체는 이후 스프링 빈으로 등록되어 애플리케이션에서 사용됨

만약 객체가 프록시 적용 대상이 아니라면, 원본 객체가 직접 스프링 빈으로 등록됨

 

6) 빈 등록

프록시 객체 또는 원본 객체가 스프링 빈으로 등록됨

 

이 과정을 통해, 애플리케이션 내의 빈은 필요에 따라 자동으로 프록시로 감싸져 부가 기능이 적용되거나, 그대로 사용됨

 

 

 

 


참조

https://velog.io/@smc2315/spring-aop

 

Spring AOP 동작 원리 및 사용법

AOP의 등장 배경과 Spring에서의 AOP 사용

velog.io

https://hudi.blog/java-reflection/

 

자바 리플렉션 (Reflection) 기초

리플렉션 (Reflection) JVM은 클래스 정보를 클래스 로더를 통해 읽어와서 해당 정보를 JVM 메모리에 저장한다. 그렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어, 리플렉션

hudi.blog

https://www.podo-dev.com/blogs/93

 

podo-dev : Spring AOP

AOP(Aspect Oriented Programming), 관점지향 프로그래밍 기능을 핵심기능과 공통기능을 분리하고, 공통기능을 필요로하는 핵심기능들에서 사용할 수 있도록 하는 프로그래밍. AOP 관련 용어 Joinpoint [조인

www.podo-dev.com

https://rlawls1991.tistory.com/entry/AOP-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%98-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%B9%88

 

스프링의 프록시 팩토리 빈

ProxyFactoryBean 자바에는 JDK에서 제공하는 다이내믹 프록시 외에도 편리하게 프록시를 만들 수 있도록 지원해주는 다양한 기술이 존재한다. 따라서 스프링은 일관된 방법으로 프록시를 만들 수 있

rlawls1991.tistory.com

https://yejun-the-developer.tistory.com/6

 

[Spring] 다이내믹 프록시(DynamicProxy)

(프록시에 대한 이해가 부족하신 분들은 이전 포스팅을 참고하세요!!) [Spring] 프록시와 디자인패턴 프록시와 디자인 패턴 스프링의 3대 기반기술 중 AOP를 공부하던 중 관심사 분리를 위한 다이내

yejun-the-developer.tistory.com