ProxyFactoryBean
ProxyFactoryBean은 스프링 프레임워크에서 제공하는 빈(Bean)으로, 다양한 유형의 프록시(Proxy) 객체를 생성하고 관리하는데 사용되는 중요한 구성 요소 중 하나입니다. ProxyFactoryBean을 사용하면 다음과 같은 기능을 수행할 수 있습니다.
1. 프록시 객체 생성:
ProxyFactoryBean은 주어진 대상 객체(타겟 객체)를 감싸고, 필요한 경우 프록시를 생성합니다. 이 프록시는 대상 객체와 동일한 인터페이스를 구현하거나 상속받은 클래스를 생성할 수 있습니다. 이렇게 생성된 프록시 객체는 대상 객체의 메서드 호출을 중간에서 가로채서 추가 동작(로깅, 트랜잭션 관리 등)을 수행하거나, 대상 객체의 메서드를 호출하기 전/후에 사전/사후 처리를 할 수 있습니다.
2. 다양한 프록시 유형 지원:
ProxyFactoryBean은 JDK dynamic proxy와 CGLIB 같은 다양한 프록시 유형을 생성할 수 있는 능력을 제공합니다. 이는 프록시 대상 객체가 인터페이스를 구현한 경우와 그렇지 않은 경우에 모두 유용합니다. JDK dynamic proxy는 인터페이스를 기반으로 프록시를 생성하고, CGLIB은 클래스를 상속받아 프록시를 생성합니다.
3. AOP(Aspect-Oriented Programming) 지원:
ProxyFactoryBean은 스프링의 AOP를 활용하여 관심사(Aspect)를 적용할 때 유용합니다. AOP는 애플리케이션의 핵심 로직과 분리된 횡단 관심사를 적용하는 기술로, 예를 들어 로깅, 보안, 트랜잭션 관리 등과 같은 공통 기능을 쉽게 적용할 수 있게 해줍니다. ProxyFactoryBean을 사용하면 AOP를 적용하고 구성하기가 훨씬 쉽습니다.
4. 설정과 관리:
ProxyFactoryBean은 스프링 IoC 컨테이너에 빈으로 등록하여 관리할 수 있습니다. 이렇게 하면 ProxyFactoryBean을 빈으로 등록하고 다른 빈에서 참조하여 프록시를 생성하고 사용할 수 있습니다. 또한 ProxyFactoryBean을 통해 프록시 생성과 관리에 대한 설정을 XML 또는 Java 구성 파일에서 선언적으로 처리할 수 있으며, 이로써 런타임에서의 동작을 변경하지 않고 프록시를 구성 및 변경할 수 있습니다.
5. 라이프사이클 관리:
ProxyFactoryBean은 스프링의 라이프사이클 관리를 자동으로 처리합니다. 이는 대상 객체의 생성, 초기화, 종료 등의 라이프사이클 이벤트를 스프링이 관리하도록 할 수 있습니다.
ProxyFactoryBean은 기본적으로 JDK가 제공하는 다이내믹 프록시를 생성합니다. 경우에 따라서는 CGLib이라고 하는 오픈소스 바이트코드 생성 프레임워크를 이용해 프록시를 생성하기도 합니다.
ProxyFactoryBean은 일반적으로 스프링 기반의 애플리케이션에서 AOP와 같은 측면에서 주로 사용되며, 코드의 재사용성과 유지보수성을 개선하는데 도움이 됩니다. 대상 객체를 감싸는 프록시를 생성하고 관리하려면 빈 구성 파일에서 ProxyFactoryBean을 정의하고 설정해야 합니다.
Spring의 ProxyFactoryBean vs Java dynamic proxy
Spring의 ProxyFactoryBean과 직접 Java dynamic proxy를 생성하는 것 간의 주요 장점은 다음과 같습니다:
1. 설정 및 관리의 간편성:
- ProxyFactoryBean을 사용하면 스프링의 IoC 컨테이너에 빈으로 등록하여 관리할 수 있습니다. 이렇게 하면 객체의 생성과 초기화, 종료 등의 라이프사이클 관리가 스프링에 의해 자동으로 처리됩니다. 반면에 Java dynamic proxy를 직접 생성하면 이러한 라이프사이클 관리를 직접 구현해야 합니다.
2. 의존성 주입(Dependency Injection):
- ProxyFactoryBean을 사용하면 프록시 대상 객체에 의존성 주입(Dependency Injection)을 쉽게 적용할 수 있습니다. 스프링은 필요한 의존성을 자동으로 주입해줍니다. Java dynamic proxy를 사용할 경우 의존성 주입을 수동으로 처리해야 하며, 이는 복잡성을 증가시킬 수 있습니다.
3. AOP 지원:
- ProxyFactoryBean은 스프링 AOP를 쉽게 활용할 수 있도록 지원합니다. AOP는 관심사(Aspect)와 핵심 로직 사이에서 횡단 관심사를 적용하는 기술로, 로깅, 트랜잭션 관리, 보안 등과 같은 공통 로직을 쉽게 적용할 수 있습니다. ProxyFactoryBean을 사용하면 AOP 설정을 훨씬 간단하게 구성할 수 있습니다.
4. 다양한 프록시 유형:
- ProxyFactoryBean은 JDK dynamic proxy 외에도 CGLIB과 같은 라이브러리를 활용하여 다양한 프록시 유형을 생성할 수 있습니다. 이는 인터페이스를 가지지 않는 클래스나 final 클래스 등을 프록시화할 때 매우 유용합니다. Java dynamic proxy를 직접 사용하는 경우 이러한 상황을 다루기가 더 어려울 수 있습니다.
5. 스프링 표준 방식 준수:
- ProxyFactoryBean은 스프링 프레임워크의 표준 방식을 따르므로 스프링의 다른 기능과 호환성을 유지하는 데 도움이 됩니다. 이로 인해 애플리케이션의 유지보수가 더 쉬워집니다.
6. 선언적 설정:
- ProxyFactoryBean은 XML 또는 Java 구성 파일에서 선언적으로 설정할 수 있습니다. 이는 프록시를 사용하고자 하는 빈과 연결하기가 간단하며, 설정을 변경하기가 용이합니다.
7. ProxyFactoryBean을 사용한 부가기능의 동적 추가와 효율적인 관리 :
- ProxyFactoryBean을 사용하면 새로운 부가기능을 추가할 때마다 프록시 및 프록시 팩토리 빈을 추가할 필요가 없으며, 기존의 프록시 팩토리 빈에 부가기능을 추가할 수 있습니다. 이를 통해 코드의 중복을 피하고 프록시와 부가기능을 더욱 효율적으로 관리할 수 있습니다.
8. ProxyFactoryBean을 통한 다중 부가기능 프록시 생성:
- ProxyFactoryBean이 여러 개의 부가기능을 제공하는 프록시를 만들 수 있는 부분은 바로 MethodInterceptor를 여러 개 설정할 수 있다는 점입니다. MethodInterceptor는 스프링 AOP에서 부가기능을 정의하는 데 사용되는 인터페이스입니다.
9. ProxyFactoryBean의 인터페이스 자동 감지 및 지정
- 타깃 오브젝트가 구현하는 모든 인터페이스를 탐지합니다.
- 탐지된 인터페이스를 모두 구현하는 프록시 객체를 동적으로 생성합니다.
- 필요한 경우에는 인터페이스 정보를 직접 제공하여 일부 인터페이스만 프록시에 적용할 수도 있습니다
Spring의 ProxyFactoryBean을 사용하면 프록시 생성 및 관리를 단순화하고, AOP와 같은 고급 기능을 쉽게 활용할 수 있으며, 스프링의 표준 방식을 따르므로 유지보수와 호환성을 개선할 수 있습니다. 반면에 Java dynamic proxy를 직접 사용하려면 모든 것을 수동으로 처리해야 하고, 더 많은 수작업과 노력이 필요할 수 있습니다.
MethodInterceptor
MethodInterceptor는 스프링과 같은 AOP(Aspect-Oriented Programming) 프레임워크에서 사용되는 중요한 인터페이스 중 하나로, AOP에서 부가기능을 정의하고 구현하는 데 사용됩니다. MethodInterceptor를 구현하는 클래스는 타깃 메서드의 호출을 가로채고 추가 동작을 수행할 수 있습니다.
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
다음은 MethodInterceptor에 대한 상세한 설명입니다:
1. MethodInterceptor 인터페이스:
- org.aopalliance.intercept.MethodInterceptor 인터페이스는 AOP에 사용되는 표준 인터페이스로, 다양한 AOP 프레임워크에서 지원됩니다.
- 스프링 AOP 및 다른 AOP 프레임워크에서 MethodInterceptor를 구현하여 부가기능을 정의하고 적용할 수 있습니다.
2. invoke 메서드:
- MethodInterceptor 인터페이스에는 단일 메서드인 invoke가 정의되어 있습니다.
- invoke 메서드는 타깃 메서드 호출을 가로채고 부가 동작을 수행하기 위해 사용됩니다.
- invoke 메서드는 MethodInvocation 인터페이스의 구현을 파라미터로 받습니다.
3. invoke 메서드의 파라미터 : MethodInvocation 인터페이스 구현 참조 변수:
- MethodInvocation은 MethodInterceptor의 invoke 메서드에 전달되는 인터페이스로, 타깃 메서드의 호출과 관련된 정보를 제공합니다.
- MethodInvocation에는 다음과 같은 중요한 메서드 및 정보가 포함됩니다:
getMethod() | 호출된 메서드에 대한 정보를 반환합니다. |
getArguments() | 메서드 호출에 사용된 아규먼트 배열을 반환합니다. |
proceed() | 타깃 메서드 호출을 진행합니다. 이 메서드를 호출하면 실제 대상 메서드가 실행됩니다. |
기타 메서드 및 정보를 포함합니다. |
4. 부가 동작 추가:
- MethodInterceptor를 구현하는 클래스에서 invoke 메서드를 오버라이드하여 부가 동작을 추가합니다.
- invoke 메서드 내부에서 MethodInvocation을 이용하여 타깃 메서드 호출을 가로채고, 추가적인 동작을 수행한 후 proceed() 메서드를 호출하여 타깃의 메서드를 실행합니다.
- 부가 동작은 타깃 메서드 호출 전후 또는 예외 발생 시에 수행할 수 있습니다.
5. AOP에서 활용:
- MethodInterceptor를 활용하면 로깅, 보안 검사, 트랜잭션 관리, 예외 처리 등과 같은 횡단 관심사를 쉽게 적용할 수 있습니다.
- 여러 개의 MethodInterceptor를 조합하여 하나의 프록시에 여러 부가기능을 적용할 수 있습니다.
MethodInterceptor를 이용하면 AOP의 핵심 개념 중 하나인 어드바이스(Advice)를 정의하고 구현할 수 있으며, 이를 통해 대상 메서드 호출을 가로채고 부가 동작을 추가하는데 활용됩니다. 이를 통해 코드의 재사용성과 유지보수성을 향상시킬 수 있습니다.
MethodInvocation
MethodInvocation은 주로 AOP(Aspect-Oriented Programming)와 관련된 개념으로, 프록시(Proxy) 객체를 사용하여 메서드 호출을 가로채고 추가 동작을 수행하는 데 사용됩니다. MethodInvocation은 스프링 프레임워크와 같은 AOP 라이브러리에서 중요한 역할을 합니다.
MethodInvocation 인터페이스의 정의와 상속 관계는 다음과 같습니다:
public interface MethodInvocation extends Invocation {
/**
* Get the method being called.
* <p>This method is a friendly implementation of the
* {@link Joinpoint#getStaticPart()} method (same result).
* @return the method being called
*/
@Nonnull
Method getMethod();
}
public interface Invocation extends Joinpoint {
/**
* Get the arguments as an array object.
* It is possible to change element values within this
* array to change the arguments.
* @return the argument of the invocation
*/
@Nonnull
Object[] getArguments();
}
public interface Joinpoint {
/**
* Proceed to the next interceptor in the chain.
* <p>The implementation and the semantics of this method depends
* on the actual joinpoint type (see the children interfaces).
* @return see the children interfaces' proceed definition
* @throws Throwable if the joinpoint throws an exception
*/
@Nullable
Object proceed() throws Throwable;
/**
* Return the object that holds the current joinpoint's static part.
* <p>For instance, the target object for an invocation.
* @return the object (can be null if the accessible object is static)
*/
@Nullable
Object getThis();
/**
* Return the static part of this joinpoint.
* <p>The static part is an accessible object on which a chain of
* interceptors are installed.
*/
@Nonnull
AccessibleObject getStaticPart();
}
MethodInvocation, Invocation, 그리고 Joinpoint와 같은 상속 구조를 가지게 된 이유는 다음과 같습니다:
1. 다형성 활용: 상속 구조를 사용함으로써 여러 다른 유형의 AOP 인터셉터를 통합하고 통일된 방식으로 다룰 수 있습니다. 이는 다양한 부가 작업을 수행하는 인터셉터를 효과적으로 구성하고 관리할 수 있도록 도와줍니다.
2. 일관된 인터페이스: MethodInvocation, Invocation, Joinpoint와 같은 공통 인터페이스를 정의함으로써 AOP 관련 클래스 및 라이브러리 간에 일관된 인터페이스를 제공합니다. 이로써 다양한 AOP 관련 기능을 통합하고 확장할 수 있습니다.
3. 다양한 AOP 기능 지원: 이러한 인터페이스를 사용하면 다양한 AOP 기능(예: 어드바이스 종류, 포인트컷 등)을 지원할 수 있습니다. 각 인터페이스는 특정 AOP 관점에 맞게 확장하고 구현할 수 있으므로 유연성을 제공합니다.
4. 프레임워크 호환성: 다양한 AOP 프레임워크 및 라이브러리에서 이러한 공통 인터페이스를 구현하면 서로 다른 AOP 구현체 간에도 호환성을 유지할 수 있습니다. 이는 스프링과 같은 AOP 프레임워크가 여러 다른 AOP 구현을 지원하는 데 도움이 됩니다.
5. 유지보수 및 확장성: 이러한 인터페이스는 새로운 AOP 기능을 추가하거나 기존 기능을 수정하기 쉽도록 설계되었습니다. 새로운 유형의 어드바이스를 구현하거나 특정 요구 사항에 맞게 인터페이스를 확장할 수 있습니다.
요약하면, 이러한 상속 구조는 AOP 관련 기능을 효과적으로 구성하고 관리하기 위해 필요한 다형성, 일관성, 확장성 및 호환성을 제공하기 위해 사용됩니다. AOP 프레임워크와 관련된 다양한 요구 사항을 처리하기 위한 강력한 기반을 제공합니다.
1. MethodInvocation 인터페이스:
- MethodInvocation은 AOP에서 타깃 메서드 호출 정보를 캡슐화하는 인터페이스입니다.
- getMethod(): 이 메서드는 호출된 타깃 메서드에 대한 정보를 반환합니다. 메서드 이름, 매개변수 유형, 반환 타입 등을 얻을 수 있습니다.
2. Invocation 인터페이스:
- Invocation 인터페이스는 MethodInvocation의 상위 인터페이스이며, 메서드 호출과 관련된 정보를 캡슐화합니다.
- getArguments(): 메서드 호출에 사용된 인자(매개변수) 배열을 반환합니다.
3. Joinpoint 인터페이스:
- Joinpoint는 메서드 호출 또는 다른 AOP 이벤트에 대한 공통 인터페이스를 정의합니다.
- proceed(): 현재 Joinpoint의 다음 단계로 진행하도록 하는 메서드입니다. 이 메서드를 호출하면 다음 인터셉터 또는 타깃 메서드를 실행하게 됩니다.
- getThis(): 현재 Joinpoint에 대한 객체를 반환합니다. 이 객체는 주로 타깃 객체를 나타냅니다.
- getStaticPart(): 이 Joinpoint의 정적 부분(Static Part)을 나타내는 AccessibleObject를 반환합니다. 정적 부분은 인터셉터 체인이 적용된 대상 메서드나 필드를 가리킵니다.
이 인터페이스들은 AOP 프레임워크에서 사용자가 정의한 어드바이스(Advice)와 함께 동작하여 메서드 호출을 가로채고 부가 작업을 수행할 수 있도록 해줍니다. 각 인터페이스의 구현체는 구체적인 AOP 라이브러리나 프레임워크에서 제공됩니다. 이를 통해 횡단 관심사(Cross-Cutting Concerns)와 같은 부가 작업을 쉽게 적용할 수 있으며, 예를 들어 로깅, 트랜잭션 관리, 보안 검사 등을 구현할 수 있습니다.
MethodInvocation을 사용하는 AOP 프록시는 보통 다음과 같은 과정을 따릅니다:
1. 클라이언트 코드에서 타깃 객체의 메서드를 호출합니다.
2. AOP 프록시는 MethodInvocation 객체를 생성하고, 이 객체에 타깃 객체, 메서드, 아규먼트 정보를 전달합니다.
3. MethodInvocation 객체는 추가 동작(로깅, 보안 검사, 트랜잭션 처리 등)을 수행합니다.
4. 추가 동작이 완료되면 MethodInvocation 객체는 Proceed 메서드를 호출하여 타깃 객체의 메서드를 실행합니다.
5. 타깃 객체의 메서드 실행 결과가 반환되고, 이 결과를 클라이언트 코드에 반환합니다.
스프링 AOP와 같은 AOP 프레임워크에서는 MethodInvocation을 사용하여 메서드 호출을 가로채고 공통 관심사를 적용합니다. 이를 통해 각각의 비즈니스 로직에 횡단 관심사(로깅, 보안, 트랜잭션 관리 등)를 쉽게 추가할 수 있으며, 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있습니다. MethodInvocation은 AOP의 핵심 개념 중 하나로, 다양한 AOP 라이브러리에서 지원되는 일반적인 구성 요소입니다.
MethodInterceptor vs InvocationHandler
MethodInterceptor와 InvocationHandler의 주요 차이점은 다음과 같습니다:
1. 사용되는 프레임워크:
- MethodInterceptor는 주로 스프링 AOP 및 AOP 관련 라이브러리에서 사용됩니다.
- InvocationHandler는 자바의 기본 다이내믹 프록시(Dynamic Proxy) 및 다이내믹 프록시 관련 API에서 사용됩니다.
2. 목적:
- MethodInterceptor는 AOP를 구현하고 부가 기능을 추가하는 데 주로 사용됩니다.
- InvocationHandler는 다이내믹 프록시를 생성하고 메서드 호출을 가로채기 위해 사용됩니다.
3. 스프링 컨텍스트와 통합:
- MethodInterceptor는 스프링 AOP와 함께 사용되며 스프링 빈에 AOP를 적용하는 데 용이합니다.
- InvocationHandler는 스프링과는 직접적인 관련이 없으며, 순수한 자바 다이내믹 프록시 생성에 사용됩니다.
간단하게 말해서,
MethodInterceptor는 AOP와 관련된 프레임워크에서 사용되고 AOP 부가 기능을 추가하는 데 중점을 두며, InvocationHandler는 자바의 다이내믹 프록시를 생성하고 메서드 호출을 가로채는 데 중점을 둡니다.
// InvocationHandler를 사용한 동적 프록시 생성
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 추가 동작 수행
// 예: 메서드 호출 전에 로깅
System.out.println("Before invoking method: " + method.getName());
// 원래 메서드 호출
Object result = method.invoke(target, args);
// 추가 동작 수행
// 예: 메서드 호출 후에 로깅
System.out.println("After invoking method: " + method.getName());
return result;
}
}
// MyMethodInterceptor을 사용한 AOP 프록시 생성 (스프링 AOP 예제)
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 추가 동작 수행
// 예: 메서드 호출 전에 로깅
System.out.println("Before invoking method: " + invocation.getMethod().getName());
// 원래 메서드 호출
Object result = invocation.proceed();
// 추가 동작 수행
// 예: 메서드 호출 후에 로깅
System.out.println("After invoking method: " + invocation.getMethod().getName());
return result;
}
}
간단한 예에서는 InvocationHandler를 사용한 동적 프록시와 MyMethodInterceptor을 사용한 AOP 프록시 모두 메서드 호출을 가로채고 추가 동작을 수행하고 있음을 볼 수 있습니다. 그러나 InvocationHandler는 주로 자바의 동적 프록시 생성에 사용되고, MethodInvocation은 스프링 AOP와 같은 AOP 프레임워크에서 사용됩니다.
스프링 ProxyFactoryBean을 이용한 다이내믹 프록시 테스트
package com.coga.learningtest.jdk.proxy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
public class DynamicProxyTest {
@Test
public void simpleProxy() {
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Hello.class},
new UppercaseHandler(new HelloTarget()));
assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY");
assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY");
assertEquals(proxiedHello.sayThankYou("Toby"), "THANK YOU TOBY");
}
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
pfBean.addAdvice(new UppercaseAdvice());
Hello proxiedHello = (Hello) pfBean.getObject();
assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY");
assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY");
assertEquals(proxiedHello.sayThankYou("Toby"), "THANK YOU TOBY");
}
static class UppercaseAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
String ret = (String)invocation.proceed();
return ret.toUpperCase();
}
}
static interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
static class HelloTarget implements Hello {
public String sayHello(String name) {
return "Hello " + name;
}
public String sayHi(String name) {
return "Hi " + name;
}
public String sayThankYou(String name) {
return "Thank You " + name;
}
}
}
Advice
어드바이스(Advice)는 AOP(Aspect-Oriented Programming)에서 사용되는 개념으로, 프로그램의 핵심 로직과는 독립적으로 공통 관심사(Cross-Cutting Concerns)를 정의하고 캡슐화하는 역할을 합니다. 어드바이스는 메서드 호출 또는 객체의 생명주기와 관련하여 추가적인 행동을 삽입하거나 실행할 수 있게 해줍니다. 어드바이스는 AOP의 핵심 구성 요소 중 하나로, 다양한 상황에서 사용됩니다.
어드바이스에는 다음과 같은 종류가 있습니다:
1. Before Advice (이전 어드바이스):
- 타깃 메서드가 호출되기 전에 실행되는 어드바이스입니다.
- 주로 로깅, 인증, 인가 등의 사전 처리를 수행하는 데 사용됩니다.
- 대상 메서드 실행 전에 어떤 작업을 수행하려는 경우에 유용합니다.
2. After Returning Advice (리턴 후 어드바이스):
- 타깃 메서드가 성공적으로 실행된 후에 실행되는 어드바이스입니다.
- 타깃 메서드가 값을 반환한 경우에만 동작합니다.
- 타깃 메서드 실행 후에 결과를 처리하거나 로깅하는 데 사용됩니다.
3. After Throwing Advice (예외 발생 후 어드바이스):
- 타깃 메서드에서 예외가 발생한 후에 실행되는 어드바이스입니다.
- 예외 처리, 예외 로깅 또는 복구 작업을 수행하는 데 사용됩니다.
4. After (Finally) Advice (종료 어드바이스):
- 대상 메서드 실행 후 항상 실행되는 어드바이스입니다.
- 예외가 발생하더라도 실행됩니다.
- 자원 해제 또는 정리 작업을 수행하는 데 사용됩니다.
5. Around Advice (어라운드 어드바이스):
- 대상 메서드를 실행하기 전과 후에 커스텀한 동작을 수행할 수 있는 가장 강력한 어드바이스입니다.
- 대상 메서드 호출을 가로채고 직접 메서드 실행 여부를 제어할 수 있습니다.
- 메서드 호출 전후에 추가 작업을 수행할 때 사용됩니다.
※ 특징
어드바이스(Advice)는 스프링 AOP에서 사용되는 부가 기능[공통 관심사]을 담은 객체로, AOP 관점(Aspect)에서 핵심 비즈니스 로직을 보완하거나 확장하는 역할을 합니다. 아래는 어드바이스의 주요 특징입니다:
1. 비즈니스 로직과 분리: 어드바이스는 핵심 비즈니스 로직과 분리되어 있습니다. 이로 인해 핵심 로직의 수정 없이 부가 기능을 추가하거나 변경할 수 있습니다.
2. 재사용성: 어드바이스는 여러 다른 지점에서 동일한 부가 기능을 재사용할 수 있습니다. 이것은 코드의 중복을 줄이고 유지보수를 쉽게 만듭니다.
3. 여러 유형의 어드바이스: 스프링은 다양한 유형의 어드바이스를 지원합니다. 예를 들어, 메서드 호출 전, 후, 예외 발생 시, 주변(around) 등 다양한 시점에서 어드바이스를 적용할 수 있습니다.
4. 순수한 부가 기능: 어드바이스는 부가 기능을 제공하며, 타갓 메서드 자체를 수정하지 않습니다. 이로 인해 핵심 비즈니스 로직을 영향을 주지 않고 부가 기능을 적용할 수 있습니다.
5. 목적에 따른 분리: 어드바이스는 특정 목적에 따라 정의됩니다. 예를 들어, 로깅 어드바이스는 로그를 출력하고, 트랜잭션 어드바이스는 트랜잭션을 관리합니다.
6. 파라미터 전달: 어드바이스는 타깃 메서드의 아규먼트, 리턴 값, 예외 등과 같은 정보에 접근할 수 있습니다. 이를 통해 부가 기능을 구현할 때 필요한 정보에 접근할 수 있습니다.
7. AOP 프록시를 통한 적용: 어드바이스는 AOP 프록시를 통해 대상 메서드 호출을 가로채고 부가 기능을 적용합니다. 이로 인해 어드바이스가 타깃 오브젝트를 수정하지 않고 부가 기능을 추가할 수 있습니다.
어드바이스는 AOP 프레임워크에서 정의하고 구현하며, 타깃 메서드에 어떤 시점에서 적용할 것인지를 설정합니다. AOP 프록시가 타깃 객체를 감싸고 메서드 호출을 중간에서 가로채면, 어드바이스가 실행됩니다. 이를 통해 관심사의 분리, 코드의 재사용성, 유지보수성 향상과 같은 이점을 얻을 수 있습니다.
스프링과 같은 AOP 프레임워크에서 어드바이스를 구현하고 설정함으로써 애플리케이션에 AOP를 적용할 수 있으며, 이는 특정 비즈니스 로직과는 독립적으로 로깅, 트랜잭션 관리, 보안 등과 같은 공통 관심사를 쉽게 추가하고 변경할 수 있게 해줍니다.
ProxyFactoryBean이 여러 개의 부가기능을 제공하는 프록시를 만들 수 있는 부분은 바로 MethodInterceptor를 여러 개 설정할 수 있다는 점입니다. MethodInterceptor는 스프링 AOP에서 부가기능을 정의하는 데 사용되는 인터페이스입니다.
ProxyFactoryBean은 addAdvice() 메서드를 사용하여 MethodInterceptor를 추가할 수 있습니다. 이를 통해 하나의 ProxyFactoryBean에 여러 개의 MethodInterceptor를 설정할 수 있습니다. 각 MethodInterceptor는 다른 부가기능을 나타내며, 이러한 부가기능들이 모두 하나의 프록시 객체에 적용됩니다.
예를 들어, 다음과 같이 코드를 작성할 수 있습니다:
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(targetObject);
// 여러 개의 MethodInterceptor(부가기능)를 설정
proxyFactoryBean.addAdvice(new BeforeAdvice());
proxyFactoryBean.addAdvice(new AfterReturningAdvice());
proxyFactoryBean.addAdvice(new AfterThrowingAdvice());
// 하나의 ProxyFactoryBean으로 여러 개의 부가기능을 가진 프록시 생성
MyInterface proxy = (MyInterface) proxyFactoryBean.getObject();
위의 코드에서 BeforeAdvice, AfterReturningAdvice, AfterThrowingAdvice는 각각 다른 부가기능을 정의한 MethodInterceptor 구현체입니다. 이러한 부가기능들이 ProxyFactoryBean에 추가되고, ProxyFactoryBean이 getObject() 메서드를 호출하여 하나의 프록시 객체를 생성합니다. 이 프록시 객체는 모든 추가된 부가기능을 가지고 있으며, 대상 객체의 메서드 호출 시에 부가기능이 적용됩니다.
따라서 ProxyFactoryBean 하나만으로 여러 개의 부가기능을 가진 프록시를 만들 수 있습니다. 이것이 스프링의 AOP 프레임워크에서 제공하는 강력한 기능 중 하나입니다.
포인트: 부가기능 적용 대상 메소드 선정 방법
기존에 InvocationHandler를 직접 구현했을 때, 부가 기능 적용 외에도 메소드 이름을 가지고 부가 기능을 적용할 타깃 메소드를 선정해야 했습니다. 예를 들어, TxProxyFactoryBean은 패턴과 메소드 이름을 비교하여 트랜잭션 적용 대상 메소드를 판별하는 기능을 제공했습니다.
그러나 스프링의 ProxyFactoryBean과 MethodInterceptor를 사용하는 방식에서는 메소드 선정 기능을 구현하기에는 어려움이 있습니다. MethodInterceptor는 타깃 정보를 갖고 있지 않으며 여러 프록시가 공유해서 사용할 수 있도록 설계되었습니다. 따라서 특정 프록시에만 적용되는 메소드 선정 패턴을 MethodInterceptor 내부에 구현하기 어렵습니다.
이러한 문제는 다음과 같은 전략을 사용하여 해결할 수 있습니다:
1. MethodInterceptor는 부가 기능 제공에만 집중하고, 메소드 선정 기능은 분리합니다.
2. 메소드 선정 기능은 전략 패턴을 활용하여 별도의 클래스로 구현합니다.
3. 각 프록시에 메소드 선정 전략을 설정하고, 필요에 따라 다른 전략을 사용합니다.
이렇게 함으로써 부가 기능과 메소드 선정 전략이 서로 독립적으로 변경 및 확장될 수 있으며, OCP(Open-Closed Principle) 원칙을 준수하고 코드의 재사용성을 향상시킬 수 있습니다.
그림 6-18에 나타난 스프링의 ProxyFactoryBean 방식은 두 가지 확장 기능인 Advice[부가기능]과 Pointcut[메소드 선정 알고리즘]을 활용하는 유연한 구조를 제공합니다.
스프링에서의 "조인 포인트(Point)"는 부가 기능을 적용할 대상 메소드를 지정하는 역할을 합니다. 조인 포인트는 부가 기능을 어떤 메소드에 적용할 것인지를 결정하는데 사용됩니다. 스프링에서는 포인트컷(Pointcut)이라고도 부릅니다.
포인트는 일반적으로 패키지, 클래스, 메소드 이름 패턴 등의 조건을 사용하여 메소드를 선정하게 됩니다. 예를 들어, 모든 서비스 레이어의 메소드에 트랜잭션 부가 기능을 적용하려면 서비스 레이어의 모든 메소드가 포인트가 될 수 있습니다.
스프링 AOP(Aspect-Oriented Programming)에서는 포인트컷을 정의하고, 어드바이스(Advice)를 특정 포인트컷에 연결하여 부가 기능을 적용합니다. 이를 통해 공통 관심사(예: 로깅, 트랜잭션 관리)를 간단하게 적용하고, 코드의 모듈화와 재사용성을 높일 수 있습니다.
포인트컷을 정의하는 방법은 다양하며, 스프링은 다양한 방식으로 포인트컷을 지정할 수 있는 기능을 제공합니다. 일반적으로 어드바이스와 포인트컷을 함께 사용하여 부가 기능을 적용하는 것이 스프링 AOP의 핵심입니다.
어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용됩니다. 두 가지 모두 여러 프록시에서 공유가 가능하도록 만들어지기 때문에 스프링의 싱글톤 빈으로 등록이 가능합니다.
프록시는 클라이언트로부터 요청을 받으면 먼저 포인트컷에게 부가기능을 부여할 메소드인지를 확인해달라고 요청합니다.
포인트컷은 Pointcut 인터페이스를 구현하면 됩니다.
프록시는 포인트컷으로부터 부가기능을 적용할 타깃 메소드인지 확인받으면, MethodInteceptor 타입의 어드바이스를 호출합니다. 어드바이스는 JDK의 다이내믹 프록시의 InvocationHandler와 달리 직접 타깃을 호출하지 않습니다. 자신이 공유돼야 하므로 타깃 정보라는 상태를 가질 수 없다.
따라서 타깃에 직접 의존하지 않도록 일종의 템플릿 구조로 설계되어 있다. 어드바이스가 부가기능을 부여하는 중에 타깃 메소드의 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출해주기만 하면 됩니다.
실제 위임 대상인 타깃 오브젝트의 레퍼런스를 갖고 있고, 이를 이용해 타깃 메소드를 직접 호출하는 것은 프록시가 메소드 호출에 따라 만드는 Invocation 콜백의 역할이다. 재사용 가능한 기능을 만들어두고 바뀌는 부분(콜백 오브젝트와 메소드 호출정보)만 외부에서 주입 해서 이를 작업 흐름(부가기능 부여) 중에 사용하도록 하는 전형적인 템플릿/콜백 구조입니다. 어 드바이스가 일종의 템플릿이 되고 타깃을 호출하는 기능을 갖고 있는 MethodInvocation 오브젝트가 콜백이 되는 것입니다. 템플릿은 한 번 만들면 재사용이 가능하고 여러 빈이 공 유해서 사용할 수 있듯이, 어드바이스도 독립적인 싱글톤 빈으로 등록하고 DI를 주입해서 여러 프록시 사용하도록 만들수 있습니다.
프록시로부터 어드바이스와 포인트컷을 독립시키고 DI를 사용하게 한 것은 전형적인 전략 패턴 구조입니다. 덕분에 여러 프록시가 어드바이스와 포인트 컷을 공유해서 사용할 수도 있고, 또 구체적인 부가기능 방식이나 메소드 선정 알고리즘이 바뀌면 구현 클래스만 바꿔서 설정에 넣어주면 됩니다. 프록시와 ProxyFactoryBean 등의 변경 없이도 기능을 자유롭게 확장할 수 있는 OCP를 충실히 지키는 구조가 되는 것입니다.
MethodInterceptor로 만들었던 어드바이스와 함께 이름 패턴을 이용해 메소드를 선정하는 포인트까지 적용되는 학습 테스트를 만들어 보겠숩나더. configuration을 통해 빈으로 configuration 파일에 등록해볼 수도 있겠지만 학습 테스트이니만큼 코드만 가지고 DI 되는 구조를 살펴가면서 만들어 보겠습니다.
리스트 6-42는 스프링이 제공하는 NameMatchMethodPointcut을 앞에서 만든 UppercaseAdvice와 함께 사용하도록 만든 테스트 코드입니다. 포인트컷을 직접 만들 수도 있 겠지만, 스프링이 제공하는 다양한 Pointcut 구현 클래스가 있으므로 이를 사용하면 편리합니다. 메소드 선정이 필요 없기 때문에 포인트컷을 적용하지 않았을 때의 테스트와 비교해보기 바랍니다.
리스트 6-42 포인트컷까지 적용한 Proxy FactoryBean
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
pfBean.addAdvice(new UppercaseAdvice());
Hello proxiedHello = (Hello) pfBean.getObject();
assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY");
assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY");
assertEquals(proxiedHello.sayThankYou("Toby"), "THANK YOU TOBY");
}
포인트컷이 필요 없을 때는 ProxyFactoryBean의 addAdvice() 메소드를 호출해서 어드바이스만 등록하면 되었습니다.
그런데 포인트컷을 함께 등록할 때는 어드바이스와 포인트을 Advisor 타입으로 묶어서 addAdvisor() 메소드를 호출해야 합니다.
어드바이스를 등록하듯이 포인트컷도 그냥 추가하면 될 것을 왜 굳이 별개의 오브젝트로 묶어서 등록해야 할까요?
그 이유는 ProxyFactoryBean에는 여러 개의 어드바이스와 포인트컷이 추가될 수 있기 때문입니다. 포인트컷과 어드바이스를 따로 등록하면 어떤 어드바이스(부가기능)에 대해 어떤 포인트(메소드 선정)을 적용할지 애매해지기 때문입니다. 그래서 이 둘을 Advisor 타입의 오브젝트에 담아서 조합을 만들어 등록하는 것입니다. 여러 개의 어드바이스가 등록되더라도 각각 다른 포인트컷과 조합될 수 있기 때문에 각기 다른 메소드 선정 방식을 적용할 수 있습니다. 예를 들어 트랜잭션은 add로 시작하는 메소드에만 적용하지만, 보안 부가기능은 모든 메소드에 적용하고, 기능 검사 부가기능은 get으로 시작하는 메소드에만 적용 할 수가 있습니다. 이렇게 어드바이스와 포인트컷을 묶은 오브젝트를 인터페이스 이름을 따서 어드바이저라고 부른다.
이 세 가지 용어와 그 관계는 매우 중요하고, 앞으로도 자주 언급될 테니 잘 기억해 두어야 합니다. 다음식으로 기억해두면 좋을 것입니다.
어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)
pointcutAdvisor() 테스트에서 사용한 NameMatchPointcut은 mappedName 프로퍼티 값을 이용해 메소드의 이름을 비교하는 방식으로 대상을 선정합니다. sayH*라고 하면 sayH 로 시작하는 메소드에만 선택해 줍니다. 따라서 마지막 메소드인 sayThankYou()는 포인트에 의해 부가기능이 부여되지 않는 메소드가 됐음을 테스트에서 확인할 수 있습니다.
ProxyfactoryBean 적용
JDK 다이내믹 프록시의 구조를 그대로 이용해서 만들었던 TxProxyFactoryBean을 이제 스프링이 제공하는 ProxyFactoryBean을 이용하도록 수정해 보겠습니다.
TransactionAdvice
부가기능을 담당하는 어드바이스는 테스트에서 만들어본 것처럼 MethodInterceptor라는 Advice 서브인터페이스를 구현해서 만듭니다. 리스트 6-43과 같이 JDK 다이내믹 프록시 방식으로 만든 TransactionHandler의 코드에서 타깃과 메소드 선정 부분을 제거해주면 됩니다.
리스트 6-43 트랜잭션 어드바이스
package com.coga.springframe.service;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
public class TransactionAdvice implements MethodInterceptor {
PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = invocation.proceed();
this.transactionManager.commit(status);
return ret;
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
JDK 다이내믹 프록시의 InvocationHandler를 이용해서 만들었을 때보다 코드가 간결합니다. 리플렉션을 통한 타깃 메소드 호출 작업의 번거로움은 MethodInvocation 타입의 콜백을 이용한 덕분에 대부분 제거할 수 있습니다. 타깃 메소드가 던지는 예외도 InvocationTargetException으로 포장돼서 오는 것이 아니기 때문에 그대로 잡아서 처리하면 됩니다.
자바 Configuration
코드는 더 이상 손볼 데가 없습니다. 이제 남은 것은 스프링 자바 configuration 뿐입니다. 학습 테스트에 직접 DI 해서 사용했던 코드를 단지 자바 configuration 설정으로 변경하기만 하면 됩니다.
먼저 리스트 6-44와 같이 어드바이스를 등록합니다. 트랜잭션 기능 적용을 위해 transactionManager만 DI 해주면 됩니다.
다음은 리스트 6-45와 같이 트랜잭션 적용 메소드 선정을 위한 포인트컷 빈을 등록합니다. 스프링이 제공하는 포인트컷 클래스를 사용할 것이므로 빈 설정만 만들어주면 됩니다. 메소드 이름 패턴은 upgrade로 시작하는 모든 메소드를 선택하도록 만듭니다. mappedName 프로퍼티에 upgrade*라고 넣어주면 됩니다.
리스트 6-44 트랜잭션 어드바이스 빈 설정
@Bean
public TransactionAdvice transactionAdvice() {
TransactionAdvice transactionAdvice = new TransactionAdvice();
transactionAdvice.setTransactionManager(transactionManager());
return transactionAdvice;
}
다음은 리스트 6-45와 같이 트랜잭션 적용 메소드 선정을 위한 포인트컷 빈을 등록합니다. 스프링이 제공하는 포인트컷 클래스를 사용할 것이므로 빈 설정만 만들어주면 됩니다. 메소드 이름 패턴은 upgrade로 시작하는 모든 메소드를 선택하도록 만듭니다. mappedName 프로퍼티에 upgrade*라고 넣어주면 됩니다.
리스트 6-45 포인트컷 빈 설정
@Bean
public NameMatchMethodPointcut transactionPointcut() {
NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
nameMatchMethodPointcut.setMappedName("upgrade*");
return nameMatchMethodPointcut;
}
이제 어드바이스와 포인트컷을 담을 어드바이저를 리스트 6-46과 같이 빈으로 등록합니다. 학습 테스트에서는 생성자로 넣어줬지만 프로퍼티를 이용해 DI해도 됩니다.
리스트 6-46 어드바이저 빈 설정
@Bean
public DefaultPointcutAdvisor transactionAdvisor() {
DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();
defaultPointcutAdvisor.setAdvice(transactionAdvice());
defaultPointcutAdvisor.setPointcut(transactionPointcut());
return defaultPointcutAdvisor;
}
이제 ProxyFactoryBean을 등록할 차례입니다. 리스트 6-47과 같이 프로퍼티에 타깃 빈과 어드바이저 빈을 지정해주면 됩니다.
리스트 6-47 Proxy FactoryBean 설정
@Bean
public ProxyFactoryBean userService() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(userServiceImpl());
proxyFactoryBean.setInterceptorNames("transactionAdvisor");
return proxyFactoryBean;
}
어드바이저는 interceptorNames라는 프로퍼티를 통해 넣습니다. 프로퍼티 이름이 advisor가 아닌 이유는 어드바이스와 어드바이저를 혼합해서 설정할 수 있도록 하기 위함입니다. 그래서 property 태그의 ref 애트리뷰트를 통한 설정 대신 list와 value 태그를 통해 여러 개의 값을 넣을 수 있도록 하고 있습니다. value 태그에는 어드바이스 또는 어드바이저로 설정한 빈의 아이디를 넣으면 됩니다. 한 개 이상을 넣을 수 있습니다. 만약 타깃의 모든 메소드에 적용해도 좋기 때문에 포인트의 적용이 필요 없다면 transactionAdvice라고 넣을 수 있습니다.
ProxyFactoryBean으로 변환이 모두 끝났습니다. 코드는 훨씬 간단해졌고 설정만 조금 추 가됐을 뿐입니다. 어드바이스와 포인트컷, 어드바이저 등으로 빈의 숫자가 늘어나서 설정이 더 복잡해진 것 같기도 하지만, 어드바이스와 포인트컷은 여러 ProxyFactoryBean에서 재사용 가능하기 때문에 복잡해진 건 아닙니다.
테스트
테스트 코드도 정리합니다.
여타 테스트는 문제가 되지 않습니다. 순수하게 UserService가 제공하는 기능의 테스트가 목적인 테스트는 프록시 구현이나 설정 방식이 어떤 것이든 상관없이 UserService를 구현한 userService 빈을 가져다 사용하거나, 트랜잭션 따위는 신경 쓰지 않고 고립된 테스트로 만들면 되기 때문입니다.
문제는 학습 테스트로 만든 upgradeAllOrNothing()입니다. 트랜잭션이 적용됐는지를 확인하는 테스트인만큼 스프링의 ProxyFactoryBean을 통해 제공되는 트랜잭션 부가 기능에 대한 테스트를 무시할 수는 없습니다. 예외상황을 강제로 만들어서 테스트하다 보니 로우레벨의 기술이나 구성에 민감한 테스트가 되어서 지금까지 꽤 여러 번 변신을 거쳐 왔습니다.
다행히 이번에는 간단한 수정으로 충분합니다. 스프링의 ProxyFactoryBean도 팩토리 빈이므로 기존의 TxProxyFactoryBean과 같은 방법으로 테스트할 수 있기 때문입니다. 리스트 6-48처럼 팩토리 빈을 직접 가져올 때 캐스팅할 타입만 ProxyFactoryBean으로 간단히 변경해주면 됩니다.
리스트 6-48 ProxyFactoryBean을 이용한 트랜잭션 테스트
@Test
@DirtiesContext
public void upgradeAllOrNothing() throws Exception {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(this.mailSender);
ProxyFactoryBean txProxyFactoryBean =
context.getBean("&userService", ProxyFactoryBean.class);
txProxyFactoryBean.setTarget(testUserService);
UserService txUserService = (UserService) txProxyFactoryBean.getObject();
userDao.deleteAll();
for(User user : users) userDao.add(user);
try {
testUserService.upgradeLevels();
fail("TestUserServiceException expected");
}
catch(TestUserServiceException e) {
}
checkLevelUpgraded(users.get(1), true);
}
이제 스프링이 지원하는 ProxyFactoryBean으로 전환을 모두 마쳤습니다. UserServiceTest 테스트를 실행해 보겠습니다.
어드바이스와 포인트컷의 재사용
ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것입니다. 그 덕분에 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있었습니다. 이제 UserService 외에 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 이미 만들어둔 TransactionAdvice를 그대로 재사용할 수 있습니다. 메소드의 선정을 위한 포인트컷이 필요하면 이름 패턴만 지정해서 ProxyFactoryBean에 등록해주면 된다. 트랜잭션을 적용할 메소드의 이름은 일관된 명명 규칙을 정해두면 하나의 포인트컷으로 충분할 수도 있습니다.
그림 6-19는 ProxyFactoryBean을 이용해서 많은 수의 서비스 빈에게 트랜잭션 부가 기능을 적용했을 때의 구조입니다. 트랜잭션 부가기능을 담은 TransactionAdvice는 하나만 만들어서 싱글톤 빈으로 등록해주면, DI 설정을 통해 모든 서비스에 적용이 가능합니다. 메 소드 선정 방식이 달라지는 경우만 포인트컷의 설정을 따로 등록하고 어드바이저로 조합해서 적용해주면 됩니다.
'Spring Framework' 카테고리의 다른 글
Spring Tool Suite 4 (0) | 2024.04.09 |
---|---|
AOP (0) | 2024.04.09 |
Factory Bean (0) | 2024.04.09 |
Java Dynamic Proxy와 Spring FactoryBean (0) | 2024.04.09 |
InvocationHandler (0) | 2024.04.09 |