2. 핵심 원리
2.1 업무 번호의 구조
주문번호를 세 부분으로 나눠 생각하면 쉽습니다.
| 부분 | 예 | 만드는 방법 |
|---|---|---|
| 접두어 | ORD- | 고정 문자열 |
| 날짜 | 20260930 | 날짜를 8자리 문자로 변환 |
| 일련번호 | 0001 | 숫자를 4자리로 0 채움 |
앞의 두 부분은 값이 정해져 있어서 어렵지 않습니다. 문제는 일련번호입니다. 그날 지금까지 몇 번까지 나갔는지를 어디에 기록하고, 여러 요청이 동시에 와도 각자 다른 번호를 받게 만드는 것이 이 레슨의 주제입니다. 방법은 크게 세 가지입니다.
| 방식 | 번호를 어디서 얻나 |
|---|---|
| MAX + 1 | 주문 표에서 그날 최댓값을 읽어 1 을 더함 |
| 채번 테이블 | 별도 표의 카운터를 UPDATE 로 올림 |
| 시퀀스 | DB 가 관리하는 숫자 발급기 |
2.2 MAX + 1 이 깨지는 이유
MAX+1 은 읽고 나서 쓰는 두 단계입니다. 두 세션이 각각 MAX 를 읽으면 둘 다 같은 값을 보고, 같은 번호를 만들어 INSERT 합니다. 주문번호에 PK 나 UNIQUE 가 있으면 두 번째 INSERT 가 오류로 막힙니다. 제약이 없으면 같은 번호의 주문이 두 건 생깁니다.
그래서 MAX+1 을 쓸 때는 다음 두 가지를 함께 둡니다.
- 번호 컬럼에 PK 나 UNIQUE 를 둔다.
- 중복 오류가 나면 번호를 다시 구해 INSERT 를 재시도한다.
트래픽이 낮은 내부 시스템이라면 이것으로 충분한 경우도 많습니다. 동시 요청이 잦으면 재시도가 늘어 곤란해집니다.
핵심MAX+1 은 읽기와 쓰기 사이에 틈이 있어서 동시 요청에 취약합니다. 최소한 PK 로 막고 재시도하거나, 채번 테이블로 바꿉니다.
2.3 채번 테이블과 UPDATE 먼저
채번 테이블은 업무 구분(biz)과 날짜(ymd)마다 행 하나를 두고, 그 행의 last_no 가 그날 마지막으로 발급한 번호입니다. 새 번호가 필요하면 그 행을 last_no = last_no + 1 로 UPDATE 하고 올라간 값을 읽습니다.
핵심은 순서입니다. UPDATE 는 실행하는 순간 그 행에 쓰기 잠금을 겁니다. 다른 세션이 같은 행을 UPDATE 하려 하면 첫 세션이 COMMIT 이나 ROLLBACK 을 할 때까지 기다립니다. 그래서 동시 요청이 줄을 서고, 각자 서로 다른 값을 받습니다. 읽기를 먼저 하지 않고 UPDATE 를 먼저 하는 것이 MAX+1 과의 차이입니다.
| 행 | biz | ymd | last_no |
|---|---|---|---|
| 1 | ORD | 20260929 | 2 |
| 2 | ORD | 20260930 | 0 |
그날 첫 요청이면 행이 아직 없습니다. 행이 없을 때만 INSERT 하고 있으면 건드리지 않는 처리가 필요하고, Oracle·MSSQL 은 MERGE, MySQL 은 INSERT IGNORE 를 씁니다. 두 세션이 동시에 행을 만들려 하면 한쪽이 PK 오류를 받으므로 그때는 UPDATE 부터 다시 시도합니다.
2.4 SELECT ... FOR UPDATE 방식
UPDATE 대신 먼저 행을 읽으면서 잠그는 방식도 있습니다. SELECT last_no ... FOR UPDATE 로 잠근 뒤 값에 1 을 더해 UPDATE 합니다. 순서가 정해지는 결과는 같지만 문장이 두 개로 늘어납니다.
| DB | 읽으면서 잠그기 | 한 문장으로 |
|---|---|---|
| Oracle · Tibero | SELECT ... FOR UPDATE | UPDATE ... RETURNING last_no INTO :n |
| MySQL InnoDB | SELECT ... FOR UPDATE | LAST_INSERT_ID(last_no + 1) 로 UPDATE 후 조회 |
| MSSQL | FROM 뒤 WITH (UPDLOCK, HOLDLOCK) 힌트 | UPDATE ... OUTPUT inserted.last_no |
MSSQL 에는 FOR UPDATE 가 없습니다. 힌트는 BEGIN TRAN 안에서만 의미가 있고, 한 문장으로 끝내려면 OUTPUT 절이 더 간단합니다. Oracle 의 RETURNING INTO 와 MSSQL 의 OUTPUT 은 UPDATE 와 값 읽기 사이의 틈을 없애 줍니다.
2.5 시퀀스와 비교
시퀀스는 트랜잭션과 무관하게 빠르고 잠금을 잡지 않아 동시성에 가장 강합니다. 대신 날짜별로 1 부터 다시 시작하기 어렵고, 롤백해도 되돌아가지 않아 번호에 구멍이 생깁니다. 그래서 "날짜별로 1 부터, 가능하면 빈 번호 없이" 가 요구인 업무 번호에는 채번 테이블이 자주 쓰이고, 번호가 그냥 유일하기만 하면 되는 내부 키에는 시퀀스가 어울립니다.
| 항목 | MAX + 1 | 채번 테이블 | 시퀀스 |
|---|---|---|---|
| 동시 요청 | 충돌, 재시도 필요 | 잠금으로 줄 섬 | 충돌 없음 |
| 일자별 1 부터 | 자연스러움 | 행을 날짜별로 둠 | 어려움 |
| 번호 구멍 | 마지막 행 삭제 시 번호 재사용 | 분리 커밋 시 발생 | 롤백해도 발생 |
| 병목 | 없음(재시도만) | 채번 행 하나에 집중 | 거의 없음 |