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.transactionalManager.commit(status);
} catch (Exception e) { // 트랜잭션 경계 설정
this.transactionManager.rollback(status);
throw e;
}
}
이 코드의 특징은 트랜잭션 경계설정 코드와 비즈니스 로직 코드 간 서로 주고받는 정보가 없으며 두 가지의 코드가 뚜렷하게 구분되어 있음을 알 수 있다.
비즈니스 로직에서 데이터베이스에 접근하지 않기 때문에 트랜잭션 준비 과정에서 생성된 DB 커넥션 정보등을 직접 참조할 필요가 없기 때문이다.
이 메소드에서 시작된 트랜잭션 정보는 트랜잭션 동기화 방법을 통해서 DAO가 알아서 활용한다.
즉, 이 두 코드는 서로 주고받는 것없이 독립된 코드라고 할 수 있다. 저 비즈니스 로직은 트랜잭션의 시작과 종료 사이에서 수행하도록 코드를 두개의 메서드로 분리시킬 수 있다.
public void upgradeLevels() throws Exception {
TransactionStatus status = this.transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
upgradeLevelsInternal();
this.transactionalManager.commit(status);
} catch (Exception e) {
this.transactionManager.rollback(status);
throw e;
}
}
private void upgradeLevelsInternal() {
List<User> users = userDao.getAll();
for (User user : users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
두개의 로직을 분리하여 이해하기 편하고 비즈니스 로직의 코드를 수정하기 간편해졌으며 실수로 트랜잭션 코드를 건드릴 일도 없어졌다.
하지만 트랜잭션을 담당하는 코드가 UserService안에 존재하고 있다. 따라서 UserService 인터페이스를 생성해 구현체와 트랜잭션을 담당하는 클래스로 분리시킨다.
UserService 인터페이스
public interface UserService {
void add(User user);
void upgradeLevels();
}
UserServiceImpl 구현 클래스
public class UserServiceImpl implements UserService {
UserDao userDao;
MailSender mailSender;
public void upgradeLevels() {
List<User> users = userDao.getAll();
for(User user: users) {
if (canUpgradeLevel(user)) {
upgradeLevel(user);
}
}
}
}
UserServiceTx 클래스
@RequiredArgsConstructor
public class UserServiceTx implements UserService {
final UserService userService;
final PlatformTransactionManager transactionManager;
@Override
public void add(User user) {
userService.add(user);
}
@Override
public void upgradeLevels() {
TransactionStatus status = transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
userService.upgradeLevels();
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
}
UserServiceTx는 비즈니스 로직을 갖지 않고 다른 UserService 구현체에 기능을 위임하도록 생성자 주입으로 DI 시킨다.
트랜잭션이라는 기능은 비즈니스 로직이 아니기 때문에 밖으로 분리가 가능하다. 따라서 부가기능을 담은 클래스로 분리했었다. 핵심기능은 부가기능을 가진 클래스의 존재 자체를 모르기 때문에 부가기능이 핵심기능을 사용하는 구조가 된다.
하지만, 클라이언트가 핵심기능을 가진 클래스를 직접 사용한다면 부가기능이 적용될 기회가 없기 때문에 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다.
그러기 위해서 클라이언트는 인터페이스를 통해서만 핵심기능을 사용하게 해야하고 부가기능도 같은 인터페이스를 구현한 뒤에 자신이 그 사이에 끼어들도록 해야 한다. 그러면 클라이언트는 인터페이스만 보고 사용하므로 핵심기능을 가진 클래스를 사용할 것이라고 기대하지만, 사실 부가기능을 통해 핵심기능을 이용하게 되는 것이다.
부가기능 코드에서는 핵심기능으로 요청을 위임해주는 과정에서 자신이 가진 부가적인 기능을 적용해줄 수 있다. 비즈니스 로직 코드에 트랜잭션 기능을 부여해주는 것이 대표적인 경우
이렇게 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리인과 같다고해서 Proxy라고 한다.
프록시를 통해 요청을 위임받아 처리하는 실제 오브젝트를 Target이라고 부른다. 즉, 아래와 같은 구조로 수행된다.
타깃에 부가적인 기능을 런타임에 동적으로 부여해주기 위해 프록시를 사용하는 패턴을 말한다.
데코레이터 패턴에서는 프록시가 꼭 한개로 제한되지 않으며 프록시가 직접 타깃을 사용하도록 고정시킬 필요도 없다. 그래서 같은 인터페이스를 구현한 타겟과 여러개의 프록시를 사용가능하다. 프록시가 여러개인 만큼 순서를 정해 단계적으로 위임하는 구조를 만들면 된다.
프록시 패턴은 프록시와 다르며, 프록시를 사용하는 방법 중 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우를 말한다. 프록시 패턴의 프록시는 타깃의 기능을 확장하거나 추가하지 않고 클라이언트가 타깃에 접근하는 방식을 변경해준다.
타깃 오브젝트를 당장 필요하지 않은 경우 꼭 필요한 시점까지 오브젝트를 생성하지 않는 것이 좋은데, 이럴 때 프록시 패턴을 적용하면 된다. 클라이언트에게 타깃에 대한 레퍼런스를 넘겨야 하는데 실제 타깃 오브젝트를 만드는 대신 프록시를 넘겨주는 것이다. 그리고 프록시의 메소드를 통해 타깃을 사용하려고 시도할 때, 그 때 프록시가 타깃 오브젝트를 생성하고 요청을 위임해주면 된다.
프록시 패턴은 타깃의 기능 자체에는 관여하지 않으면서 접근하는 방법을 제어해주는 프록시를 이용한 패턴이다.