내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
1. 리플랙션
지난번 글 에서 Proxy의 단점으로 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점을 언급했었다.
다행이 Java는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다!
JDK 동적 프록시의 이해를 위해서는 우선 리플렉션에 대하여 학습해야한다.
리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
여기서는 간단하게만 Reflection에 대하여 알아보자.
혹 더자세한 내용이 필요하다면, 블로그 주인장에 예전에 정리해둔 Reflection 가이드를 읽어보길 권장한다.
우선 문제가 되는 코드부터 살펴보자.
▶ reflection0
@Slf4j
public class ReflectionTest {
@Test
public void reflection0() {
Hello target = new Hello();
//공통 로직1 시작
log.info("start");
String result1 = target.callA(); //호출하는 메서드가 다름
log.info("result={}", result1); //공통 로직1 종료
//공통 로직2 시작
log.info("start");
String result2 = target.callB(); //호출하는 메서드가 다름
log.info("result={}", result2);
//공통 로직2 종료
}
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
}
공통 로직1과 공통 로직2는 호출하는 메서드만 다르고 전체 코드 흐름이 완전히 같다.
먼저 start 로그를 출력한다 -> 어떤 메서드를 호출한다 -> 메서드의 호출 결과를 로그로 출력한다.
어떤 메서드를 호출하는 부분 말고는 전부 동일하다.
log.info("start");
String result = xxx(); //호출 대상이 다름, 동적 처리 필요
log.info("result={}", result);
호출하는 메서드만 동적으로 처리하면 될것 같다.
이때 Reflection을 사용하면 된다!
리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.
▶ reflection1
@Test
public void reflection1() throws Exception {
// 클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result1={}", result1);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result2={}", result2);
}
위 테스트 코드를 보면 다음과 같은 부분이 있다.
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello")
이는 FQCN을 통해서 class에 접근하는 방식이다. 또한 내부 클래스는 구분을 위해 $ 를 사용해준다.
다음 getMethod()를 통하여 callA()의 메타정보를 확인한다.
Method methodCallA = classHello.getMethod("callA");
다음으로는 획득한 메서드 메타정보로 실제로 호출할 인스턴스를 인자로 넘겨준다.
이러면 해당 인스턴의 callA()를 찾아서 실행한다. 다음 코드에서는 target의 callA()를 실행할 것 이다.
Object result1 = methodCallA.invoke(target);
위와 같은 방식으로 메서드를 동적으로 호출할수가 있다.
이를 좀더 활용하여 공통로직의 메서드로 만들어보자!
▶ reflection2
@Test
public void reflection2() throws Exception {
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws InvocationTargetException, IllegalAccessException {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
dynamicCall은 기존의 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 메서드 이다.
첫 번째 파라미터는 호출할 메서드 정보가 넘어온다, 두 번째 파라미터로는 실제로 실행할 인스턴스의 정보가 넘어온다.
다만,
Reflection 기술은 최소한으로 사용해야 한다. Reflection은 컴파일 오류가 잡히지 않는다. 런타임 에러로 발생해 버린다.
가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
따라서 리플렉션은 일반적으로 사용하면 안된다.
지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데, 리플렉션은 그것에 역행하는 방식이다.
2. JDK 동적 프록시 - 소개
지난번 글에서 본 Proxy 예시는 프록시를 어떤 대상에 적용하는가 정도만 차이가 있었다.
쉽게 이야기해서 프록시의 로직은 같은데, 적용 대상만 차이가 있는 것이다.
이 문제를 해결하는 것이 바로 동적 프록시 기술이다. 개발자가 직접 프록시를 만들지 않아도, 동적으로 런타임에 만들어준다!
주의: JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
동적 프록시를 적용할 단순한 예제를 만들어보자. 간단한 AInterface, AImpl, BInterface, BImpl을 만들어보자.
▶ AInterface, AImpl
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
▶ BInterface, BImpl
public interface BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
3. JDK 동적 프록시 - 예제 코드
JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.
우선 JDK 동적 프록시가 제공하는 InvocationHandler Interface는 다음과 같다.
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달한 인수
이는 마치 위에서 살펴봤었던 dynamicCall() 메서드와 유사하다. 부가기능을 동적으로 추가하고 메서드를 호출하게 된다.
이를 구현하는 Handler를 만들어보자. 원하는 로직을 추가해주면 된다.
TimeInvocationHandler에는 시간을 측정하는 로직이 담겨있다. 공통 로직에 해당되는 부분이다.
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
Object target : 필드로 실제로 호출할 대상에 대한 참조를 가지고 있다.
method.invoke(target, args) : 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다. args 는 메서드 호출시 넘겨줄 인수이다.
테스트코드를 통해 살펴보자.
@Slf4j
public class JdkDynamicProxyTest {
@Test
public void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
@Test
public void dynamicB() {
BInterface target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
BInterface proxy = (BInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{BInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
new TimeInvocationHandler(target)은 동적 프록시에 적용할 로직을 가지고 있는 handler이다.
이후 다음 Prxoxy의 생성메서드에 인자를 넘겨준다.
Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler)
클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
실행 결과는 다음과 같다.
우리가 원하던 시간출력 기능이 Proxy를 통해 적용되었다.
프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler 는 공통으로 사용했다.
부가 기능 로직을 한번만 개발해서 공통으로 적용한 것 이다.
결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
생성된 Proxy의 이름이 각각 $Proxy9, $Proxy10 이다.
이것은 우리가 만든 클래스가 아니라 JDK 동적 프록시가 이름 그대로 동적으로 만들어준 프록시이다.
이 프록시는 TimeInvocationHandler 로직을 실행한다.
▶ 실행 순서
1. 클라이언트는 JDK 동적 프록시의 call() 을 실행한다.
2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler 가 구현체로 있으니까 TimeInvocationHandler.invoke() 가 호출된다.
3. TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체( AImpl )를 호출한다.
4. AImpl 인스턴스의 call() 이 실행된다.
5. AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
마지막으로 JDK동적 프록시 적용 전과 후의 그림을 살펴보자.
점선은 개발자가 직접 만드는 클래스가 아니다.
4. JDK 동적 프록시 - 적용1
먼저 LogTrace 를 적용할 수 있는 InvocationHandler 를 만들자!
▶ LogTraceBasicHandler
public class LogTraceBasicHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
public LogTraceBasicHandler(Object target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
로그를 출력하기 위해 message를 만드는 코드부분을 살펴보자.
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
위 코드는 LogTrace 에 사용할 메시지이다.
지난번 글처럼 Proxy를 직접 전부 개발할 때는 "OrderController.request()" 와 같이 프록시마다 호출되는 클래스와 메서드 이름을 직접 문자열로 작성했었다.
하지만 이제는 Method 를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용하면 된다.
다음으로는 빈 설정을 등록해보자.
▶ DynamicProxyBasicConfig
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
LogTraceBasicHandler handler = new LogTraceBasicHandler(orderRepository, logTrace);
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(
OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
handler);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
LogTraceBasicHandler handler = new LogTraceBasicHandler(orderService, logTrace);
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
handler
);
return proxy;
}
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1)
Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace)
);
return proxy;
}
}
모든 Proxy가 공통적으로 LogTraceBasicHandler를 사용하기는 하지만, 그렇다고 Controller, Service, Repository에서 동일한 객체를 공유해서 사용할수는 없다.
왜냐하면 각 Handler 마다 첫번째 인자로 받는것이 다르기 때문이다.
Controller에서는 orderController를, Service에서는 orderService를, Repository에서는 orderRepository를 첫번째 인자로 받는다.
따라서 각각 모두 new를 통해 생성해줘야 한다.
다음과 같이 설정파일만 변경시켜주면 된다.
@Import(DynamicProxyBasicConfig.class)
실행시 정상적으로 동작하는것을 확인 할 수 있다.
런타임 의존관계를 살펴보면 다음과 같다.
아직 우리에게 문제점이 하나 있다.
http://localhost:8080/v1/no-log
no-log를 실행해도 동적 프록시가 적용되고, LogTraceBasicHandler 가 실행되기 때문에 로그가 남는다.
이 부분을 로그가 남지 않도록 처리해야 한다.
5. JDK 동적 프록시 - 적용2
이번 단락에서는 메서드 이름 필터를 추가해보자!
메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능이다!
▶ LogTraceFilterHandler
public class LogTraceFilterHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
private final String[] patterns;
public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
this.target = target;
this.logTrace = logTrace;
this.patterns = patterns;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if(!PatternMatchUtils.simpleMatch(patterns, methodName)) {
return method.invoke(target, args);
}
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
특정 메서드 이름이 매칭 되는 경우에만 LogTrace 로직을 실행한다. 이름이 매칭되지 않으면 실제 로직을 바로 호출한다.
String methodName = method.getName();
if(!PatternMatchUtils.simpleMatch(patterns, methodName)) {
return method.invoke(target, args);
}
Spring에서 제공해주는 PatternMatchUtils.simpleMatch를 사용하여 매칭 유무를 확인하였다.
Config는 다음과 같이 설정해주면 된다.
▶ DynamicProxyFilterConfig
@Configuration
public class DynamicProxyFilterConfig {
public static final String[] PATTERNS = {"request*", "order*", "save*"};
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
LogTraceFilterHandler handler = new LogTraceFilterHandler(orderRepository, logTrace, PATTERNS);
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(
OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
handler);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
LogTraceFilterHandler handler = new LogTraceFilterHandler(orderService, logTrace, PATTERNS);
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
handler
);
return proxy;
}
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1)
Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceFilterHandler(orderController, logTrace, PATTERNS)
);
return proxy;
}
}
다음과 같이 로그를 출력할 대상의 패턴을 적어둔다.
public static final String[] PATTERNS = {"request*", "order*", "save*"};
위 단어들로 시작하는 메서드들을 로그가 남게된다.
다음과 같이 설정파일만 변경시켜주면 된다.
@Import(DynamicProxyFilterConfig.class)
실행해보면 no-log 가 사용하는 noLog() 메서드에는 로그가 남지 않는 것을 확인할 수 있다.
결과 확인을 위해 약간의 변경을 해보자. 우선 PATTERNS를 다음과 같이 변경해보자.
public static final String[] PATTERNS = {"request*", "xxx", "save*"};
PATTERNS를 위와 같이 변경하면, request, xxx, save 로 시작하는 메서드 들만 시간로그가 남게된다.
즉, 기존의 order 로 시작하던 메서드들은 출력되지 않게되는 것 이다.
의도한것 처럼, 실행시 orderService의 orderItem()은 로그추적기가 적용되지 않았다!
6. CGLIB - 예제 코드
JDK 동적 프록시는 인터페이스가 필수이다.
그렇다면 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고 CGLIB 라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야한다.
CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다.
따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
우선 설명에 앞서 예시 코드를 작성하자!
▶ ServiceInterface
public interface ServiceInterface {
void save();
void find();
}
▶ ServiceImpl
@Slf4j
public class ServiceImpl implements ServiceInterface {
@Override
public void save() {
log.info("save 호출");
}
@Override
public void find() {
log.info("find 호출");
}
}
▶ ConcreteService
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호");
}
}
JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 를 제공했듯이, CGLIB는 MethodInterceptor 를 제공한다.
package org.springframework.cglib.proxy;
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수
- proxy : 메서드 호출에 사용
▶ TimeMethodInterceptor
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
TimeMethodInterceptor는 MethodInterceptor 인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의한다.
실제 로직을 호출하는 부분에서 methodProxy를 사용하고 있다.
직전의 JDK를 생각해보면 다음과 같이 호출했었던 기억이 있다.
Object result = method.invoke(target, args)
또한 우리의 intercept() 메서드는 파라미터로 Method가 있기 때문에 가능한 방식이다.
하지만 CGLIB에서는 성능상 다음과 같이 사용하길 권장하고 있다.
Object result = methodProxy.invoke(target, args);
methodProxy를 사용하는 것 이다!
이제 테스트 코드로 CGLIB를 사용해보자.
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
}
1) Enhancer : CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
2) enhancer.setSuperclass(ConcreteService.class) : CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다.
어떤 구체 클래스를 상속 받을지 지정한다.
3) 프록시에 적용할 실행 로직을 할당하기
enhancer.setCallback(new TimeMethodInterceptor(target))
4) enhancer.create() : 프록시를 생성한다.
앞서 설정한 enhancer.setSuperclass(ConcreteService.class) 에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.
실행 결과는 다음과 같다.
CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
대상클래스$$EnhancerByCGLIB$$임의코드
마지막으로 의존관계를 살펴보면 다음과 같다.
하지만 CGLIB 방식에서는 구체클래스를 직접 상속하기 때문에 단점이 있다.
▶ CGLIB 제약
- 부모 클래스의 생성자를 체크해야 한다.
- CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- CGLIB에서는 예외가 발생한다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
- CGLIB에서는 프록시 로직이 동작하지 않는다.
다음에 학습하는 ProxyFactory 를 통해서 CGLIB를 적용하면 이런 단점을 해결하고 또 더 편리하기 때문에, 애플리케이션에 CGLIB로 프록시를 적용하는 방식을 학습해보자!
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 스프링이 지원하는 프록시 - 2 (0) | 2022.08.14 |
---|---|
[Spring] 스프링이 지원하는 프록시 - 1 (0) | 2022.08.14 |
[Spring] 인터페이스, 구체 클래스 기반 프록시 (0) | 2022.08.10 |
[Spring] 데코레이터 패턴 (0) | 2022.08.10 |
[Spring] 프록시 패턴 (0) | 2022.08.10 |
댓글