스프링을 공부하다 보면 '이건 뭐지?' 싶은 개념들을 만나게 됩니다. 제게는 @Transactional이 그랬습니다. 하나의 동작을 처리하는데 DB 작업이 여러 개 필요할 때, 중간에 하나라도 터지면 데이터가 꼬여버리는 대참사가 발생하잖아요? @Transactional은 바로 이 문제를 해결해주는, 정말 고마운 기능입니다.
이 글에서는 @Transactional이 정확히 무엇인지, 왜 꼭 필요한지, 그리고 어떤 원리로 마법처럼 동작하는지 제가 이해한 내용을 바탕으로 정리해봤습니다.
그래서 @Transactional이 하는 일이 뭐냐고요? 간단합니다. 이 어노테이션이 붙은 메서드를 하나의 팀으로 묶어서, '전부 성공 아니면 전부 실패'를 보장해주는 겁니다.
이 기능이 왜 필수적인지 '계좌 이체' 상황을 생각해보면 바로 와닿습니다. A가 B에게 돈을 보내는 과정은 최소 두 번의 DB 업데이트가 필요하죠.
만약 이런 상황에 트랜잭션 처리가 없다면, A의 돈은 사라졌는데 B에게는 들어오지 않는 끔찍한 상황이 발생합니다. @Transactional은 바로 이런 데이터 불일치 문제를 막아주는 강력한 안전장치입니다.
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional // 이 메서드는 하나의 운명 공동체다!
public void transferMoney(String fromUser, String toUser, int amount) {
accountRepository.withdraw(fromUser, amount); // 1. 출금
accountRepository.deposit(toUser, amount); // 2. 입금
}
}
위 코드에서 deposit 과정에 문제가 생기면, 성공했던 withdraw 작업까지 모두 자동으로 취소됩니다.
처음엔 commit, rollback 코드도 없는데 어떻게 이게 가능한지 정말 신기했습니다. 그 비밀은 바로 스프링이 우리 몰래 소환하는 '대리인(Proxy)' 객체에 있었습니다.
우리가 bankService.transferMoney()를 호출하면, 사실은 스프링이 만들어둔 '대리인'이 먼저 요청을 가로챕니다.
BankService 객체의 transferMoney() 메서드를 호출하여 우리가 작성한 로직을 실행시킵니다.이처럼 복잡한 과정을 대리인이 모두 대신 처리해주기 때문에, 우리는 비즈니스 로직에만 깔끔하게 집중할 수 있는 것입니다.
그럼 이 편리한 걸 아무데나 붙여도 될까요? 보통 DB 작업을 하는 DAO 클래스가 떠오르기 쉽지만, 결론부터 말하면 서비스(@Service) 계층에 붙이는 것이 정답입니다.
'상품 주문' 같은 하나의 비즈니스 '동작'에는 보통 여러 개의 DB 작업이 포함됩니다. (1.재고 감소, 2.주문 생성, 3.결제 기록 등)
이 모든 과정이 하나의 덩어리로 묶여야 의미가 있는데, 만약 각 DAO 메서드에 @Transactional을 붙이면 '재고 감소' 따로, '주문 생성' 따로 트랜잭션이 걸려버립니다.
따라서 여러 DB 접근 로직을 포함하는 하나의 비즈니스 동작 단위, 즉 서비스 계층의 메서드에 @Transactional을 붙여야 합니다.
이제 @Transactional은 더 이상 마법이 아니라, 스프링이 AOP와 Proxy를 이용해 제공하는 똑똑한 자동화 기능이라는 것을 알게 되었습니다. 데이터의 무결성을 보장하고 코드를 깔끔하게 유지해주는 이 고마운 기능을 잘 이해하고 사용하는 것이, 안정적인 애플리케이션을 만드는 데 정말 중요하다는 것을 느꼈습니다.