주문을 파일에 한 줄씩 저장하는 작은 프로그램을 생각해 보겠습니다. 처음에는 저장과 조회가 단순합니다. 주문이 많아지고 여러 사용자가 동시에 수정하며 서버가 재시작해도 기록을 보존해야 하면, 파일에 쓰는 것 외의 문제가 커집니다. 데이터 시스템은 이 저장·조회·변경·복구의 규칙을 관리합니다.
레코드·키·관계
주문 하나를 주문 번호, 고객 번호, 금액, 상태 같은 필드로 나타낼 수 있습니다. 이 한 묶음이 레코드이며 관계형 DB에서는 보통 행으로 저장합니다. 어떤 주문인지 구별하려면 안정적인 키가 필요합니다. 금액이나 고객 이름만으로는 여러 주문 중 하나를 구별하지 못합니다.
고객의 현재 주소를 모든 주문 행에 복사하면 주소 변경 때 여러 행을 고쳐야 합니다. 고객 번호로 고객 테이블을 참조하면 현재 고객 정보를 한곳에서 관리할 수 있습니다. 다만 주문 당시 배송지는 나중 주소 변경과 무관하게 보존할 사실일 수 있습니다. 중복처럼 보여도 현재 정보와 역사적 사실은 구별해야 합니다.
전체 탐색과 인덱스
고객 42의 주문을 찾으려면 모든 주문 행을 읽고 고객 번호를 비교할 수 있습니다. 데이터가 많아지면 반환할 주문은 몇 개여도 검사할 행은 수백만 개일 수 있습니다. 인덱스는 특정 키에서 관련 행으로 찾아갈 수 있도록 별도의 탐색 구조를 유지합니다.
책의 색인이 본문과 별도로 공간을 차지하듯, DB 인덱스도 저장 공간과 갱신 비용을 사용합니다. 주문을 추가할 때 테이블뿐 아니라 관련 인덱스도 바꿔야 합니다. 따라서 인덱스를 많이 만드는 것이 모든 작업을 빠르게 만들지는 않습니다. 자주 쓰는 조회 조건과 정렬 순서를 기준으로 선택합니다.
동시 변경과 트랜잭션
상태를 시간순으로 적으면 A와 B가 모두 재고 1을 읽은 뒤 두 주문을 만들고 0을 저장하는 것이 lost update의 핵심입니다. 조건부 차감이 실제로 한 행만 성공시키고 실패한 요청이 주문을 확정하지 않는지 확인해야 합니다. 예상 결과는 재고가 0인 것뿐 아니라 성공 주문 수가 재고 변화와 일치하는 것입니다.
재고가 1일 때 두 요청이 동시에 1을 읽었다고 해 보겠습니다. 각 요청이 ‘1개 남았으니 판매 가능’이라고 판단하고 0을 저장하면 최종 재고는 0이지만 판매는 두 번 성공할 수 있습니다. 최종 숫자가 음수가 아니라는 사실만으로 올바른 처리가 아닙니다.
A: 재고 1 읽기 → 주문 생성 → 재고 0 저장
B: 재고 1 읽기 → 주문 생성 → 재고 0 저장
읽기와 판단과 쓰기 사이에 다른 요청이 끼어들기 때문입니다. DB는 조건부 갱신, 잠금, 격리 수준 같은 수단을 제공합니다. 예를 들어 ‘현재 재고가 1 이상인 경우에만 1을 차감’하는 갱신에서 실제 변경 행 수를 확인하고 주문 기록과 함께 확정할 수 있습니다.
트랜잭션은 관련 변경을 하나의 확정 단위로 묶습니다. 주문 저장은 성공했지만 재고 변경은 실패한 상태를 그대로 남기지 않도록 commit과 rollback을 사용합니다. 다만 트랜잭션이라는 이름만으로 모든 동시 실행이 직렬 실행과 같아지는 것은 아닙니다. 선택한 격리와 실제 SQL이 어떤 경쟁을 막는지 확인해야 합니다.
그림을 글로 읽기
- 트랜잭션 시작
- 재고 조건 확인·차감
- 주문 기록
- 함께 commit 또는 rollback
- 트랜잭션 시작 → 재고 조건 확인·차감: 실행
- 재고 조건 확인·차감 → 주문 기록: 조건 통과
- 주문 기록 → 함께 commit 또는 rollback: 확정
메모리 변경과 장애 후 보존
메모리의 값이 바뀐 시점과 전원이 꺼져도 복구할 수 있는 시점은 다릅니다. DB는 변경 로그와 데이터 페이지 등을 사용해 복구하며, 성공 응답을 어디까지의 저장 확인 뒤에 보낼지는 설정과 저장장치의 보장에 달려 있습니다.
복제본은 장애 때 다른 노드로 전환하거나 읽기를 나누는 데 도움됩니다. 하지만 비동기 복제라면 새 주문이 아직 복제본에 없을 수 있습니다. 잘못된 삭제도 복제될 수 있으므로 복제와 백업은 같은 기능이 아닙니다.
여러 시스템 사이의 실패
파일·DB·외부 API 중 어느 경계가 권위인지에 따라 선택이 달라집니다. 한 DB 안의 주문과 재고라면 transaction과 조건 갱신을 우선 검토하고, 알림처럼 별도 시스템으로 전달해야 하면 outbox와 재전달 가능한 event를 둡니다. 장애를 진단할 때는 DB commit, outbox 기록, broker 전달, consumer ledger, 외부 효과 ID를 순서대로 조회하며 “메시지가 없다”와 “효과가 아직 없다”를 구분합니다.
주문 DB를 commit한 뒤 알림 시스템에 메시지를 보내기 전에 서버가 죽으면 주문은 있지만 알림은 없습니다. 순서를 반대로 하면 주문 저장은 실패했는데 알림만 나갈 수 있습니다. 한 DB의 트랜잭션을 여러 외부 시스템까지 자동으로 넓힐 수는 없습니다.
그래서 전달할 의도를 주문과 함께 저장하는 아웃박스, 재전달을 식별하는 이벤트 ID, 같은 요청을 반복해도 효과를 중복시키지 않는 멱등성 등이 필요해집니다. 이것들은 처음부터 암기할 용어가 아니라, 저장 경계가 둘 이상이 되면서 생기는 틈을 메우는 방법입니다.
세부 학습 경로
관계와 정규화 → 인덱스 → 동시 갱신 → WAL과 복구 순서로 읽을 수 있습니다. 여러 시스템의 변경은 트랜잭셔널 아웃박스와 멱등성으로 이어집니다.