자, 너가 인터넷 쇼핑몰 앱을 만든다고 해보자. 결제할 때 이런 일이 필요해:
- 고객 카드에서 돈을 빼기.
- 창고에서 상품 수량 줄이기.
- 성공한 트랜잭션 기록 남기기.
근데 이 중간에 뭔가 꼬이면 어떡하지? 예를 들어, 돈은 이미 빠졌는데 상품이 다 팔려서 주문 기록을 못 남겼다? 완전 난리남: 돈은 "공중에 붕", 주문은 미완, 서버엔 화난 메일(아니면 소송장?)이 쏟아질 거야.
트랜잭션이 바로 이런 상황을 막아주는 거야. 여러 작업을 하나의 "원자적" 단위로 묶어서 처리할 수 있어. 마치 텍스트 에디터에서 "실행 취소" 버튼 누르는 것처럼, 뭔가 잘못되면 처음으로 되돌릴 수 있지.
트랜잭션이 데이터 무결성을 어떻게 보장할까?
트랜잭션은 ACID라는 개념에 기반해:
- 원자성 (Atomicity) — 트랜잭션 안의 모든 작업은 전부 다 성공하거나, 아니면 아무것도 안 한 거랑 똑같아. "올 아니면 전혀".
- 일관성 (Consistency) — 트랜잭션 전후로 데이터는 항상 일관된 상태여야 해.
- 격리성 (Isolation) — 한 트랜잭션이 다른 트랜잭션에 방해 안 줘.
- 지속성 (Durability) — 트랜잭션이 끝나면, 그 결과는 시스템이 꺼져도 남아있어.
왜 또 이 얘기하냐고? 이게 바로 우리가 지향하는 이상향이거든. 그리고... 현실에선 잘 안 돼. 나중에 트랜잭션 다시 배울 때 보면, ACID 원칙 중 몇 개는 포기해야 할 때도 있어.
그러니까 지금은 트랜잭션이 이렇게 쉽고 예쁜 시절을 즐겨! 자, 이제 예제로 넘어가자!
트랜잭션 사용 예시
학생 추가하고 강좌 등록하는 시나리오를 보자.
우리가 대학 데이터베이스를 다룬다고 해보자. 우리 강좌에 외부 청강생이 생겼어. 강좌에 자리가 있으면, 그 청강생을 (임시로) 학생으로 등록하고 강좌에 추가하는 거야. 이렇게 진행돼.
새 학생을 DB에 추가하고 강좌에 등록하려면:
students테이블에 레코드 추가.- 학생과 강좌를 연결하는
enrollments테이블에 레코드 생성.
근데 뭔가 꼬이면(예: 강좌가 이미 꽉 참), 작업을 롤백해서 테이블 간 데이터가 어긋나지 않게 해야 해. 이렇게 하는 거야:
-- 트랜잭션 시작
BEGIN;
-- 1단계: 학생 추가
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 예를 들어, id = 10이 반환됨
-- 2단계: 강좌 등록
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 5);
-- 다 성공했으면 변경사항 저장
COMMIT;
에러가 나면 어떻게 될까?
강좌 등록 중 에러가 났다고 해보자: 예를 들어, 강좌가 없음. 트랜잭션 없이 하면 students 테이블엔 학생이 남고, enrollments엔 없음. 데이터 무결성 깨짐. 이걸 막으려면 ROLLBACK을 써야 해.
-- 트랜잭션 시작
BEGIN;
-- 1단계: 학생 추가
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 2단계: 강좌 등록 시도
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 999); -- 에러: id = 999인 강좌 없음!
-- 모든 변경사항 롤백
ROLLBACK;
결과적으로 아무 작업도 반영 안 되고, DB는 트랜잭션 전 상태로 남아있어.
SAVEPOINT로 제어하기
이제 좀 더 복잡한 상황을 보자. 여러 작업을 하다가, 전체가 아니라 특정 지점까지만 롤백하고 싶을 때가 있어.
단계별 학생 등록을 구현해보자
-- 트랜잭션 시작
BEGIN;
-- 학생 추가
SAVEPOINT add_student; -- 저장 지점 생성
INSERT INTO students (name, age, gender)
VALUES ('Anna Song', 22, 'Female');
-- 첫 번째 강좌 등록
SAVEPOINT enroll_course_1; -- 또 다른 저장 지점
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 5);
-- 두 번째 강좌 등록(여기서 에러)
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 999); -- 에러!
-- 마지막 저장 지점까지만 롤백
ROLLBACK TO enroll_course_1;
-- 계속 진행
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 6);
-- 변경사항 저장
COMMIT;
이렇게 하면, 한 부분에서 에러가 나도 다른 부분 데이터는 살릴 수 있어.
변경사항 체크하기
SQL 쿼리가 뭔가를 바꿨는지 실제로 확인할 수 있어.
예를 들어 DELETE를 했는데 WHERE에 걸리는 게 없을 수도 있고, UPDATE를 했는데 이미 데이터가 바뀌어서 실제론 아무 변화가 없을 수도 있지.
이럴 때 쓰는 시스템 변수 FOUND가 있어. 마지막 SQL 쿼리에서 행이 바뀌었는지 알려줘:
FOUND = TRUE— 뭔가 업데이트/삭제됨;FOUND = FALSE— 아무것도 삭제/변경 안 됨.
일반 SELECT엔 안 되고, 변경사항 추적용이야.
실전 예시: 결제 처리
트랜잭션은 특히 금융 앱에서 빛을 발해. 다시 한 번, 한 계좌에서 다른 계좌로 돈을 보내는 시스템을 만들어보자.
-- 트랜잭션 시작
BEGIN;
-- 1단계: 첫 번째 계좌에서 돈 빼기
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- 2단계: 작업 성공했는지 확인(행이 바뀌었는지)
IF NOT FOUND THEN
ROLLBACK; -- 돈 부족하면 롤백
RAISE EXCEPTION '잔액이 부족해!'; -- 에러! 예외 던지기
END IF;
-- 3단계: 두 번째 계좌에 돈 넣기
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- 트랜잭션 저장
COMMIT;
여기서, 만약 고객이 가진 돈보다 더 많이 보내려고 하면 트랜잭션이 롤백되고, DB가 "붕 뜬" 상태가 안 돼.
특징 & 흔한 실수들
COMMIT 까먹음: 트랜잭션 끝에 COMMIT 안 하면 DB가 "기다리는 중"이 되고, 변경사항이 저장 안 돼.
WHERE 까먹음: 조건 없이 데이터 업데이트/삭제하면 진짜 큰일남. 예를 들어 DELETE FROM students에 WHERE 없으면 학생 전부 삭제됨.
트랜잭션 너무 오래 열어둠: 트랜잭션이 너무 오래 열려 있으면 데이터 접근이 막혀서 성능 문제 생겨. 항상 트랜잭션은 (COMMIT이나 ROLLBACK) 최대한 빨리 끝내!
트랜잭션은 데이터 무결성 지킬 때 진짜 든든한 친구야. 특히 사용자 등록, 결제 처리, 연관 테이블 업데이트 같은 복잡한 상황에서 데이터 꼬임을 막아줘. BEGIN, COMMIT, ROLLBACK, SAVEPOINT 명령어만 잘 익혀두면 훨씬 더 안전하고 믿을 수 있는 앱을 만들 수 있어.
GO TO FULL VERSION