이전에 step1에서 트랜잭션 + AOP 탄생에 대해 간략한 큰 그림을 그려보았다.
그 과정에서 어떤 노력들이 있었는지 확인해보려고 한다.
필자는 토비의 스프링 3.1을 참고하여 나름대로 해석하여 정리한 내용이다.
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
}
}
기존에 코드에서는 하나의 함수안에 두가지 로직을 포함하는 좋지 않은 코드이다. (비즈니스 로직 + 트랜잭션 로직)
객체지향 설계 원칙 중 첫번째인 SRP(단일 책임 원칙)에 어긋나는 대표적인 예로 볼 수 있다. 코드를 해석하면 사용자를 조회하여 업그레이드를 해주는 로직이다. 이 메소드에서 시작된 트랜잭션 정보는 트랜잭션 동기화 방법을 통해 DAO가 알아서 활용한다.
그렇다면 이 성격이 다른 두 로직을 어떻게 분리하면 좋을 지 고민이다.
메소드를 분리하는건 어떤가?
public void upgradeLevels() throws Exception {
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
upgradeLevelsInternal();
this.transactionManager.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);
}
}
}
가독성이 좋아지고 비즈니스 로직을 깔끔하게 분리했다.
upgradeLevels() 함수를 호출하는 service는 아마 userService라는 계층에서 호출할 것이다. 그렇다면 여전히 트랜잭션을 담당하는 기술적인 코드가 버젓이 사용자의 비즈니스를 처리하는 userService에 포함되는 것이다. 그렇다면 userService를 한층 더 분리해 트랜잭션을 담당하는 UserServiceTx를 생성하고 비즈니스 로직을 처리하는 UserServiceImpl을 생성해 분리하면 이 문제를 해결할 수 있지 않을까? 어떤식으로 이 로직을 자연스럽게 해결할 수 있는지 확인해보자.
UserService라는 인터페이스를 생성하고 이를 구현한 userServiceTx에서 트랜잭션 로직을 처리하고 userServiceTx에서userServiceImpl을 호출하면 되지 않을까? 이런 관계를 형성하게 된다면 이를 호출하는 Client입장에서는 실제 로직을 처리하는 userServiceImpl에 대해 알지 못하게 된다. 서로 결합이 낮아지고 다양한 패턴들이 도입될 수 있을 것 같은 분위기가 느껴진다.
UserService(인터페이스) -> UserServiceTx(트랜잭션 로직) -> UserServiceImpl(비즈니스 로직)
public interface UserService{
void add(User user);
void upgradeLevel();
etc ...
}
public class UserServiceImpl implements UserService{
UserDao userDao;
private void upgradeLevelsInternal(){
List<User> users = userDao.getAll();
for(User user : users){
if(canUpgradeLevel(user)){
upgradeLevel(user);
}
}
...
}
public class UserServiceTx implements UserService{
UserService userService;
PlatFormTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager){
this.transactionManager = transactionManager;
}
public void setUserService(UserService userService){
this.userService = userService;
}
public void add(User user){
this.userService = userService;
}
public void upgradeLevels(){
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
userService.upgradeLevels();
this.transactionManager.commit(status);
}
catch(RuntimeException e){
this.transcationManager.rollback(status);
throw e;
}
}
}
계획했던 대로 UserServieImpl, UserServiceTx를 분리하였고 UserServiceTx는 트랜잭션 로직을 처리하고 UserServiceImpl을 호출하여 비즈니스 로직을 처리한다. 이제 남은 것은 트랜잭션 적용을 위한 DI 설정파일을 수정하는 부분이다.
<bean id="userService" class="package.ex.UserServiceTx">
<property name = "transactionManager" ref="transcationManager" />
<property name = "userService" ref="userServiceImpl" />
</bean>
첫번째 계획했던 대로 UserService(인터페이스) -> UserServiceTx(트랜잭션 로직) -> UserServiceImpl(비즈니스 로직)을 구현했다.
이로 인해 비즈니스 로직을 담당하고 있는 UserServiceImpl 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경쓰지 않아도 된다. 스프링의 JDBC나 JTA 같은 로우레벨의 트랜잭션 API는 물론이고 스프링의 트랜잭션 추상화 API 조차 필요가 없다. 트랜잭션은 DI를 이용해 userServiceTx와 같은 트랜잭션 기능을 가진 오브젝트가 먼저 실행되도록 만들기만 하면 된다. 뿐만 아니라 고립된 비즈니스 로직에 대한 단위테스트를 손쉽게 만들 수 있는 장점이 생겼다. 복잡한 의존 관계 속 테스트에서 문제가 생겼을 때의 폭을 더 좁게 생성할 수 있게 된 것이다.
단순히 확장성을 고려해서 한 가지 기능을 분리한다면 전형적인 전략 패턴(strategy)을 사용하면 된다. 트랜잭션 기능에는 추상화 작업에서 이미 전략 패턴이 녹아져있다. 하지만 트랜잭션 기능의 구현 내용을 분리(ex jdbc, jta, hibernate)해냈을 뿐이다. 이는 step1에 작성한 내용을 참고하자.
전략 패턴을 통한 현재 트랜잭션을 적용한다는 사실은 코드에 그대로 남아있다. 문제는 이렇게 구성한다면 클라이언트(호출하는 대상)가 핵심 기능(비즈니스 로직)을 가진 클래스를 직접 사용해버리면 부가 기능(트랜잭션 로직)이 적용될 기회가 없다는 점이다.
따라서 부가기능(트랜잭션 로직)은 마치 자신이 핵심 기능(비즈니스 로직)을 가진 클래스인 것 처럼 꾸며서 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다. 마치 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 요청을 받아주는 것을 프록시(proxy)라고 하며 위임받아 실제로 처리하는 오브젝트를 타깃(target) 또는 실체(real subject)라고 한다. 데코레이터 패턴과 헷갈리지 말자. 데코레이터 패턴은 런타임 시 다이내믹하게 부가적인 기능을 부여해주기 위해 프록시를 사용하는 패턴을 말한다. 예를 들면 소스 코드를 출력하는데 라인넘버를 붙여준다거나, 색을 추가해준다거나, 페이지를 표시해주는 등의 부가적인 기능을 각각 프록시로 만들어두고
클라이언트 -> 라인넘버 데코레이터 -> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스 코드 출력 기능(타킷) 구성을 구현하는 것이다.
위에서 구현한 UserService 인터페이스를 구현한 타깃인 UserServiceImpl에 트랜잭션 부가기능을 제공해주는 UserServiceTx를 추가한 것도 데코레이터 패턴을 적용한 것이라 볼 수 있다. 이를 구현해보면서 불편한 점이 한 둘이 아니였다. 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거롭다. 인터페이스 메소드가 많아지고 다양해지면 부담스럽고 수정이 필요한 경우 더욱 복잡해진다. 또한 현재 트랜잭션 로직을 처리하는 부가 기능 같은 경우 DB를 사용하는 대부분의 로직에서 적용될 수 있어 코드의 중복이 발생한다.
과거에 이를 어떻게 해결했는지 step3에 이어서 작성한다.
참고 자료 - 토비의 스프링 3.1 | 저자 이일민
'IT' 카테고리의 다른 글
자바 리플렉션(reflection) (0) | 2023.04.30 |
---|---|
스프링 팩토리 빈(Factory Bean) (0) | 2023.04.29 |
스프링 트랜잭션 (Spring Transaction) step1 (0) | 2023.04.22 |
Annotation(어노테이션) 만들기 + spring (0) | 2023.04.21 |
옵저버 패턴(Observer pattern) (0) | 2023.02.22 |