한줄 요약: 트랜잭션의 COMMIT과 ROLLBACK을 실전에서 안전하게 사용하는 방법과 실수 방지 패턴을 배웁니다.
“결제는 됐는데 포인트는 안 쌓였어요.” 이런 고객 문의, 받아보신 적 있으신가요? 처음엔 저도 트랜잭션 없이 코드 짰다가 새벽에 데이터 정합성 맞추느라 식은땀 뺐던 기억이 납니다.
트랜잭션이란? — 왜 알아야 하는가
트랜잭션은 여러 SQL 작업을 하나의 논리적 단위로 묶는 개념입니다.
은행 계좌이체를 생각해보세요. A계좌에서 출금하고 B계좌에 입금하는 두 작업은 반드시 함께 성공하거나 함께 실패해야 합니다.
만약 출금만 되고 입금이 실패하면? 돈이 증발하는 심각한 문제가 발생하죠.
이런 상황을 방지하기 위해 COMMIT(확정)과 ROLLBACK(취소)을 사용합니다.
실무에서는 주문 처리, 재고 관리, 회원 정보 수정 등 거의 모든 중요한 작업에 트랜잭션이 필요합니다.
특히 여러 테이블을 동시에 수정하는 경우, 트랜잭션 없이는 데이터 정합성을 보장할 수 없습니다.
기본 트랜잭션 사용법
가장 기본적인 트랜잭션 패턴부터 살펴보겠습니다. 주문과 재고를 함께 처리하는 실전 예제입니다.
-- 트랜잭션 시작
START TRANSACTION;
-- 또는 BEGIN; (MySQL/PostgreSQL)
-- 1. 주문 정보 저장
INSERT INTO orders (user_id, product_id, quantity, order_date)
VALUES (1001, 5023, 2, NOW());
-- 2. 재고 차감
UPDATE products
SET stock = stock - 2
WHERE product_id = 5023;
-- 3. 재고가 음수가 되지 않았는지 확인
SELECT stock FROM products WHERE product_id = 5023;
-- 모든 작업이 성공했다면 확정
COMMIT;
-- 문제가 발생했다면 모두 취소
-- ROLLBACK;
START TRANSACTION으로 트랜잭션을 시작하면, 이후 모든 SQL은 임시 상태로 실행됩니다.
COMMIT을 실행해야 비로소 데이터베이스에 실제로 반영되고, ROLLBACK을 실행하면 트랜잭션 시작 전 상태로 되돌아갑니다.
⚠️ 중요한 점은 트랜잭션 중간에 문제가 생기면 반드시 ROLLBACK을 해야 한다는 것입니다.
그렇지 않으면 다른 사용자들이 해당 데이터를 읽거나 수정할 때 락(Lock)이 걸려 대기 상태가 될 수 있습니다.
실전 패턴 — 조건부 ROLLBACK
실무에서는 특정 조건을 체크해서 COMMIT 또는 ROLLBACK을 결정해야 하는 경우가 많습니다.
-- 안전한 재고 차감 패턴
START TRANSACTION;
-- 현재 재고 확인
SELECT stock INTO @current_stock
FROM products
WHERE product_id = 5023
FOR UPDATE; -- 다른 트랜잭션이 동시에 수정 못하도록 락 걸기
-- 재고가 충분한지 검증
IF @current_stock >= 2 THEN
-- 주문 생성
INSERT INTO orders (user_id, product_id, quantity)
VALUES (1001, 5023, 2);
-- 재고 차감
UPDATE products
SET stock = stock - 2
WHERE product_id = 5023;
COMMIT;
SELECT '주문 성공' AS result;
ELSE
ROLLBACK;
SELECT '재고 부족' AS result;
END IF;
FOR UPDATE 구문이 핵심입니다. 이걸 사용하면 해당 행에 락을 걸어서 다른 트랜잭션이 동시에 수정하는 것을 방지합니다.
만약 두 명의 고객이 동시에 마지막 남은 상품 1개를 주문하면 어떻게 될까요?
FOR UPDATE 없이는 둘 다 재고가 있다고 판단해서 주문이 성공하고, 재고가 -1이 되는 문제가 발생합니다.
이런 동시성 문제는 테스트 환경에서는 잘 안 나타나지만, 실서비스에서 트래픽이 몰릴 때 터집니다.
자주 하는 실수와 해결법
현장에서 자주 발생하는 트랜잭션 관련 실수들을 정리했습니다.
- 잘못된 방법: 트랜잭션 안에서 시간이 오래 걸리는 외부 API 호출하기. 예를 들어 결제 API를 호출하는 동안 트랜잭션이 열려있으면, 해당 데이터에 락이 걸려 다른 사용자들이 대기하게 됩니다. 최악의 경우 데드락이 발생할 수 있습니다.
- 올바른 방법: 외부 API는 트랜잭션 밖에서 먼저 호출하고, 성공하면 트랜잭션을 시작해서 DB만 빠르게 업데이트합니다. 또는 트랜잭션을 최대한 짧게 유지하고, 보상 트랜잭션 패턴을 사용해 실패 시 롤백 처리합니다.
- 꿀팁: 애플리케이션 코드에서는 try-catch-finally 패턴을 사용하세요. try에서 SQL 실행, catch에서 ROLLBACK, finally에서 연결 종료. 이렇게 하면 예외가 발생해도 안전하게 롤백됩니다. 그리고 트랜잭션 격리 수준(Isolation Level)도 꼭 이해하세요. READ COMMITTED가 일반적으로 가장 안전합니다.
애플리케이션 코드에서의 트랜잭션
실무에서는 SQL을 직접 쓰기보다 프로그래밍 언어에서 트랜잭션을 다룹니다. Python 예제를 보겠습니다.
-- Python + MySQL 예제 (참고용)
import mysql.connector
conn = mysql.connector.connect(
host='localhost',
user='root',
password='password',
database='shop'
)
try:
cursor = conn.cursor()
# 트랜잭션 시작 (자동)
cursor.execute(
"INSERT INTO orders (user_id, product_id, quantity) "
"VALUES (%s, %s, %s)",
(1001, 5023, 2)
)
cursor.execute(
"UPDATE products SET stock = stock - %s "
"WHERE product_id = %s",
(2, 5023)
)
# 모든 작업 성공 시 확정
conn.commit()
print("주문 처리 완료")
except Exception as e:
# 오류 발생 시 롤백
conn.rollback()
print(f"오류 발생, 롤백: {e}")
finally:
# 반드시 연결 종료
cursor.close()
conn.close()
대부분의 프레임워크(Spring, Django 등)는 트랜잭션을 자동으로 관리해주는 데코레이터나 어노테이션을 제공합니다.
하지만 내부 동작 원리를 이해하지 못하면 예상치 못한 버그가 발생할 수 있습니다.
예를 들어 @Transactional 어노테이션을 private 메서드에 붙이면 작동하지 않는다는 것, 알고 계셨나요?
이런 세부사항은 프레임워크마다 다르므로 공식 문서를 꼭 확인하세요.
트랜잭션 격리 수준 이해하기
조금 더 깊이 들어가면 격리 수준(Isolation Level)을 알아야 합니다.
-- 현재 격리 수준 확인 (MySQL)
SELECT @@transaction_isolation;
-- 격리 수준 변경
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 또는 세션 전체에 적용
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 격리 수준별 특징
-- READ UNCOMMITTED: 커밋 안 된 데이터도 읽음 (Dirty Read 발생)
-- READ COMMITTED: 커밋된 데이터만 읽음 (일반적 권장)
-- REPEATABLE READ: 트랜잭션 내에서 같은 결과 보장 (MySQL 기본값)
-- SERIALIZABLE: 가장 엄격, 성능 저하 (거의 안 씀)
격리 수준이 높을수록 데이터 정합성은 좋아지지만 동시성 성능은 떨어집니다.
대부분의 경우 READ COMMITTED면 충분하고, 특별한 이유가 없다면 기본값을 사용하는 게 좋습니다.
⚠️ 격리 수준을 잘못 설정하면 “Phantom Read”나 “Non-repeatable Read” 같은 현상이 발생할 수 있습니다.
하지만 실무에서는 이런 이론보다는 “트랜잭션을 짧게 유지하라”는 원칙이 더 중요합니다.
퀴즈: 당신의 이해도를 체크해보세요
-- 다음 코드의 결과는?
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE user_id = 2;
-- 이 시점에서 정전이 발생했다면?
-- 데이터베이스에 반영될까요?
-- 정답: 반영되지 않습니다!
-- COMMIT을 하지 않았으므로 자동으로 ROLLBACK됩니다.
정리하며
트랜잭션은 데이터 정합성을 지키는 가장 기본적이면서도 강력한 도구입니다.
COMMIT과 ROLLBACK의 원리를 이해하고, 실무에서 안전하게 사용하는 패턴을 익히면 데이터 관련 버그를 크게 줄일 수 있습니다.
특히 동시성이 높은 서비스에서는 FOR UPDATE 같은 락 메커니즘과 격리 수준에 대한 이해가 필수입니다.
처음엔 복잡해 보이지만, 몇 번 실수해보고 나면 자연스럽게 몸에 배게 됩니다.
- 이 글의 핵심: 트랜잭션은 짧게 유지하고, 조건 체크 후 COMMIT/ROLLBACK을 명확히 하며, 동시성 문제는 FOR UPDATE로 해결합니다.
- 다음 단계: 인덱스 최적화, 쿼리 실행 계획 분석(EXPLAIN), 데드락 해결 방법을 공부해보세요.
참 쉽죠?