요즘 점진적인 예제를 통해서 학습하는 과정들이 Spring framework을 이해하는데 큰 도움이 되는 걸 느끼고 있는데요. 블로그를 통해서 이런 과정들을 정리해보려고 합니다.
토비의 스프링을 보면 트랜잭션 처리에 대한 공통적인 로직의 분리를 예제로 들고 있습니다. 저는 service 호출마다 공통적인 시작, 종료 로그를 찍어야 한다고 요구사항을 잡고 예제를 진행하도록 하겠습니다.
Decorator + Proxy Pattern
공통적인 로그를 출력하는 로직과 service로직을 분리하기 위해 전략 패턴을 통해서 분리는 가능하지만 부가기능이 핵심기능을 사용하게 되는 구조를 가져가게 됩니다. 이 경우에 핵심기능을 가져간 클래스는 부가기능 클래스의 존재를 알 수 없게 되고 실제로 사용할 때 직접 핵심기능으로 접근해버리면 분리한 의미가 없어지게 되는 단점이 있습니다.
그래서 Proxy Pattern 과 Decorator Pattern을 적용하게 됩니다. 우선 Proxy 패턴에서 말하는 Proxy는 실제 타겟에 대한 접근방법을 제어하는 목적으로 구현하고, 데코레이터 패턴은 부가적인 기능을 런타임 시 동적으로 부여하기 위한 프록시로 구현합니다.
데이터를 추가, 조회하는 핵심 로직과 호출과 종료에 로그를 출력하는 부가기능을 분리하는 예제를 준비했습니다.
클라이언트는 로그를 출력하는 부가기능의 프록시를 통해 데이터 관련 핵심기능을 처리하게 됩니다. 데이터와 관련된 핵심기능은 부가기능의 프록시를 통해서 요청을 위임받아 처리하게 되는 구조입니다.
우선 핵심 기능에 대한 인터페이스를 정의했습니다.
public interface DataService {
String getData(Long key);
List<String> getDatas();
void addData(String data);
}
핵심기능을 구현한 Proxy의 실제 객체를 만들었구요.
public class DataServiceTarget implements DataService {
private static final Logger LOGGER = LoggerFactory.getLogger(DataServiceTarget.class);
private static final Map<Long, String> dataMap = new HashMap<>();
private Long index = 0L;
public List<String> getDatas(){
return dataMap.keySet().stream()
.map(dataMap::get)
.collect(Collectors.toList());
}
public String getData(Long key){
return dataMap.get(key);
}
public void addData(String data){
dataMap.put(++index, data);
}
}
핵심기능 사이에 끼어들어 부가기능을 구현하고 핵심 기능들은 핵심기능의 실제 객체로 위임하는 코드를 구현했습니다.
public class DataLogDecorator implements DataService {
private static final Logger LOGGER = LoggerFactory.getLogger(DataLogDecorator.class);
private final DataService dataService;
public DataLogDecorator(DataService dataService) {
this.dataService = dataService;
}
@Override
public String getData(Long key) {
LOGGER.info("start");
String result = dataService.getData(key);
LOGGER.info("end");
return result;
}
@Override
public List<String> getDatas() {
LOGGER.info("start");
List<String> result = dataService.getDatas();
LOGGER.info("end");
return result;
}
@Override
public void addData(String data) {
LOGGER.info("start");
dataService.addData(data);
LOGGER.info("end");
}
}
이런 구조를 가져가면서 부가기능이 마치 핵심 기능인 것 같이 동작하고 부가기능은 핵심기능으로 처리를 위임하게 됩니다.
이 방법의 문제점은 두 가지가 있습니다. 부가기능을 추가할 때마다 클래스를 만들고 핵심기능을 위임하는 코드를 작성해야 한다는 점입니다. 그리고 다른 핵심 로직에도 동일한 부가기능을 제공하려면 중복된 코드가 발생하게 되는 점이 있습니다.
이런 방법을 해결하기 위한 방법으로 jdk Dynamic Proxy를 사용하게 됩니다.
Dynamic Proxy + Factory Bean
jdk Dynamic Proxy를 사용해서 위 방식의 문제점을 개선할 수 있습니다. Dynamic Proxy는 프록시 팩토리에 의해 런타임 시 동적으로 생성되는 오브젝트입니다. 코드를 보면서 어떤 식으로 개선되는지 확인해볼게요.
public class DataLogHandler implements InvocationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(DataLogHandler.class);
private final Object target;
public DataLogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
LOGGER.info("start");
Object result = method.invoke(target, args);
LOGGER.info("end");
return result;
}
}
Dynamic Proxy는 Java reflection을 활용하는데요. InvocationHandler를 통해 실제 Target에 대한 reference를 가지고 있으면 Reflection을 통해서 위임 코드를 간단히 할 수 있습니다.
@DisplayName("dynamic proxy")
@Test
void testDynamicProxy() {
DataService dataService = (DataService)Proxy.newProxyInstance(getClass().getClassLoader(),
new Class[] {DataService.class},
new DataLogHandler(new DataServiceTarget()));
dataService.addData("test");
dataService.getData(1L);
dataService.getDatas();
}
Proxy 팩토리 메서드를 통해 Proxy객체를 생성하는 코드입니다. 이전 Decorator패턴을 활용한 것과 다르게 핵심기능을 위임하는 클래스와 코드를 작성하지 않아도 reflection을 통해 수행된다는 점에서 코드가 간결해졌습니다.
하지만 Dynamic Proxy도 단점이 있는데요. 일반적인 스프링 빈으로 등록할 수 없어 FactoryBean을 통해 등록해야 합니다.
public class DataLogProxyFactoryBean implements FactoryBean<Object> {
private Object target;
private Class<?> serviceInterface;
public void setTarget(Object target) {
this.target = target;
}
public void setServiceInterface(Class<?> serviceInterface) {
this.serviceInterface = serviceInterface;
}
@Override
public Object getObject() throws Exception {
DataLogHandler dataLogHandler = new DataLogHandler(target);
return Proxy.newProxyInstance(getClass().getClassLoader(),
new Class[] {serviceInterface},
dataLogHandler);
}
@Override
public Class<?> getObjectType() {
return null;
}
}
@Bean
public DataLogProxyFactoryBean dataLogProxyFactoryBean() {
DataLogProxyFactoryBean dataLogProxyFactoryBean = new DataLogProxyFactoryBean();
dataLogProxyFactoryBean.setServiceInterface(DataService.class);
dataLogProxyFactoryBean.setTarget(new DataServiceTarget());
return dataLogProxyFactoryBean;
}
FactoryBean을 통해 등록하더라도 한 번에 여러 클래스에 공통의 부가기능을 추가하기에는 어려움이 있습니다.
ProxyFactoryBean
스프링의 ProxyFactoryBean은 위에서 발생한 문제점들을 깔끔하게 해결합니다. (앞서 만든 FactoryBean과 달라요..!)
ProxyFactoryBean은 순수하게 프록시 생성 작업만 담당합니다. 그리고 부가기능은 MethodInterceptor 인터페이스를 통해 구현합니다.
public class DataLogAdvice implements MethodInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(DataLogAdvice.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
LOGGER.info("start advice");
Object result = invocation.proceed();
LOGGER.info("end advice");
return result;
}
}
이런 식으로 부가기능을 제공하는 객체를 생성할 수 있고 이를 Advice라고 부릅니다.(예제로 설명하진 않았지만 Pointcut은 Advice를 어디에 적용할지를 선정하는 방법을 의미하고, Pointcut + Advice = Advisor가 됩니다)
위 방식들과 다르게 Advice는 핵심기능 위임을 위한 Target reference가 없다는 점인데요. ProxyFactoryBean을 통해 Advice를 등록하는 방식으로 Target에 의존적이지 않아 추가적인 클래스 생성 없이 재사용이 가능하다는 장점이 있습니다.
@DisplayName("ProxyBeanFactory test")
@Test
void testProxyBeanFactory() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(new DataServiceTarget());
proxyFactoryBean.addAdvice(new DataLogAdvice());
DataService service = (DataService)proxyFactoryBean.getObject();
service.addData("test");
service.getData(1L);
service.getDatas();
}
ProxyFactoryBean에 target과 Advice를 추가하는 방식으로 구현하게 됩니다.
Wrap-up
AOP는 횡단 간 관심사의 분리(cross-cutting-concenrs)를 통해 부가기능을 별도의 모듈로 분리 및 설계하는 개발 방법이라고 이해할 수 있었습니다. Proxy Decorator패턴, Dynamic Proxy, ProxyBeanFactory 방식으로 점진적인 개선을 했고 어떤 문제점들이 있었는지 기억해두면 좋을 것 같습니다.
글이 길어지는 것 같아서 Proxy를 통한 AOP로 정리를 마무리하고, AspectJ는 추후 포스팅으로 CGLib과 함께 또 한 번 정리하도록 하겠습니다. :)
Reference..
토비의 스프링 Vol.1편 - 6장
'Spring' 카테고리의 다른 글
Annotation based Interceptor (0) | 2021.09.10 |
---|---|
Spring MVC - Filter, Interceptor (0) | 2021.08.11 |
Spring Transaction (0) | 2021.08.09 |
댓글