AOP는
IoC/DI
,서비스 추상화
와 더불어 스프링의 3개 기반기술의 하나입니다.
AOP는 스프링의 기술 중에서 가장 이해하기 힘든 난해한 용어와 개념을 가진 기술로 악명이 높습니다....
필연적인 등장배경과 스프링이 그것을 도입한 이유, 그 적용을 통해 얻을 수 있는 장점이 무엇인지에 대한 충분한 이해가 필요합니다.
그래야지만 AOP의 가치를 이해하고 효과적으로 사용할 방법을 찾을 수 있습니다.
(p.401)
이번 6장은 분량이 많아 1, 2로 나누어 포스팅하겠습니다.
6장은 기존의 어떤 문제로 인해 AOP가 등장하게 되었는지, 어떤 방식으로 AOP가 동작하는지 그리고 AOP에 관련된 용어들에 대해 학습합니다.
읽기 모임에서 토비 님께서 직접 AOP가 가장 재미있고 중요하다고 생각한다고 말씀하셨는데, 책에서도 가장 많은 분량이 할애된 걸로 보아 그 말이 진심인 것은 분명히 알 수 있었습니다.
토비 님이 강조하시는 것처럼 AOP는 처음 듣는 용어도 난무하고, 이해하기 힘든 개념을 가졌기 때문에 어렵다고 하시지만 사실 우리는 AOP가 몰라도 AOP를 이제껏 잘 사용해왔다고 하면 이게 무슨 소리인가? 싶을 것입니다.
대표적인 AOP 적용 대상은 바로 선언적 트랜잭션 기능
입니다.
@Transactional
눈치채신 분들도 있겠지만 이번 6장에서는 5장까지 사용해왔던 초난감 DAO의 예외 처리의 문제를 AOP를 사용함으로써 해결하는 식으로 AOP를 학습합니다.
public void upgradeLevels() throws Exception {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
List<User> users = userDao.getAll();
for (User user : users) {
if (policy.canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
// 정상일 경우 commit
this.transactionManager.commit(status);
} catch (Exception e) {
// 예외가 발생할 경우 rollback
this.transactionManager.rollback(status);
throw e;
}
}
위 코드의 문제는 트랜잭션 로직과 비지니스 로직이 한 곳에 모여 있다는 것입니다.
가장 간단하게는 비지니스 로직을 별도의 메서드로 분리하여 각 기능별 코드를 분리할 수 있을 것입니다.
public void upgradeLevels() throws Exception {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
upgradeLevelsInternal();
// 정상일 경우 commit
this.transactionManager.commit(status);
} catch (Exception e) {
// 예외가 발생할 경우 rollback
this.transactionManager.rollback(status);
throw e;
}
}
private void upgradeLevelsInternal() {
List<User> users = userDao.getAll();
for (User user : users) {
if (policy.canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
언뜻 보면 각 기능별 코드가 분리되어 있기 때문에 해결한 듯싶지만, 문제는 여전히 Service 영역에 트랜잭션 기능 코드가 존재하고 있는 것입니다.
이 문제를 해결하기 위해 인터페이스와 위임을 사용합니다.
인터페이스를 사용해서 UserService
를 인터페이스로 변경합니다.
그리고 기존의 비지니스 로직을 담당하는 구현체인 UserServiceImpl
을 작성하고 트랜잭션 기능 코드가 추가된 구현체인 UserServiceTx
를 작성합니다.
그리고 UserServiceTx
에서 같은 인터페이스를 위임한 UserServiceImpl
에게 요청을 위임하면 됩니다.
그렇게 되면 UserServiceTx
에서는 비지니스 로직에 관여하지 않고,
UserServiceImpl
에서는 트랜잭션 로직에 관여하지 않게 됩니다.
public interface UserService {
void add(User user);
void upgradeLevels();
}
public class UserServiceImpl implements UserService {
...
@Override
public void upgradeLevels() {
List<User> users = userDao.getAll();
for (User user : users) {
if (policy.canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
}
public class UserServiceTx implements UserService {
private UserService userService;
private PlatformTransactionManager transactionManager;
public void setUserService(UserService userService) {
this.userService = userService;
}
...
@Override
public void upgradeLevels() {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userService.upgradeLevels();
this.transactionManager.commit(status);
} catch (Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
이제 클라이언트에서는 UserServiceTx
빈을 호출하여 사용하도록 하면 됩니다.
UserServiceTx처럼
자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프락시라고 부릅니다.
그리고 프락시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃 또는 실체라고 부릅니다.
프락시:프락시: UserServiceTx
타깃, 실제 오브젝트: UserServiceImpl
프락시
프락시는 사용 목적에 따라 두 가지로 분리 할 수 있습니다.
- 클라이언트가 타깃에 접근하는 방법을 제어 → 프록시 패턴
- 타깃에 부가적인 기능을 추가하기 위해 → 데코레이터 패턴
둘 다 프락시를 사용한다는 점은 동일하지만 목적에 따라서 디자인 패턴에서는 다른 패턴으로 구분합니다.
다이내믹 프록시 적용
다이내믹 프락시는 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트입니다.
다이나믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어집니다.
프록시 팩토리에게 인터페이스 정보만 전달해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어 줍니
다.
다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만, 부가기능 제공 코드는 직접 작성해야 합니다.
부가 기능은 InvocationHandler
를 구현한 오브젝트에 담습니다.
InvocationHandler
인터페이스는 다음과 같은 메서드 한 개만 가진 인터페이스입니다.
public Object invoke(Object proxy, Method method, Object[] args)
다이나믹 프락시를 적용하면 위와 같은 흐름으로 실제 타깃에 대한 제어, 부가 기능을 추가할 수 있습니다.
public class TransactionHandler implements InvocationHandler {
private Object target;
private PlatformTransactionManager transactionManager;
private String pattern;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeInTransaction(Method method, Object[] args) {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object result = method.invoke(target, args);
this.transactionManager.commit(status);
return result;
} catch (IllegalAccessException | InvocationTargetException e) {
this.transactionManager.rollback(status);
throw new RuntimeException(e);
}
}
}
InvocationHandler
를 구현한 TransactionHandler
클래스를 작성했습니다.
이 클래스는 특정 패턴과 일치하는 메서드만 부가 기능을 적용하며 이때 부가 기능은 트랜잭션 코드 적용입니다.
@Test
public void upgradeAllOrNothing() {
TransactionHandler handler = new TransactionHandler();
handler.setTarget(userService);
handler.setTransactionManager(transactionManager);
handler.setPattern("upgradeLevels");
UserService txUserService = (UserService) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{UserService.class},
handler
);
txUserService.upgradeLevels();
}
위 테스트 코드처럼 TransactionHandler
에 target 정보로 UserService
를 전달하고 TransactionHandler
를 기반으로 UserService
프락시 객체를 생성합니다.
그런데 문제는 DI 대상이 되는 다이내믹 프락시 오브젝트는 일반적인 스프링의 빈으로는 등록할 방법이 없다는 것입니다.
팩토리 빈
그래서 스프링은 팩토리 빈을 사용하여 빈을 생성하는 방법을 제공합니다.
package org.springframework.beans.factory;
public interface FactoryBean<T> {
T getObject() throws Exception; // 빈 오브젝트를 생성해서 돌려준다.
Class<? extends T> getOBjectType(); // 생성되는 오브젝트의 타입을 알려준다.
boolean isSingleton(); // getObject()가 돌려주는 오브젝트가 항상 같은 싱글톤 오브젝트인지 알려준다.
}
@Bean
public ProxyFactoryBean userService() {
ProxyFactoryBean factoryBean = new ProxyFactoryBean();
factoryBean.setTarget(userServiceImpl);
factoryBean.setInterceptorNames("transactionAdvisor");
return factoryBean;
}
@Bean
public TransactionAdvice transactionAdvice() {
TransactionAdvice transactionAdvice = new TransactionAdvice();
transactionAdvice.setTransactionManager(transactionManager);
return transactionAdvice;
}
@Bean
public NameMatchMethodPointcut transactionPointcut() {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("upgrade*");
return pointcut;
}
@Bean
public PointcutAdvisor transactionAdvisor() {
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setAdvice(transactionAdvice());
advisor.setPointcut(transactionPointcut());
return advisor;
}
ProxyFactoryBean
에는 intercepterNames
를 지정해야 하는데 이곳에 Advisor
(어드바이저)를 설정해야 합니다.
어드바이저 = 포인트컷(메서드 선정 알고리즘) + 어드바이스(부가기능)
어드바이저는 위와 같이 부가 기능을 적용할 메서드를 선정하는 알고리즘을 가지고 있는 포인트컷과
적용할 부가 기능을 작성한 어드바이스로 이루어져 있습니다.
어드바이저, 포인트컷, 어드바이스는 AOP에서 사용되는 용어들이며 다음 포스팅에서 정리해 보겠습니다.
어드바이스를 작성하기 위해서는 Advice
인터페이스를 구현해야 합니다. 여기서는 Advice 상속한 서브 인터페이스인 MethodInterceptor
를 구현합니다.
public class TransactionAdvice implements MethodInterceptor {
private 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 result = invocation.proceed();
this.transactionManager.commit(status);
return result;
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
제가 6장을 1, 2로 나누어 포스팅을 하려는 것은 분량도 분량이지만
1은 AOP가 어떻게 등장하게 되었는가, AOP로 인해서 어떤 문제를 해결하려고 하였는가, AOP를 왜 사용해야 하는가에 대해서 중점을 두었다면,
2는 실제로 AOP를 어떻게 사용해야 하는지, AOP는 세부적으로 어떤 식으로 동작하는지, AOP에서 사용되는 용어들은 어떤 것인지에 대해 중점을 두고 있기 때문에
목적에 따라 집중하기 위해 1, 2로 나누어봤습니다.
AOP가 어떤 것이다,라고만 보여주는 것보다 지금처럼 AOP 이전부터 하나의 흐름으로 코드를 설명해주니 AOP를 이해하기 쉬운 것을 넘어서 AOP의 원리, 그리고 왜 AOP를 사용해야 하는지 더 잘 이해할 수 있었던 것 같습니다.
다음 포스팅에 이어서 6장을 마무리하도록 하겠습니다.
'Spring > Toby's Spring Reading Club' 카테고리의 다른 글
1-7. 스프링 핵심 기술의 응용 (0) | 2023.01.15 |
---|---|
1-6. AOP (2) (0) | 2023.01.10 |
1-5장. 서비스 추상화 (0) | 2023.01.08 |
1-4장. 예외 (0) | 2023.01.08 |
1-3장. 템플릿 (0) | 2023.01.07 |
댓글