이번 강의에서는 트랜잭션을 다루다가 자주 마주치는 실수들과, 그걸 어떻게 피할 수 있는지에 집중할 거야. 진짜야, 아무리 SQL 고수라도 COMMIT 까먹는 경우가 있거든! 트랜잭션에서 실수를 드물게 만드는 팁도 알려줄게.
불행히도(혹은 다행히도), 데이터베이스는 모든 게 완벽하게 돌아가는 마법의 성이 아니야. 트랜잭션 작업에서 실수하는 건 꽤 흔한 일이야, 특히 SQL을 막 배우는 사람들한테는 더더욱. 하나씩 자세히 보자.
COMMIT 또는 ROLLBACK 까먹기
트랜잭션을 끝내는 걸 까먹는 건 진짜 "고전적인 실수"야. 식당에서 음식 주문했는데, 서버가 계산서를 안 가져다주는 상황을 생각해봐. PostgreSQL 세계에선 이게 트랜잭션이 "멈춰있는" 상태고, DB가 리소스를 계속 잡고 있으면서 다른 작업도 막아버리는 거지.
실수 예시:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 이런! COMMIT이나 ROLLBACK을 추가하는 걸 까먹었어.
트랜잭션이 "멈춰" 있으면, 리소스 락이 테이블 전체로 퍼질 수도 있어. DB 관리자가 상황이 심각하다고 판단하면, 강제로 트랜잭션을 종료시킬 수도 있지. 근데 그 전에 미리 처리하는 게 훨씬 낫겠지.
어떻게 피할까?
- 항상 트랜잭션을 명시적으로 끝내:
COMMIT또는ROLLBACK! - 트랜잭션이 멈춰있을 때 자동으로 알려주는 클라이언트 툴을 써봐.
- 트랜잭션이 끝나지 않은 상태에서 앱을 재시작하면, DB가 자동으로
ROLLBACK을 해주긴 하는데, 시스템 상태에 따라 불편할 수도 있어.
잘못된 isolation level 사용하기
isolation level 고르는 게 그냥 형식적인 거라고 생각할 수 있는데, 사실 이게 이상한 현상을 막는 데 핵심 역할을 해. 예를 들어, 중요한 금융 작업에 READ UNCOMMITTED를 쓰면, 나중에 취소될 수도 있는 "더러운" 데이터를 읽게 될 수도 있어.
예시:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 다른 트랜잭션이 바꾸고 있는 데이터를 읽어
SELECT balance FROM accounts WHERE account_id = 1;
-- 다른 트랜잭션이 ROLLBACK 하면, 네 데이터는 무효가 돼.
어떻게 피할까?
- 네 앱에서 데이터가 얼마나 중요한지 먼저 생각해봐.
- 대부분의 경우엔 "더러운" 읽기를 막으려면
READ COMMITTED를 써. - 변경이나 phantom data를 진짜 막아야 할 땐
REPEATABLE READ나SERIALIZABLE같은 더 엄격한 isolation level을 써.
트랜잭션 충돌과 락
가끔 두 개 이상의 트랜잭션이 같은 데이터를 바꾸려고 할 때가 있어. 이럴 땐 PostgreSQL이 한쪽 트랜잭션을 다른 쪽이 끝날 때까지 기다리게 해. 이게 deadlock(교착 상태)으로 이어질 수도 있지.
실수 예시:
-- 첫 번째 트랜잭션
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 두 번째 트랜잭션
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- 첫 번째 트랜잭션을 기다리는 중...
두 트랜잭션이 서로 필요한 리소스를 잡고 있으면, 교착 상태가 생겨. PostgreSQL이 이걸 감지하면, 한쪽 트랜잭션을 에러와 함께 종료시키고 이런 메시지를 보여줄 거야:
ERROR: deadlock detected
어떻게 피할까?
- 트랜잭션 안에서 작업 순서를 항상 일정하게 유지해.
- 트랜잭션 실행 시간을 최대한 줄여서 락 걸릴 확률을 낮춰.
SERIALIZABLEisolation level은 진짜 필요할 때만 써.
SAVEPOINT 실수
SAVEPOINT는 부분적으로 롤백할 수 있는 멋진 도구지만, 잘못 쓰면 헷갈릴 수 있어. 예를 들어, 저장 지점(RELEASE SAVEPOINT)을 해제하는 걸 까먹으면, 불필요한 락이나 에러가 생길 수 있지.
실수 예시:
BEGIN;
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT my_savepoint;
-- SAVEPOINT 해제하는 걸 까먹었어!
어떻게 피할까?
- 더 이상 필요 없는
SAVEPOINT는 꼭 지워줘. - 저장 지점을 너무 많이 만들면 쿼리가 복잡해지니까, 적당히 써.
트랜잭션과 외부 시스템의 호환성 문제
PostgreSQL 트랜잭션이 외부 시스템이랑 상호작용하려고 한다고 생각해봐: 알림 보내기, API 업데이트 등등. 외부 시스템에서 문제가 생기면, 변경 사항을 롤백하기가 진짜 힘들어져.
예시:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 알림을 보낼 수 없음: email-server가 응답하지 않아.
COMMIT; -- 변경 사항은 저장됐지만, 알림은 안 갔어.
어떻게 피할까?
- 가능하면 외부 시스템과의 상호작용은 따로 분리해.
- 외부 시스템과의 작업을 조율하려면 중간 테이블이나 작업 큐를 써봐.
큰 트랜잭션에서 생기는 실수
수많은 작업이 들어간 큰 트랜잭션은 락, 타임아웃, deadlock 같은 문제에 더 취약해.
예시:
BEGIN;
-- 수천 개의 업데이트 작업
UPDATE orders SET status = '완료됨' WHERE delivery_date < CURRENT_DATE;
COMMIT; -- 꽤 오래 걸릴 수도 있어.
어떻게 피할까?
- 큰 트랜잭션은 여러 개의 작은 트랜잭션으로 나눠.
- 데이터 업데이트는 batch로 처리해.
- 한 트랜잭션에서 바꾸는 데이터 양을 최소화해.
에러 체크를 까먹는 실수
트랜잭션 안의 모든 SQL 쿼리가 다 성공하는 건 아니야. 예를 들어, 작업 중 하나에서 에러가 나면 트랜잭션 전체가 실패 상태가 돼.
예시:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = -1; -- 에러: account_id가 없어.
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- 에러 때문에 실행 안 됨.
어떻게 피할까?
- 각 작업의 실행 결과를 항상 체크해.
- 쿼리나 클라이언트 코드에서 에러 처리를 꼭 해줘.
ROLLBACK 동작을 잘못 이해하는 실수
많은 개발자들이 ROLLBACK이 모든 변경을 취소하고 원래 상태로 돌려준다고 생각해. 하지만 ROLLBACK은 현재 트랜잭션 안에서만 동작해.
오해 예시:
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK; -- 에러! 트랜잭션 안에서 실행된 게 아니라서 안 돼.
어떻게 피할까?
- 기억해:
BEGIN이 있어야ROLLBACK이 힘을 발휘해. - 중요한 작업은 항상 트랜잭션으로 감싸.
GO TO FULL VERSION