트랜잭션
Transaction
개념
여러 번의 데이터 변경을 한 덩어리로 묶어, 전부 확정되거나 전부 없던 일이 되게 만드는 장치입니다.
등장 배경
한 가지 일처럼 보이는 작업이 데이터에 닿을 때는 여러 번으로 쪼개집니다. 요금제를 올리는 일은 결제를 기록하는 쓰기 한 번과 플랜 값을 바꾸는 쓰기 한 번입니다. 사람 눈에는 버튼 하나를 누른 한 동작인데 저장소 쪽에서는 두 사건입니다.
그 사이에 문제가 생기면 어중간한 상태가 남습니다. 페이지덱에서 실제로 그랬습니다. 결제 대행사에서 승인은 떨어졌는데 다음 줄에서 오류가 나 플랜이 무료로 남은 계정이 한 달에 열넷 생겼습니다. 고객은 돈을 냈고 제품은 기능을 열어 주지 않았습니다. 고객센터가 목록을 받아 하나씩 고쳤고, 그 과정에서 이미 처리된 결제를 헷갈려 두 번 열어 준 계정도 나왔습니다.
코드로 순서를 잘 쓰는 것으로는 이 문제가 풀리지 않습니다. 첫 줄과 둘째 줄 사이에서 서버가 꺼지는 경우를 코드는 다룰 수 없기 때문입니다. 트랜잭션은 그 틈을 데이터베이스 쪽에서 막습니다. 묶어 둔 작업 전부가 확정되거나 하나도 남지 않는 두 가지 결말만 가능하게 만듭니다. 데이터를 저장하고 꺼내 쓰게 해 주는 데이터베이스 아래에서 값이 어중간하게 남지 않도록 지키는 일관성 갈래에 속합니다.
작동 방식
명령은 셋입니다. 시작하고, 확정하고, 되돌립니다.
- 시작을 선언합니다. 이 뒤의 변경은 곧바로 반영되지 않고 임시로 쌓입니다. 다른 사람이 같은 데이터를 읽어도 아직 바뀌기 전 값이 보입니다.
- 확정합니다. 쌓아 둔 변경이 한꺼번에 반영되고, 그 순간부터 다른 사람에게도 보입니다. 이 시점 이후에는 되돌릴 수 없습니다.
- 되돌립니다. 프로그램이 오류를 만나 명시적으로 부르기도 하고, 연결이 끊기거나 서버가 꺼지면 데이터베이스가 알아서 되돌립니다. 이 자동 되돌림이 트랜잭션의 실제 값어치입니다.
이 묶음이 지켜야 할 네 가지 성질에 붙은 이름이 ACID입니다. 전부 아니면 전무, 규칙을 어기는 상태로는 끝나지 않을 것, 동시에 돌아가는 다른 묶음과 섞이지 않을 것, 확정한 뒤에는 전원이 꺼져도 남을 것 넷입니다.
동시성이 여기서 문제가 됩니다. 두 사람이 같은 행을 동시에 고치려 하면 데이터베이스가 한쪽을 기다리게 만드는데, 이것을 잠금이라 부릅니다. 잠금은 짧을수록 좋습니다. 두 트랜잭션이 서로가 쥔 것을 기다리면 둘 다 영원히 멈추는 교착이 생기고, 그때는 데이터베이스가 한쪽을 강제로 되돌려 풉니다.
그래서 경계를 어디에 두느냐가 실제 설계입니다. 원칙은 짧게 유지하는 것입니다. 트랜잭션 안에서 결제 대행사의 응답을 기다리면 그 몇 초 동안 잠금이 유지되고, 대행사가 느린 날에는 같은 데이터를 건드리는 모든 요청이 함께 밀립니다. 외부에 말을 거는 일은 트랜잭션 밖으로 빼고, 안에는 우리 저장소에 쓰는 일만 둡니다.
여러 서비스로 나뉜 구조에서는 하나의 트랜잭션으로 묶을 수 없습니다. 결제 서비스와 계정 서비스가 각자 자기 데이터베이스를 가지고 있으면 한쪽이 실패했을 때 다른 쪽을 되돌릴 방법이 없습니다. 그때는 실패를 뒤늦게 알고 반대 동작을 수행하는 보상 절차를 따로 만듭니다. 결제를 취소하는 요청을 보내는 식인데, 이 요청은 여러 번 보내도 결과가 같은 멱등성을 갖춰야 합니다.
쓰이는 곳
돈과 재고와 권한을 다루는 자리에는 거의 예외 없이 들어갑니다. 값이 어중간하게 남으면 사람이 손으로 고쳐야 하고, 그 과정에서 또 틀립니다.
페이지덱에서 트랜잭션으로 묶인 자리는 셋입니다. 결제 기록과 플랜 변경을 묶은 요금제 업그레이드, 문서를 다른 팀으로 옮기는 동작, 그리고 팀원 초대입니다. 문서 이동은 새 위치에 만들고 옛 위치에서 지우는 두 쓰기라, 묶지 않으면 문서가 두 곳에 있거나 어디에도 없는 상태가 나옵니다. 초대는 계정을 만들고 권한을 붙이는 두 쓰기이고, 묶지 않으면 아무 권한도 없는 계정이 남아 로그인은 되는데 아무것도 보이지 않습니다.
반대로 넣지 않는 자리도 있습니다. 읽기만 하는 조회, 그리고 실패해도 다시 남기면 되는 기록성 쓰기입니다. 접속 기록을 트랜잭션으로 묶으면 본래 작업의 잠금 시간만 길어집니다.
표 구조에 매이지 않는 NoSQL 계열을 쓸 때는 지원 범위를 먼저 확인해야 합니다. 기록 하나 안에서는 전부 아니면 전무를 보장하지만 여러 기록에 걸치면 보장하지 않는 제품이 있습니다. 표와 관계로 저장하는 관계형 데이터베이스에서는 대개 기본으로 갖춰져 있습니다.
실무 용례
"이 두 쓰기는 한 트랜잭션으로 묶어야 해요. 사이에서 죽으면 결제만 남습니다."
작업 묶음의 경계를 정하는 말입니다.
"트랜잭션 안에서 외부 API 부르지 마세요. 잠금이 3초씩 잡혀요."
경계를 너무 넓게 잡았다는 지적입니다.
"교착이 하루에 두 번씩 나는데 잠금 순서를 맞춰야 할 것 같아요."
두 묶음이 서로를 기다리는 상태를 손보자는 제안입니다.
페이지덱에서 이 대화는 태오가 먼저 꺼내고 수아가 결과를 듣는 쪽입니다. 어중간한 상태가 남으면 그 뒷정리를 고객 응대 쪽이 하기 때문입니다. 열넷을 손으로 고친 뒤로는 결제 코드를 고칠 때마다 경계를 어디에 뒀는지 확인하는 절차가 생겼습니다.
확인하는 방법은 문서를 읽는 것이 아닙니다. 가운데에서 일부러 오류를 던져 보고 데이터가 어떤 상태로 남는지 봅니다.
흔한 오해
코드에서 순서대로 쓰면 되니 따로 쓸 필요가 없다고 보는 경우가 가장 많습니다. 두 줄을 나란히 써 두면 둘 다 실행되는 것이 당연해 보이기 때문입니다. 하지만 두 줄 사이에는 서버가 꺼질 수 있는 틈이 있고, 그 틈은 코드로 메울 수 없습니다. 게다가 이 문제는 평소에 드러나지 않습니다. 하루에 만 번 돌아가는 코드에서 그 틈에 걸리는 경우가 한 달에 열 몇 번이라, 없는 문제처럼 보이다가 사용자가 늘면 비례해서 늘어납니다.
트랜잭션으로 감싸 두면 안전해진다고 여기는 것도 흔합니다. 넓게 감쌀수록 더 안전해 보이는 착각이 여기서 나옵니다. 실제로는 경계가 넓어지면 잠금이 길어지고, 잠금이 길어지면 같은 데이터를 쓰는 다른 요청이 줄을 섭니다. 요청 하나를 안전하게 만드는 대신 전체가 느려지는 거래입니다. 심하면 교착이 일상적으로 나기 시작합니다. 그래서 감싸는 범위는 넓히는 방향이 아니라 좁히는 방향으로 손봅니다. 한 묶음 안에 우리 저장소에 쓰는 일만 남기고 나머지는 전부 밖으로 뺍니다.
같이 보면 좋은 페이지
마지막 업데이트 2026-09-16