SQL
[DB] 트랜잭션
코딩로봇
2025. 5. 1. 13:58
금융 IT에서 트랜잭션은 시스템의 신뢰성과 데이터 무결성을 지키는 핵심이다.
은행 송금이나 결제 시스템 같은 비즈니스 로직에서 트랜잭션이 없으면 데이터 꼬임은 물론이고 사용자 신뢰도 무너진다. 이 글은 트랜잭션의 기본 개념, ACID 속성, 상태, 스프링에서의 처리 방식,
그리고 실제 금융 시나리오를 다루며, 최대한 실무 개발자 입장에서 자연스럽게 풀어볼려고 한다.
✅ 트랜잭션이란?
- 데이터베이스에서 하나의 논리적 작업 단위다.
- 예를 들어, 계좌 이체는 출금과 입금 두 작업으로 나뉘는데, 둘 다 성공하거나 둘 다 실패해야 한다. 하나만 성공하면 데이터가 엉망이 되니까.
다른 예시로는 , 아래와 같다.
- 💸 계좌 이체: 송금자의 계좌에서 돈이 빠지고, 수취인의 계좌에 돈이 들어가야 한다. 중간에 하나라도 실패하면 전체 작업을 취소해야 한다.
- ✈️ 비행기여행 예약: 비행기 티켓, 숙소, 비자 발급이 모두 완료되어야 여행이 가능하다. 하나라도 실패하면 나머지도 취소된다.
금융 IT에서는 이런 복잡한 작업을 안전하게 처리하기 위해 트랜잭션이 필수다.
✅ ACID: 트랜잭션의 4가지 필수 속성
- 트랜잭션은 ACID라는 4가지 속성을 통해 데이터의 신뢰성을 보장한다.
Atomicity (원자성) | 모든 작업이 성공하거나, 실패 시 아무것도 반영되지 않는다. 예: 이체 중 입금이 실패하면 출금도 취소. |
Consistency (일관성) | 트랜잭션 후에도 데이터베이스는 항상 일관된 상태를 유지. 예: 이체 후 계좌 잔액 합계는 변함없음. |
Isolation (격리성) | 트랜잭션은 서로 독립적으로 실행. 예: 한 사용자의 이체가 다른 사용자의 잔액 조회에 영향 주지 않음. |
Durability (지속성) | 성공한 트랜잭션은 영구 저장. 예: 시스템 장애가 나도 이체 기록은 남는다 |
✅ 트랜잭션 상태: 어떤 흐름으로 진행되나?
- 활동 상태 (Active) : 트랜잭션이 시작된 상태. 예: 이체 요청 처리 중.
- 부분 완료 상태 (Partially Committed) : 모든 작업은 끝났지만 커밋은 안 된 상태. 예: 출금, 입금 완료 후 커밋 직전.
- 완료 상태 (Committed) : 트랜잭션이 커밋되어 데이터베이스에 영구 반영. 예: 이체 기록이 저장됨.
- 실패 상태 (Failed) : 오류로 진행 불가 상태. 예: 수취인 계좌가 없어 이체 실패.
- 철회 상태 (Aborted) : 롤백으로 이전 상태로 복구. 예: 출금 기록이 취소됨.
✅ 스프링에서의 트랜잭션 관리
- 프링은 트랜잭션 처리를 간편하게 해주는 강력한 도구다.
- 특히 금융 IT에서 @Transactional은 트랜잭션 동작을 제어하는 핵심이다.
♐ 스프링의 롤백 기본 동작
스프링은 예외 발생 시 롤백 여부를 아래처럼 결정한다.
Unchecked Exception (RuntimeException, Error) | ✅ | 기본적으로 롤백. 예: NullPointerException 발생 시 롤백. |
Checked Exception (Exception 상속, RuntimeException 제외) | ❌ | 기본적으로 롤백 안 함. 명시적 설정 필요. |
rollbackFor 설정 | ✅ | 특정 Checked Exception에 대해 롤백 지정 가능. |
noRollbackFor 설정 | ❌ | 특정 예외에 대해 롤백 제외 가능. |
♐ 트랜잭션 내부 호출 문제
- 스프링에서 트랜잭션은 프록시 패턴으로 동작한다. @Transactional이 붙은 메서드는 프록시 객체를 통해 트랜잭션을 시작하고 종료한다.
- 하지만 같은 클래스 안에서 메서드를 호출하면 프록시를 안 거쳐서 트랜잭션이 적용되지 않는다.
public class MyService {
private final OrderRepository orderRepository;
public void save() {
orderRepository.save(new Order("내부 아이템", 10));
innerSave();
}
@Transactional
public void innerSave() {
orderRepository.save(new Order("외부 아이템", 10));
throw new RuntimeException("내부 예외 발생");
}
}
자세히 보기
https://computerreport.tistory.com/132
[Spring] 트랜잭션 내부 호출 문제
https://computerreport.tistory.com/manage/newpost# 티스토리좀 아는 블로거들의 유용한 이야기, 티스토리. 블로그, 포트폴리오, 웹사이트까지 티스토리에서 나를 표현해 보세요.www.tistory.com 해당 글에서 트
computerreport.tistory.com
✅ 트랜잭션 전파 속성 (Propagation)
- 전파 속성은 트랜잭션이 다른 트랜잭션과 어떻게 상호작용할지를 정의한다.
- 금융 시스템에서 주문, 결제, 알림 같은 복잡한 로직을 처리할 때 필수다.
REQUIRED | 새 트랜잭션 생성 | 기존 트랜잭션 참여 | 기본값. 결제, 주문 처리. |
REQUIRES_NEW | 새 트랜잭션 생성 | 기존 트랜잭션 중단, 새 트랜잭션 생성 | 독립적 결제 처리. |
SUPPORTS | 트랜잭션 없이 진행 | 기존 트랜잭션 참여 | 조회 로직. |
NOT_SUPPORTED | 트랜잭션 없이 진행 | 기존 트랜잭션 중단 | 로그 저장. |
MANDATORY | 예외 발생 | 기존 트랜잭션 참여 | 트랜잭션 필수 로직. |
NEVER | 트랜잭션 없이 진행 | 예외 발생 | 외부 시스템 호출. |
NESTED | 새 트랜잭션 생성 | 중첩 트랜잭션 생성 | 부분 롤백 필요한 경우. |
- 예를 들어, 결제는 REQUIRES_NEW로 독립 트랜잭션을 만들고,
- 알림은 NOT_SUPPORTED로 트랜잭션 없이 실행할 수 있다.
✅ 트랜잭션 격리 수준 (Isolation Level)
- 격리 수준은 트랜잭션 간 데이터 접근을 얼마나 엄격히 제어할지 결정한다.
- 금융 시스템에서는 데이터 정합성을 위해 적절한 격리 수준을 선택해야 한다. (MySQL 기준)
격리 수준 | 설명 | 문제점 |
READ UNCOMMITTED | 커밋 안 된 데이터 읽기 가능. | Dirty Read: 잘못된 데이터 읽기. |
READ COMMITTED | 커밋된 데이터만 읽기 가능. | Non-Repeatable Read: 동일 쿼리 결과 불일치. |
REPEATABLE READ (MySQL 기본값) | 동일 트랜잭션 내 동일 결과 보장. | Phantom Read: 추가 데이터로 불일치. |
SERIALIZABLE | 데이터에 락을 걸어 완전 격리. | 성능 저하. |
💡 금융 시스템에서는 REPEATABLE READ가 많이 쓰인다. 계좌 잔액 조회 시 동일 트랜잭션에서 일관된 결과를 보장해야 하기 때문이다. 하지만 중요한 결제 로직에서는 SERIALIZABLE을 고려할 수 있다.
✅ 정리
- 트랜잭션: 데이터베이스 작업의 최소 단위. 금융 시스템에선 이게 없으면 데이터 엉망
- 예: 계좌 이체는 출금+입금이 한 묶음.
- ACID: 트랜잭션의 4대 원칙. 원자성(전부 되거나 전부 취소), 일관성(데이터 항상 깔끔), 격리성(다른 작업과 충돌 X), 지속성(장애 나도 기록 남음).
- 상태 흐름: 시작(Active) → 작업 끝, 커밋 전(Partially Committed) → 성공(Committed) or 에러(Failed) → 롤백(Aborted).
- 스프링에서 트랜잭션 관리:
- @Transactional로 쉽게 제어. 기본적으론 RuntimeException에서 롤백.
- 내부 호출 함정: 같은 클래스 안 메서드 호출은 트랜잭션 안 걸림. 클래스 분리하거나 self-injection 써야 함.
- 전파 속성: REQUIRED(기본, 같이 쓰기), REQUIRES_NEW(따로 트랜잭션), SUPPORTS(트랜잭션 없이도 OK), NOT_SUPPORTED(트랜잭션 끊기), MANDATORY(트랜잭션 필수), NEVER(트랜잭션 절대 X), NESTED(부분 롤백 가능).
- 격리 수준: REPEATABLE READ(MySQL 기본, 잔액 조회에 딱), SERIALIZABLE(결제처럼 민감한 로직에).