home
Real MySQL 8.0

[5장 트랜잭션과 잠금] 트랜잭션 주의사항

트랜잭션 범위를 최소화해야 하는 이유와, 외부 통신·불필요한 연산이 트랜잭션에 섞였을 때 생기는 문제를 정리한다.

트랜잭션도 DBMS 커넥션과 같은 원칙을 따른다. 꼭 필요한 최소한의 코드에만 적용하는 것이 좋다. 이는 프로그램 코드에서 트랜잭션의 범위를 최소화하라는 의미다. 트랜잭션이 열려 있는 동안에는 변경한 행에 잠금이 걸려 있고, 트랜잭션이 길게 이어질수록 그 잠금도 오래 유지된다. 그동안 같은 데이터를 건드리려는 다른 요청은 잠금이 풀리기를 기다려야 한다. 트랜잭션 범위를 좁히는 일이 곧 동시 처리 성능과 직결되는 이유다.

상품 구매 예제

이커머스에서 상품을 구매하는 흐름을 예로 든다. 사용자가 결제 버튼을 누르면 서버는 재고를 줄이고, 주문을 기록하고, 포인트를 적립한 뒤, 구매 완료 알림을 보낸다. 아래는 이 과정을 트랜잭션 하나로 넓게 묶은, 좋지 않은 예다.

트랜잭션 시작
1. 재고 확인 및 차감 (UPDATE)
2. 주문 레코드 생성 (INSERT)
3. 포인트 적립 (UPDATE)
4. 구매 완료 메일/푸시 발송   // 외부 통신
트랜잭션 커밋

이 절차도 동작은 한다. 하지만 트랜잭션이 1번부터 4번까지 전 구간에 걸쳐 열려 있고, 그 안에는 트랜잭션에 넣지 말아야 할 두 가지가 섞여 있다.

1. 외부 통신이 트랜잭션 안에 있는 경우

4번의 메일·푸시 발송은 외부 시스템에 요청을 보내고 응답을 기다리는 작업이다. 이 응답 시간은 애플리케이션이 통제할 수 없다. 상대 서버가 느리게 응답하거나 타임아웃까지 매달리면, 그동안 트랜잭션은 계속 열려 있고 1~2번에서 변경한 재고·주문 행의 잠금도 그대로 유지된다. DB 작업은 이미 다 끝났는데 메일 한 통 때문에 잠금이 몇 초씩 유지되고, 같은 상품을 사려는 다른 요청들이 그 잠금을 기다리며 줄줄이 밀린다.

되돌릴 수 없다는 문제도 있다. 트랜잭션은 실패하면 롤백으로 DB 변경을 원상 복구하지만, 이미 발송된 메일은 되돌리지 못한다. 4번에서 메일을 보낸 뒤 커밋이 실패하면 주문은 없는데 "구매 완료" 메일만 나간 상태가 된다.

2. 트랜잭션이 필요 없는 연산이 섞인 경우

트랜잭션은 여러 변경을 하나의 단위로 묶어 전부 반영하거나 전부 되돌리기 위한 장치다. 되돌릴 것이 없는 연산은 트랜잭션으로 묶을 이유가 없다. 1번의 재고 "확인"은 값을 읽기만 하는 조회이므로, 차감과 달리 트랜잭션을 시작하기 전에 먼저 끝낼 수 있다. 이런 조회까지 트랜잭션 안에 넣으면 얻는 것 없이 트랜잭션이 열려 있는 시간만 늘어난다.

범위를 좁힌 절차

같은 흐름을 트랜잭션 범위를 좁혀 다시 배치하면 다음과 같다.

1. 입력 검증, 재고 확인 (조회)   // 트랜잭션 밖
트랜잭션 시작
2. 재고 차감 (UPDATE)
3. 주문 레코드 생성 (INSERT)
4. 포인트 적립 (UPDATE)
트랜잭션 커밋
5. 구매 완료 메일/푸시 발송      // 트랜잭션 밖

DB를 실제로 변경하는 2~4번만 트랜잭션으로 묶었다. 되돌릴 수 없는 외부 통신(메일)은 커밋 뒤로 빼냈고, 조회는 트랜잭션을 시작하기 전에 처리했다. 트랜잭션이 열려 있는 구간이 DB 변경 세 건으로 줄면서, 그 세 건이 실행되는 짧은 시간에만 잠금이 유지된다.

이 배치가 유일한 정답은 아니다. 모범 사례가 곧 최적의 설계는 아니며, 구현하려는 업무의 특성에 따라 크게 달라진다. 예를 들어 결제 대행사(PG) 연동처럼 외부 결제가 끼면, 결제 승인도 외부 통신이므로 같은 원칙에 따라 트랜잭션 밖에서 처리한다. 다만 "결제는 승인됐는데 주문 저장 트랜잭션이 실패한" 경우를 어떻게 맞추느냐는 결제 상태와 DB 상태의 정합성 문제이고, 보상 트랜잭션 같은 별도 설계가 필요하다. 이는 트랜잭션 범위 최소화와는 다른 주제라 여기서는 다루지 않는다.

왜 트랜잭션을 짧게 유지하는가

트랜잭션이 열려 있는 동안에는 변경한 행에 잠금이 유지된다. 트랜잭션 범위가 넓을수록 이 잠금이 오래 유지되고, 잠금이 오래 유지될수록 같은 데이터를 기다리는 다른 요청이 많아진다. 트랜잭션 하나의 길이가 서버 전체의 동시 처리량을 좌우하는 것이다. 요청 하나의 코드 몇 줄이 문제가 아니라, 그 길이가 동시 요청 수만큼 누적되어 처리량을 깎는 것이 문제다. 트랜잭션이 점유하는 DB 커넥션 역시 트랜잭션이 길어지는 만큼 오래 반납되지 않는다.

네트워크 작업은 트랜잭션 밖으로

코드 라인 수로는 한두 줄이라도, 네트워크 작업이 들어간다면 반드시 트랜잭션에서 배제해야 한다. 외부 시스템의 응답 시간은 통제할 수도 예측할 수도 없기 때문이다. 상대 서버가 느려지거나 응답하지 않으면 그 지연이 고스란히 트랜잭션 유지 시간이 되고, 그동안 잠금은 풀리지 않는다.

이 상태에서 요청이 계속 들어오면 잠금을 기다리는 요청이 쌓이고, 트랜잭션마다 붙잡고 있는 커넥션도 반납되지 않아 함께 고갈된다. 결국 DBMS 서버는 높은 부하 상태로 빠진다. 외부 시스템 한 곳의 지연이 잠금과 커넥션을 매개로 DB 서버 전체의 장애로 전이되는 것이다. 그래서 네트워크 작업은 길이와 무관하게 트랜잭션 경계 밖으로 빼는 것을 원칙으로 삼는다.

댓글

아직 댓글이 없습니다.