이전에는 Template Method Pattern, Strategy Pattern을 통해서 공통 로직과 서비스 로직을 분리하여 단일 책임을 지키도록 하였다.
이전 글
프록시(Proxy) 패턴은 대리자 역할을 하는 객체를 통해, 실제 객체에 대한 접근을 제어하거나 추가적인 작업을 수행하는 구조적인 디자인 패턴입니다. 프록시 객체는 실제 객체와 동일한 인터페이스를 구현하거나 상속하여 클라이언트가 프록시를 통해 마치 실제 객체처럼 접근할 수 있도록 합니다.
이전 포스팅에서 참고한 요구사항은 다음과 같았습니다.
만약, 이 요구사항에 Running Time을 ms단위에서 sec 단위로 변경해주세요! 라고 접수되면, 개발자는 경우에 따라, 모든 class 파일을 뒤지며 수정해야되는 아름답지 못한 일이 일어날 수 있습니다.
프록시 패턴은 이 점을 완벽하게 보안합니다.
예제에 사용한 코드 : Github
- baekgwa.proxypattern.web.app 참조
- 사용한 Config = ApplicationConfig
컴포넌트 스캔
방식으로 진행하려다가, 어려움이 있어 수동 등록으로 진행 하였습니다.요구사항은 다음과 같습니다.
예제에 사용한 코드 : Github
- baekgwa/proxypattern/gloabl/config/v1_proxy 참조
- 사용한 Config = InterfaceProxyConfig
TestServiceInterfaceProxy
를 등록합니다. 즉 프록시 객체를 등록하죠CGLIB
과 JDK 동적 프록시
를 통해 회피 해 보겠습니다.Template
는 필요 합니다.)리플렉션이란, 클래스나 메서드의 메타 정보를 동적으로 획득하고, 코드도 동적으로 호출하는 방법입니다.
@Slf4j
public class JustReflectionTest {
@Test
void 문제코드() {
Printer printer = new Printer();
log.info("시작");
printer.printA();
log.info("끝");
log.info("시작");
printer.printB();
log.info("끝");
}
@Slf4j
static class Printer {
public void printA(){
log.info("A");
}
public void printB(){
log.info("B");
}
}
}
@Test
void 공통_메서드로_처리()
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Printer printer = new Printer();
Class printerClass = Class.forName(
"baekgwa.proxypattern.reflection.JustReflectionTest$Printer");
dynamicCall(printerClass.getMethod("printA"), printer);
dynamicCall(printerClass.getMethod("printB"), printer);
}
private void dynamicCall(Method method, Object obj)
throws InvocationTargetException, IllegalAccessException {
log.info("시작");
method.invoke(new Object());
log.info("끝");
}
동적 프록시를 생성하기 위해서는 Advisor가 필요합니다.
예제에 사용한 코드 : Github
- baekgwa/proxypattern/gloabl/config/v2_proxyfactory 참조
- baekgwa/proxypattern/gloabl/config/advice 참조
- 사용한 Config = ProxyFactoryConfig
MethodInterceptor
를 implement 하여 사용합니다.org.aopalliance.intercept
를 import 해야 합니다!@RequiredArgsConstructor
public class LoggerAdvice implements MethodInterceptor {
private final Logger logger;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = invocation.getThis().getClass();
String message = targetClass.getSimpleName() + "." + invocation.getMethod().getName() + "()";
LogInfo logInfo = logger.start(message);
Object result = invocation.proceed();
logger.end(logInfo);
return result;
}
}
Pointcut
객체를 생성하면 되는데, 이는 @Configuration 파일에서 같이 진행하도록 하겠습니다.@Configuration
@RequiredArgsConstructor
@Slf4j
public class ProxyFactoryConfig {
private final Logger logger;
@Bean
public TestService testService(){
TestServiceImpl testService = new TestServiceImpl(testRepository());
ProxyFactory factory = new ProxyFactory(testService);
factory.addAdvisor(getAdvisor());
TestService proxy = (TestService) factory.getProxy();
log.info("TestService 프록시 객체 생성 완료");
return proxy;
}
@Bean
public TestRepository testRepository() {
TestRepositoryImpl testRepository = new TestRepositoryImpl(); //실제 동작할 구현체
ProxyFactory factory = new ProxyFactory(testRepository);
factory.addAdvisor(getAdvisor());
TestRepository proxy = (TestRepository) factory.getProxy();
log.info("TestRepository 프록시 객체 생성 완료");
return proxy;
}
private Advisor getAdvisor(){
//Pointcut 생성
//포인트 컷은, 필터와 같은 역할을 한다.
//다양한 포인트 컷이 있다,
// NameMatchMethodPointcut 같은 경우, 메서드와 매칭된 이름을 기준으로 필터링 한다.
Pointcut pointcut = Pointcut.TRUE; //무조건 통과하는 Pointcut
//Advice
//필터링 되고 실제로 프록시 객체가 생성될 때 실행될 로직
//MethodInterceptor 를 구현한 객체가 들어가면 된다.
LoggerAdvice advice = new LoggerAdvice(logger);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
pointcut
과 advice
를 주입하여 만들어 집니다.Pointcut.TRUE
로 넣어주어, 무조건 통과하도록 만들어 주겠습니다. (뒤에서 다시 한번 자세히 다룰 예정입니다.)factory.getProxy()
를 호출해서, 프록시 객체를 return 받고, 다운캐스팅을 통해 각각의 인터페이스에 맞는 객체로 생성하고 반환하여 Bean 등록을 완료 해줍니다.LoggerAdvice
를 따라 작동하게 될 예정입니다.어디에
, 어떻게
라는 구현을 담고있는 객체 입니다.어디에
에 해당하는 내용은 Pointcut
어떻게
에 해당하는 내용은 Advice
라고 이해하시면 편합니다.MethodInterceptor
를 통해, Adviser를 구현하게 되면, invoke() 메서드를 Overriding 해야하는데, 이 메서드가 바로, 프록시 객체가 동작할 동작을 구현하는 부분입니다.Pointcut.TRUE
를 통해 기능을 잠시 막아두었지만, 다음과 같이 설정한다면 메서드 명칭 단위로 필터링을 진행 할 수도 있습니다.*Name
이라는 매칭 조건을 넣어, 해당하는 method만, 프록시 동작을 하도록 만들 수 있습니다.Pointcut
를 implement 해서, 직적 포인트 컷을 만들 수도 있습니다.ProxyFactory
를 통해 원본코드는 전혀 건들이지 않은채로, pointcut을 통해 원하는 메서드, 클래스에만 logger를 적용할 수 있었습니다.빈 후처리기
를 도입하여 이 문제점을 조금더 편하게 사용할 수 있도록 개선해보도록 하겠습니다.