📌 트랜잭션?
데이터를 저장할 때 데이터베이스를 이용하는 이유는 무엇일까요?
여러 이유가 있을 수 있지만, 대표적으로는 트랜잭션을 지원하기 때문입니다. 이는 하나의 거래를 안전하게 처리하도록 보장한다는 것을 의미합니다. 계좌이체 예시를 들어보겠습니다.
A는 B에게 10,000원을 송금한다고 가정해 봅시다. A의 잔고를 10,000원 감소시키고 B의 잔고를 10,000원 증가시켜야 합니다.
- A의 잔고 -10,000
- B의 잔고 +10,000
1번은 성공했는데 2번에서 문제가 발생하면 A의 잔고만 감소하는 심각한 문제가 발생합니다. 데이터베이스의 트랜잭션 기능을 사용하면 1, 2 둘 다 성공해야 저장하고, 하나라도 실패한다면 이전 상태로 돌아갈 수 있습니다.
ACID
트랜잭션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 합니다.
- 원자성 : 트랜잭션 내 실행한 작업들은 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.
- 일관성 : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다.
- 격리성 : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야 한다.
- 지속성 : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다.
이때 격리성은 락을 통해서 관리할 수 있습니다.
📌 스프링에서의 트랜잭션
스프링을 사용하는 경우, 주로 우리는 트랜잭션을 다음과 같이 적용합니다.
@Transactional
public void example() {
}
@Transactional 어노테이션을 선언하여 매우 편리하게 트랜잭션을 적용할 수 있습니다. 직접 트랜잭션 매니저를 통해 적용하는 것보다 훨씬 간편하고 실용적입니다.
📌 @Transactional의 동작 구조
@Transactional 을 통해 트랜잭션을 적용하면 프록시 방식의 AOP가 적용됩니다.
코드만 보면 단순히 스프링이 해당 메서드를 실행했을 때 트랜잭션을 걸어주는 것처럼 보입니다. 하지만 사실 스프링은 트랜잭션을 처리하기 위해 프록시를 적용합니다.
Transactional 어노테이션이 특정 클래스나 메서드에 하나라도 있으면, 스프링의 트랜잭션 AOP는 트랜잭션을 처리하는 프록시를 적용합니다. 따라서 컨트롤러에서는 트랜잭션을 처리해 주는 프록시 객체가 주입됩니다.
📌 트랜잭션 AOP 주의 사항
프록시 내부 호출
@Transactional을 사용하면, 스프링의 트랜잭션 AOP가 적용됩니다. 트랜잭션 AOP는 프록시 방식의 AOP를 사용하기 때문에 프록시 객체를 통해 대상 객체를 호출해야 합니다.
만약 프록시를 거치지 않고 대상 객체를 직접 호출한다면, AOP가 적용되지 않고 트랜잭션 또한 적용되지 않습니다.
public void sample1() {
sample2();
}
@Transactional
public void sample2() {
// 트랜잭션 코드
}
위의 예시에서 클라이언트가 sample2를 호출한다면 트랜잭션이 적용됩니다. 하지만, 클라이언트가 sample1을 호출하여 내부에서 sample2를 호출했을 때에는 트랜잭션이 적용되지 않습니다.
- sample1() 에는 @Transactional 이 적용되어 있지 않으므로 프록시는 트랜잭션을 적용하지 않는다.
- 실제 Service 인스턴스의 sample1() 호출
- sample1() 은 내부에서 sample2() 호출
이 때 sample2는 프록시를 거치지 않으므로 트랜잭션이 적용되지 않습니다. 이를 해결하기 위해서는 클래스를 분리할 수 있습니다.
- 클라이언트는 Service1.sample1() 호출
- 실제 Service1 인스턴스는 주입받은 Service2.sample2() 호출
- Service2는 트랜잭션 프록시 객체이므로, 트랜잭션 적용
- 트랜잭션 적용 후 실제 Service2 인스턴스의 sample2() 호출
이처럼 트랜잭션이 적용되는 메서드를 클래스를 통해 분리한다면 해결할 수 있습니다.
@Transactional 은 public, protected , default 에만 적용된다.
📌 트랜잭션 전파
트랜잭션이 진행중일 때, 추가로 트랜잭션을 수행하면 어떻게 될까요? 이런 경우 어떻게 동작할지 결정하는 것을 트랜잭션 전파라고 합니다.
외부 트랜잭션과 내부 트랜잭션
트랜잭션 수행 중 다른 트랜잭션 메서드를 수행할 경우, 해당 메서드까지 트랜잭션이 이어집니다. 즉, 스프링은 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션으로 만들어줍니다.
각각의 논리 트랜잭션은 같은 커넥션을 공유합니다. 따라서 다음과 같이 정리할 수 있습니다.
- 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
- 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.
이를 분리하기 위해서는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리하여, 각각 별도의 물리 트랜잭션을 사용하는 방법을 적용할 수 있습니다.
REQUIRES_NEW 옵션을 사용한다면, 내부 트랜잭션은 별도의 트랜잭션을 시작합니다. 따라서 내부 트랜잭션이 실패하여 롤백되더라도, 외부 트랜잭션은 커밋을 수행합니다.
'스프링' 카테고리의 다른 글
@Modifying의 동작 알아보기 (0) | 2025.01.23 |
---|---|
[JPA] 연관관계의 주인이란? (0) | 2025.01.13 |
Mybatis vs JPA (0) | 2025.01.08 |
SELECT 작업에 트랜잭션은 필요할까? (0) | 2025.01.08 |
테스트는 왜 필요할까? (2) | 2024.12.22 |