한 줄 요약: 트랜잭션은
데이터베이스의 상태
를 변화시키기 위한 작업의 단위이다.
저는 지금껏 Service Layer에 존재하는 모든 메서드에 @Transactional을 사용했었습니다.
제가 메서드마다 트랜잭션을 선언했던 이유는 간단합니다. 메서드의 모든 작업이 하나의 묶음
으로 진행되길 원했기 때문입니다.
어느날 외부 API를 활용하는 작업을 진행하면서 트랜잭션 내부에서 외부 API를 호출하는 건 좋지 않다
는 사실을 알게 되었습니다. 그래서 리팩토링을 시작했습니다.
우선 이랬던 코드를
@Transactional
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
double exchangeRate = getExchangeRate(source,target); // 외부 API 호출 로직
Exchange exchange = Exchange.of(source,target,remittance,exchangeRate);
exchangeRepository.save(exchange);
return new ExchangeMoneyResponse(exchange.calculateRemittance(),target);
}
이렇게 수정했습니다.
@Transactional
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
double exchangeRate = getExchangeRate(source,target); // 외부 API 호출 로직
Exchange exchange = saveExchange(source,target,remittance,exchangeRate);
return new ExchangeMoneyResponse(exchange.calculateRemittance(),target);
}
@Transactional
public Exchange saveExchange(Currency source, Currency target, Remittance remittance, double exchangeRate){
Exchange exchange = Exchange.of(source,target,remittance,exchangeRate);
return exchangeRepository.save(exchange);
}
@Transactional
이 필요하다는 잘못된 생각에 사로잡혀 있는 상태입니다. 위 코드는 메서드만 분리했을 뿐 결과적으로 아무것고 달라진 게 없습니다.한번 더 공부한 뒤 수정한 결과는 아래와 같습니다.
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
double exchangeRate = getExchangeRate(source,target); // 외부 API 호출 로직
Exchange exchange = saveExchange(source,target,remittance,exchangeRate);
return new ExchangeMoneyResponse(exchange.calculateRemittance(),target);
}
@Transactional
public Exchange saveExchange(Currency source, Currency target, Remittance remittance, double exchangeRate){
Exchange exchange = Exchange.of(source,target,remittance,exchangeRate);
return exchangeRepository.save(exchange);
}
@Transactional
이 필요하지 않습니다. 생각해보면 트랜잭션은 작업이 정상적이지 않을 때 롤백
하기 위한 기능입니다. DB와 관련되지 않은 작업에서 '롤백'이라는 개념은 어울리지 않습니다. 메서드 스택이 소멸되면 어차피 안에 있던 변수들은 함께 사라지게 되는데 굳이 '롤백'이라는 의미를 지닐 필요가 없다는 걸 깨달았습니다.
아쉽게도 위 코드 역시 우리가 원하는대로 동작하지 않습니다. TransactionSynchronizationManager.isActualTransactionActive()
는 해당 메서드에 트랜잭션이 적용되는지 확인할 수 있는 코드입니다. 이를 통해 트랜잭션 적용 여부를 확인해 보겠습니다.
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
System.out.println("exchangeMoneyMethod: "+txActive);
double exchangeRate = getExchangeRate(source,target); // 외부 API 호출 로직
Exchange exchange = saveExchange(source,target,remittance,exchangeRate);
return new ExchangeMoneyResponse(exchange.calculateRemittance(),target);
}
@Transactional
public Exchange saveExchange(Currency source, Currency target, Remittance remittance, double exchangeRate){
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
System.out.println("saveExchangeMethod: "+txActive);
Exchange exchange = Exchange.of(source,target,remittance,exchangeRate);
return exchangeRepository.save(exchange);
}
위와 같이 설정한 뒤 코드를 실행하면 다음과 같은 결과가 나옵니다.
exchangeMoney
는 애초에 @Transactional
을 적용하지 않았기 때문에 트랜잭션이 적용되지 않는게 쉽게 이해됩니다. 하지만 saveExchange
메서드는 @Transactional
을 선언했는데 왜 트랜잭션이 적용되지 않았을까요?
스프링의 @Transactional
은 프록시 방식
으로 동작합니다. @Transactional
이 활용된 클래스는 원본 객체가 아닌 프록시 객체
가 등록되고, 트랜잭션이 적용된 메서드는 프록시 객체에서 전후로 트랜잭션 작업을 처리하고 중간에 원본 메서드가 호출되는 방식이 됩니다.
그렇기 때문에 트랜잭션이 적용되려면 항상 프록시 객체를 통해 원본 객체
를 호출해야 합니다.
원본 객체인 originalClass는 다음과 같이 생겼다면
public class OriginalClass{
@Transactional
public void txMethod(){}
}
스프링 빈에 등록되는 프록시 객체는 대략적으로 다음과 같은 모양일 겁니다.
public class ProxyClass{
@Autowired
private OriginalClass originalClass;
public void txMethod(){
tx.start();
originalClass.txMethod();
tx.end();
}
}
이러한 내용을 바탕으로 우리 코드를 다시 살펴보겠습니다.
public class ExchangeService{
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
double exchangeRate = getExchangeRate(source,target);
Exchange exchange = saveExchange(source,target,remittance,exchangeRate);
return new ExchangeMoneyResponse(exchange.calculateRemittance(),target);
}
@Transactional
public Exchange saveExchange(Currency source, Currency target, Remittance remittance, double exchangeRate){
Exchange exchange = Exchange.of(source,target,remittance,exchangeRate);
return exchangeRepository.save(exchange);
}
}
@Transactional
이 적용됐기 때문에 프록시 객체
로 스프링 빈에 등록될 겁니다.public class ExchangeServiceProxy{
@Autowired
private ExchangeService exchangeService;
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
return exchangeService.ExchangeMoney();
}
public Exchange saveExchange(Currency source, Currency target, Remittance remittance, double exchangeRate){
tx.start();
Exchange value = exchangeService.saveExchange();
tx.end();
return value;
}
}
exchangeMoney
는 트랜잭션이 설정되어 있지 않기 때문에 곧바로 원본 객체 인스턴스(exchangeService
)의 ExchangeMoney를 호출합니다.ExchangeService
의 exchangeMoney
는 saveExchange
메서드를 호출합니다. 이때 메서드를 호출하는 saveExchange(source,target,remittance,exchangeRate);
는 사실 this.saveExchange(source,target,remittance,exchangeRate);
입니다. this
는 ExchangeService
를 의미하기 때문에 ExchangeServiceProxy
의 saveExchange
가 아니라 ExchangeService
의 saveExchange
가 실행되고, 이는 트랜잭션이 적용되지 않는 일반 메서드가 됩니다.@Transactional
이 선언된 클래스는 원본 객체가 아니라 프록시 객체
가 스프링 빈에 등록된다.프록시 객체를 통해 원본 객체
를 호출해야 합니다.@Transactional
이 선언된 메서드를 호출(내부 호출)하더라도 이는 프록시 객체를 호출하는 게 아니라 원본 객체를 곧바로 호출하는 방식이기 때문에 트랜잭션이 적용되지 않습니다.
내부 호출로 원본 객체를 직접 호출했기 때문에 프록시 객체를 활용하지 못했습니다.
해당 메서드를 별도의 클래스로 분리하면 더 이상 내부 호출이 아니기 때문에 정상적으로 프록시 객체를 활용할 수 있게 됩니다.
트랜잭션을 적용했던 saveExchange메서드를 별도의 클래스에서 정의했습니다.
@Component
@RequiredArgsConstructor
public class SaveExchange {
private final ExchangeRepository exchangeRepository;
@Transactional
public Exchange saveExchange(Currency source, Currency target, Remittance remittance, double exchangeRate){
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
System.out.println("saveExchangeMethod: "+txActive);
Exchange exchange = Exchange.of(source,target,remittance,exchangeRate);
return exchangeRepository.save(exchange);
}
}
ExchangeService는 정의된 클래스를 주입받아 호출합니다. 이 과정에서 프록시 객체를 주입받게 되므로 트랜잭션을 원하는대로 활용할 수 있습니다.
@Service
@RequiredArgsConstructor
public class ExchangeService {
// 주입
private final SaveExchange saveExchange;
public ExchangeMoneyResponse exchangeMoney(Currency source, Currency target, double givenRemittance){
Remittance remittance = Remittance.from(givenRemittance);
double exchangeRate = getExchangeRate(source,target);
boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
System.out.println("exchangeMoneyMethod: "+txActive);
Exchange exchange = saveExchange.saveExchange(source,target,remittance,exchangeRate);
return new ExchangeMoneyResponse(exchange.calculateRemittance(),target);
}
}
실행 결과는 다음과 같습니다.
처음 목표한대로 saveExchange메서드에 대해서만 트랜잭션이 적용되고, 외부 API통신을 진행하는 getExchangeRate작업을 담은 exchangeMoney메서드는 트랜잭션이 적용되지 않았습니다.