SQL 트랜잭션과 롤백 처리 완전 정복: 데이터 신뢰성을 지키는 핵심 기술
1. 트랜잭션이란 무엇인가?
1-1. 트랜잭션의 정의와 개념
트랜잭션(Transaction)은 데이터베이스에서 하나의 작업 단위로 처리되는 일련의 쿼리 집합을 의미한다. 단일 작업이라고 표현하지만, 실제로는 여러 SQL 명령문이 포함되며, 이들이 모두 성공해야만 데이터베이스에 반영된다. 하나라도 실패하면 이전까지 수행된 모든 작업이 무효화되어야 하며, 그 상태 이전으로 되돌려야 한다. 예를 들어, 사용자가 계좌 이체를 할 때 보내는 사람의 계좌에서 출금하고, 받는 사람의 계좌에 입금하는 두 가지 작업이 있다. 이 중 하나라도 실패하면 전체 거래는 무효가 되어야 한다. 이처럼 “모두 성공하거나 모두 실패”해야 하는 작업의 묶음이 바로 트랜잭션이다.
1-2. 트랜잭션은 언제 쓰이는가?
실무에서 트랜잭션은 다음과 같은 상황에서 필수적으로 사용된다:
- 주문과 결제, 포인트 차감이 동시에 이뤄지는 장면
- 회원가입과 동시에 프로필, 알림, 기본 설정이 여러 테이블에 들어가는 경우
- 게시글 등록과 동시에 첨부파일, 태그, 히스토리 로그가 생성되는 경우
이처럼 트랜잭션은 단순한 기술이 아니라, 데이터가 신뢰할 수 있는 상태로 유지되도록 보장하는 핵심 수단이다.
2. 트랜잭션이 필요한 이유
2-1. 부분 반영의 위험
트랜잭션을 사용하지 않고 여러 SQL 문을 순차적으로 실행한다면, 중간에 에러가 발생했을 경우 이전 쿼리들은 그대로 DB에 반영되고 이후 쿼리는 실행되지 않게 된다. 이런 경우, 데이터는 논리적으로 일관성 없는 상태에 빠지게 되며, 복구하기도 어렵다. 예를 들어, 포인트 차감은 됐는데 결제 내역은 생성되지 않았거나, 주문은 완료됐는데 재고는 차감되지 않은 상태가 될 수 있다.
2-2. 예외 상황의 대응 수단
트랜잭션이 없으면 예외 발생 시 개발자는 수동으로 상태를 복원하거나 부분적으로 성공한 데이터를 추적해서 정리해야 한다. 이러한 사후 처리 비용이 높기 때문에, 사전 방지 수단으로 트랜잭션이 반드시 필요하다.
3. 트랜잭션의 기본 동작 원리
3-1. 시작, 반영, 되돌리기
트랜잭션의 흐름은 기본적으로 다음 세 가지 단계로 구성된다.
- BEGIN 또는 START TRANSACTION을 통해 트랜잭션 시작
- 중간 작업 수행
- COMMIT으로 모든 작업을 반영하거나, ROLLBACK으로 모두 취소
이 흐름은 단순해 보이지만, 매우 엄격하게 동작한다. 예를 들어 5개의 쿼리 중 4개가 성공하고 마지막 1개에서 오류가 발생하면, 이미 실행된 4개의 작업도 모두 취소된다.
3-2. 자동 커밋(Autocommit) 설정의 의미
MySQL을 포함한 대부분의 RDBMS는 기본적으로 Autocommit이 활성화되어 있다. 즉, 각 SQL 명령문이 실행되자마자 자동으로 COMMIT 되어 데이터베이스에 즉시 반영된다. 이 설정은 SELECT나 간단한 INSERT에는 편리하지만, 여러 단계의 작업을 묶어 하나의 논리 단위로 처리하려는 상황에서는 위험할 수 있다. 이럴 때는 반드시 Autocommit을 비활성화하거나, 명시적으로 트랜잭션 블록을 구성해야 한다.
4. 트랜잭션의 4가지 속성(ACID)
트랜잭션은 단순히 COMMIT, ROLLBACK을 의미하는 것이 아니다. 모든 트랜잭션은 다음의 네 가지 속성(ACID)을 충족해야 한다.
4-1. Atomicity (원자성)
Atomicity는 “모든 작업이 전부 실행되거나, 전혀 실행되지 않아야 한다”는 개념이다. 즉, 트랜잭션의 일부만 성공해서는 안 되며, 중간 상태가 DB에 남아 있어도 안 된다.
4-2. Consistency (일관성)
Consistency는 트랜잭션이 완료되면 데이터베이스의 모든 제약 조건과 규칙을 만족해야 한다는 의미다. 예를 들어 외래 키 제약 조건이 깨지거나, 계좌 잔액이 음수가 되는 상태는 허용되지 않는다.
4-3. Isolation (격리성)
격리성은 동시에 실행되는 여러 트랜잭션이 서로 영향을 주지 않도록 완전히 분리되어야 한다는 의미다. 단, 현실적으로는 이 격리를 완벽하게 보장하기 위해 성능을 희생할 수 있기 때문에, DBMS는 일정 수준에서 조정 가능한 격리 레벨을 제공한다.
4-4. Durability (지속성)
Durability는 트랜잭션이 COMMIT된 이후에는, 시스템에 장애가 발생하더라도 그 변경 사항이 유지되어야 한다는 성질이다. 이는 하드웨어 오류나 프로세스 충돌에도 데이터가 보존되어야 함을 뜻한다.
5. 트랜잭션 격리 수준(Isolation Level)
5-1. 트랜잭션 간 충돌 문제
동시에 여러 트랜잭션이 동작할 때, 읽고 있는 데이터가 수정되거나, 변경 중인 데이터를 읽는 문제가 발생할 수 있다.
이런 충돌을 해결하기 위한 도구가 바로 트랜잭션 격리 수준이다.
5-2. 발생 가능한 문제 유형
- Dirty Read: 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 문제
- Non-repeatable Read: 같은 SELECT인데 두 번 실행 시 결과가 다름
- Phantom Read: SELECT 시점에는 없던 데이터가 COMMIT 전에 삽입되어 결과가 바뀌는 현상
5-3. 대표적인 격리 수준
- READ UNCOMMITTED: 가장 낮은 수준, Dirty Read 허용
- READ COMMITTED: 커밋된 데이터만 읽음, Oracle 기본값
- REPEATABLE READ: 동일 트랜잭션 내에서는 동일 결과 보장, MySQL 기본값
- SERIALIZABLE: 완전한 격리, 가장 안전하지만 동시성 성능 저하
개발자는 성능과 안정성 사이에서 균형을 맞춰 격리 수준을 선택해야 하며, 모든 상황에서 SERIALIZABLE을 고집하면 대기, 락 충돌 문제가 자주 발생한다.
6. 트랜잭션 실무 설계 전략
6-1. 트랜잭션은 짧고 명확하게
트랜잭션 범위가 길면 길수록 락을 점유하는 시간이 길어지고, 다른 트랜잭션의 실행을 차단하게 된다. 이는 곧 시스템 병목, 데드락으로 이어질 수 있다. 가능한 한 짧은 시간 내에 필요한 로직만 수행하고 즉시 COMMIT 또는 ROLLBACK 해야 한다.
6-2. 로그성 테이블은 분리 처리
많은 시스템이 사용자 활동이나 처리 이력을 별도 테이블에 기록한다. 이러한 로그성 데이터는 트랜잭션으로 묶지 말고, 비동기 처리하거나 INSERT만 독립적으로 수행하는 것이 좋다. 그렇지 않으면 본질적인 핵심 로직이 로그 실패로 인해 ROLLBACK될 위험이 있다.
6-3. 트랜잭션 테스트 습관화
실무에서는 반드시 트랜잭션 내 쿼리를 명확히 구분하고, 실패 시 어떤 데이터가 영향을 받는지를 사전에 테스트해야 한다. 특히 ROLLBACK이 제대로 동작하지 않는 구조, 예외 처리가 누락된 로직은 시간이 지나면 치명적 문제로 이어진다.
7. 트랜잭션과 오류 처리 구조 설계
7-1. 예외 발생 시 안전한 처리 플로우
트랜잭션이 필요한 로직에서는 반드시 예외 발생을 대비한 처리 구조를 함께 설계해야 한다. 아무리 트랜잭션을 시작하고 COMMIT 또는 ROLLBACK 구문을 명시하더라도, 중간에 오류가 발생했을 때 이 오류를 정확하게 캐치하지 않으면 트랜잭션이 열린 상태로 남아 시스템 자원을 잠식할 수 있다. 이런 문제는 실시간 API, 배치 작업, 외부 연동 시스템에서 빈번하게 발생한다. 예외가 발생했을 때는 반드시 다음 네 가지를 고려해야 한다
- 트랜잭션이 열려 있는가?
- 롤백이 명시적으로 호출되었는가?
- 예외가 로그로 남았는가?
- 이후 트랜잭션 수행에 영향을 주지 않는가?
7-2. 프레임워크 사용 시 주의점
Spring, Django, Laravel 등 대부분의 프레임워크는 자체 트랜잭션 처리 메커니즘을 제공한다. 하지만 다음과 같은 함정을 주의해야 한다:
- Spring에서는 @Transactional 어노테이션을 붙여도 예외가 try-catch로 감싸지면 롤백되지 않음
- Django에서는 트랜잭션 블록 안에서 예외가 누락되면 rollback이 자동 처리되지 않음
따라서 트랜잭션 처리 시 예외 처리를 프레임워크의 정책에 맞게 명확히 구성하는 것이 중요하다.
8. 트랜잭션과 동시성 이슈: 실전에서 자주 발생하는 문제
8-1. 데드락(Deadlock)
데드락은 두 개 이상의 트랜잭션이 서로가 가진 자원을 기다리면서 무한히 대기 상태에 빠지는 현상이다. 이 문제는 특히 다음과 같은 상황에서 발생한다
- 두 개 이상의 트랜잭션이 동일한 테이블의 다른 row를 갱신하는 경우
- 각 트랜잭션이 먼저 획득한 자원을 상대방이 후속으로 필요로 할 때
- 락 해제 전에 COMMIT 또는 ROLLBACK이 지연되는 경우
데드락은 트랜잭션이 많을수록, 데이터가 많을수록 빈번하게 발생하며 쿼리 순서를 통일하거나, 트랜잭션 범위를 줄이는 것이 가장 현실적인 대응 방법이다.
8-2. 교착 상태 해결 전략
- 트랜잭션 처리 순서를 고정한다 (예: 항상 A → B 순서로 처리)
- SELECT FOR UPDATE 같은 명시적 락을 사용하여 충돌을 조율한다
- 실행 시간이 긴 트랜잭션은 분리하여 처리하고, 빠른 트랜잭션만 DB 안에서 처리한다
이러한 전략을 통해 동시성 문제를 최소화할 수 있다.
9. 트랜잭션 설계의 판단 기준
트랜잭션을 어디서 시작하고 어디서 끝낼 것인가에 대한 명확한 기준이 없으면, 코드 곳곳에서 트랜잭션이 중복되거나 누락되어, 유지보수가 어렵고 장애 대응도 늦어진다.
9-1. 기준 1: 상태 변화 여부
단순 조회(SELECT)는 트랜잭션을 사용할 필요가 거의 없다. 하지만 UPDATE, DELETE, INSERT가 동시에 동작하는 시나리오에서는 반드시 트랜잭션이 필요하다. 또한 상태 변화가 사용자에게 중요한 의미를 가질 경우, 더욱 신중한 처리가 요구된다.
예:
- 포인트 차감
- 게시글 신고 처리
- 예약 승인 여부 결정
이런 작업은 대부분 "되돌릴 수 없음"이라는 전제를 갖고 있기 때문에, 트랜잭션을 통해 일관성 있는 상태 전이가 필요하다.
9-2. 기준 2: 외부 연동 여부
트랜잭션 안에서 외부 시스템(결제 API, 이메일 발송 등)과 통신하면, 응답 지연으로 인해 트랜잭션이 장시간 열린 상태로 유지될 수 있다. 이럴 때는 DB 작업과 외부 연동을 분리하거나, 중간에 로그 저장 및 재처리 로직을 통해 트랜잭션 범위를 좁히는 것이 권장된다.