0. 개요
앞서 작성한 다음과같은 트랜잭션 코드가 있다고 가정할 때, 실제 Dao에 data를 읽어오는 핵심로직과, TransactionManager부분은 분리될 필요가 있다. 왜냐하면 다른 메소드에 같은 코드가 반복될 수 있기 때문이다.
public void upgradeLevels() throws Exception {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
//핵심 로직
List<User> users = userDao.getAll();
for(User user : users) {
if(canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
this.transactionManager.commit(status);
} catch (Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
1. AOP란?
AOP는 애플리케이션의 핵심적인 비즈니스로직으로부터 부가적인 기능을 분리해서 애스펙트(Aspect)로 정의하고 설계하여 개발하는 방식.
2. AOP용어
- Advice : 타깃에게 제공할 부가 기능을 담은 모듈을 의미한다. 즉 앞 예제에 비즈니스로직 전 후로 나오는 트랜잭션을 의미한다.
- Joinpoint : 어드바이스가 적용될 수 있는 위치를 말한다. AOP에서 조인포인트는 메소드의 실행 단계에 적용된다.
- pointcut : 조인포인트를 선별하는 작업 또는 그 기능을 정의한 모듈을 말한다. (조인포인트의 집합)
- adviser : 포인트컷과 어드바이스를 하나씩 가지고 있는 오브젝트를 의미한다.
- aspect: 한개 이상의 포인트컷과 어드바이스의 조합으로 만들어진 공통 기능 (트랜잭션, 로그 등)
3. AOP Proxy
핵심 기능과 부가기능을 나누어 사용할 수 있는 이유는 AOP가 proxy를 이용하기 때문이다.
proxy란?
클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장하여 클라이언트의 요청을 받고 대리하여 업무를 처리하는 대리자 역할.
함수호출자는 주요업무가 아닌 보조업무를 프록시에게 맡기고, 프록시는 내부적으로 보조업무를 처리.
- Proxy를 호출
- 보조업무처리
- 실제 핵심 함수 호출 및 처리
- Proxy로 다시 넘어오고 보조업무처리
- 메서드 반환
4. 예시
개요에서 작성한 트랜잭션코드를 프록시 형태로 수정하면 다음과 같다.
기존 핵심 로직은 UserDao 클래스내부에 그대로 있고 부가기능을 수행할 handler를 따로 만들어서 handler부분이 선처리 후처리를 하도록 하는 예시이다.
TransactionHandler 클래스(부가기능)
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(PlatormTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public Object invoke(Object proxy, 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) throws Throwable {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = method.invoke(target, args); //타깃 함수 호출
this.transactionManager.commit(status); //타깃 호출후 예외가 발생하지 않으면 commit
return ret;
} catch (InvocationTargetException e) { //예외가 발생하면 트랜잭션 롤백
this.transactionManager.rollback(status);
return e.getTargetException();
}
}
}
Test클래스
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(testUserService);
txHandler.setTransactionManager(transactionManager);
txHandler.setPattern("upgradeLevels");
UserService txUserService = (UserService)Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] {UserService.class }, txHandler);
txUserService.upgradeLevels();
5. 프록시 방식의 AOP의 이슈
1. public 클래스를 이용해야 한다.
만약 private클래스에 @Transactional 어노테이션을 활용한다고 가정해보자. @Transactional을 이용하면 스프링에서 프록시를 이용해 프록시 클래스에서 타겟클래스 메소드를 호출하게 하는데, private로 선언된 메소드는 같은 클래스 내에서 만 호출이 가능하다 즉 proxy클래스는 타겟클래스와 같은 클래스가 아니기 때문에 사용이 불가하므로 생기는 이슈
2. 같은 클래스의 메소드가 다른 메소드를 호출할 때 생기는 이슈
다음 그림과 같은 구조에서 update만 트랜잭션이 적용되어야 한다고 하면, 다음과 같은 문제가 발생할 것이다. 외부 클라이언트가 delete를 호출하고, delete는 바로 내부의 update를 호출하는데, 이때 프록시 클래스를 통한 호출이 아니기 때문에 트랜잭션이 적용되지 않는 이슈가 발생한다.
따라서 delete메소드에도 @Transactional을 걸면 delete 요청시 프록시 클래스로 요청이 이루어지고 이후 update를 요청하기 때문에 이슈를 해결할 수 있다.
또는 aspectj를 공식 페이지에서 권장한다.
public void delete(String name) {
//delete 코드
update(name);
}
@Transactional
public void update(String name) {
//업데이트 코드
}
이슈가 발생한 이유는 스프링 AOP 방식을 이용하면 내부 메소드가 다른 메소드를 호출하게 되면 프록시 클래스를 거치지 않기 때문
'프로그래밍 > 시큐어코딩' 카테고리의 다른 글
[디자인 패턴] 옵저버 패턴 (0) | 2020.07.13 |
---|---|
[스프링 이해와 원리] 프록시패턴, 데코레이터패턴이란? (0) | 2020.07.09 |
[스프링 이해와 원리] 서비스단에서 트랜잭션 추상화하기 (0) | 2020.07.08 |
[스프링 이해와 원리] 전략패턴 이란? 순서2 (0) | 2020.07.08 |
[스프링 이해와 원리] IOC와 DI 및 패턴 (순서 1) (0) | 2020.07.08 |