트랜잭션이란 데이터베이스 작업을 하나의 묶음으로 관리하는 기능
이다. 이 묶음은 한 배를 탄 운명이라서 안에 몇 가지 내부 기능이 들어 있든 둘 중에 하나다. 전부 성공하거나, 전부 실패하거나.
예를 들어, ATM에서 계좌이체를 한다고 생각해보자. A의 계좌에서 10만원이 출금되고
B의 계좌에 10만원이 입금되어야 한다.
만약 출금만 되고 입금이 되지 않으면 심각한 문제다. 이렇게 모든 작업이 다 성공해야 성공으로 인정하는 것을 트랜잭션이라고 한다.
트랜잭션의 4가지 특성인 ACID도 위 개념을 이해하고 나면 자연스럽게 이해가 된다.
특성 | 설명 |
---|---|
원자성 (Atomicity) | 한 번에 다 성공하거나, 다 실패해야 함. |
일관성 (Consistency) | 데이터 규칙(제약 조건 등)을 꼭 지켜야 함. |
고립성 (Isolation) | 동시에 여러 트랜잭션이 있어도 서로 간섭 못 함. |
지속성 (Durability) | 성공한 트랜잭션 결과는 영구 보장 (DB 장애가 발생해도 보장) |
트랜잭션이 뭔지 알았으니, 이제 트랜잭션 전파와 관련 용어들에 대해서도 알아보자.
📌트랜잭션 전파란?
스프링에서 메서드들끼리 서로 호출할 때, 트랜잭션을 어떻게 넘겨받고 사용할 지 정하는 규칙이다. 예를 들어 서비스A에서 서비스B를 호출할 때, 서비스 B도 A의 트랜잭션 안에서 돌게 할 건지, 새로 만들건지, 아예 트랜잭션 없이 돌게 할 지 같은 것들을 결정하는 것이다.
위와 같이 주문 기능 안에 들어있는 여러가지 기능들(주문서 작성, 결제, 로그 저장…)의 트랜잭션 진행을 어떻게 할 지 결정하는 것이 전파 속성(Propagation)이다.
📌물리 트랜잭션과 논리 트랜잭션
논리 트랜잭션
논리 트랜잭션은 스프링이 관리하는 트랜잭션
을 말한다. 우리가 서비스 메서드에 @Transactional
을 붙이면 하나의 논리 트랜잭션이 생기는 것이다. 논리 트랜잭션은 서비스 메서드가 호출될 때 트랜잭션을 열고, 끝날 때 커밋하거나 롤백한다.
물리 트랜잭션
물리 트랜잭션은 진짜 DB 커넥션에서 관리하는 트랜잭션
이다. 실제로 데이터베이스에 BEGIN TRANSACTION, COMMIT, ROLLBACK 같은 명령이 실행되는 진짜 트랜잭션을 말한다.
위 이미지의 주문 기능()
에서 결제 기능()
과 로그 저장 기능()
을 호출하고 있다. 트랜잭션 전파 설정이 모두 REQUIRES(디폴트 상태)라면 이 기능들은 하나의 물리 트랜잭션을 공유하게 된다. 즉, 하나라도 실패하면 전체가 다 롤백된다.
📌트랜잭션 전파 속성, 두개만 기억하자
트랜잭션의 전파 속성은 7개가 있는데 REQUIRED(기본값)
와 REQUIRED_NEW
만 확실히 알아두면 된다. 둘의 차이는 다음과 같다.
전파 옵션 | 현재 트랜잭션 있음 | 현재 트랜잭션 없음 |
---|---|---|
REQUIRED (기본값) | 참여 | 새로 생성 |
REQUIRES_NEW | 기존 트랜잭션 보류 | 새로 생성 |
📌REQUIRED_NEW는 언제 쓸까?
위에서 예로 든 주문 기능을 보자. 사실 로그 저장이 성공하든 말든 고객은 아무 상관이 없다. 주문을 하고 결제를 마치면 내 택배가 빨리 도착하기를 기다릴 뿐이다. 로그 저장이 실패했다고 해서 결제까지 실패시켜 버리면 짜증이 난 고객은 다른 사이트로 떠나 버릴 수도 있다. 이럴 때 필요한 것이 REQUIRED_NEW 옵션이다.
REQUIRES_NEW는 현재 트랜잭션이 있어도 무조건 새 트랜잭션을 생성한다. 그 과정에서 기존 트랜잭션은 잠깐 보류 상태가 된다. 위 이미지에서, 새로 생긴 로그 저장 기능()
의 트랜잭션이 실패하면 어떻게 될까? 로그 저장 트랜잭션은 롤백이 되지만, 결제 기능은 영향을 받지 않고 커밋되거나 (실패하면)롤백된다.
비즈니스에 대한 이해도가 높아야 트랜잭션 전파 속성을 올바르게 설정할 수 있을 것 같다. 그리고 REQUIRES_NEW를 사용할 때 주의해야 할 점! 트랜잭션 수만큼 DB 커넥션이 생성되므로 너무 남발하면 DB 부하가 발생할 수 있다.
📌실제 예시를 보면서 이해하기
트랜잭션 어노테이션이 있을때, 없을때, REQUIRES_NEW, 내부 트랜잭션 안에만 뭐가 붙어있을 때… 등등 아주 많은 경우의 수가 있다. 은근히 헷갈리기 때문에 각각의 경우를 차근차근 살펴보면서 커밋/롤백 여부를 확인해보자.
- 트랜잭션 어노테이션이 없을 경우 : 내부 메서드들 중 일부가 실패해도 롤백되지 않고 성공한 내역들만 DB에 커밋된다.
- 트랜잭션 어노테이션이 있을 경우 : 전체가 한 덩어리. 과정 중에 문제가 생기면 전체 롤백된다.
- REQUIRES_NEW가 붙었을 경우 : 외부 메서드에 REQUIRES_NEW가 붙어있어도 여전히
@Transactional
이므로 전체는 한 덩어리이다.
여기까지만 보면 이해가 쉬운데, 조금 더 복잡한 구조도 살펴보자.
자식 트랜잭션과 부모 트랜잭션의 관계도 중요하다.
- 자식 트랜잭션은 보통(REQUIRED, 디폴트 옵션일 때) 부모 트랜잭션과 운명을 함께한다. 그래서 자식 트랜잭션이 성공해도 부모가 실패하면 같이 롤백된다.
- 만약 부모 트랜잭션이 없다면, 자식 트랜잭션은 혼자서 새로운 트랜잭션을 시작한다.
- 자식 트랜잭션에 REQUIRED_NEW가 붙어있으면 : 부모 트랜잭션의 일을 잠깐 멈추고, 새로운 트랜잭션으로 자식 트랜잭션이 독립 실행된다. 쿠키 굽기가 망하거나 말거나 꿋꿋하게 과일을 붙일 수 있는 것이다.
아래와 같은 상황들도 생각해보자.
📌기타 : 트랜잭션 내부에서 변경된 데이터 사용하기
- 트랜잭션 내부에서 변경된 데이터를 바로 사용할 수 있을까? : 같은 트랜잭션 안에서는 아직 DB에 반영이 되지 않았어도 1차 캐시에 저장된 변경사항을 읽을 수 있다.
📌기타 : Transactional(readOnly = true)는 어떻게 작동할까
Transactional(readOnly = true)
을 GET 메서드에 붙이면 성능이 향상된다고들 한다. 어떤 이유로 성능이 향상되는 걸까?
기본적으로 엔티티매니저는 트랜잭션 안에서 쓰기 지연(쓰기 작업을 모아서 한 번에 처리)을 한다. Transactional(readOnly = true)를 붙이면 쓰기 관련 기능이 비활성화 되고, 플러시(DB 반영)도 하지 않고, DB 드라이버에도 읽기 전용이라는 힌트를 줄 수 있다(DB마다 다름). 즉 더티체킹이나 플러시, 쓰기 지연 같은 기능들이 꺼지기 때문에 성능이 약간 좋아질 수 있다.