한 줄 요약: 트랜잭션은
데이터베이스의 상태를 변화시키기 위한 작업의 단위이다.
저는 지금껏 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메서드는 트랜잭션이 적용되지 않았습니다.