같은 요청을 가드 없이 한 번, 가드를 걸고 한 번 보냅니다.
달라지는 것은 데이터베이스 제약 하나뿐입니다.
데이터베이스 가드
시나리오
발사
배정일자
—
—
이 모드로 아직 발사하지 않았습니다
위 발사 버튼을 누르면 이 서버가 실제로 동시 요청을 만들어 배정을 시도하고,
그 결과를 데이터베이스에서 다시 세어 아래에 표시합니다.
위원 격자 · 당일 배정 카운터
—
근거
—
배정 규칙 3단 폴백
한 건을 실제로 보내고, 서버가 어떤 순서로 후보를 시도했는지 그대로 받습니다.
—
동일 지역에 후보가 여럿이면 당일 배정 수가 적은 위원을 먼저 시도하고,
같으면 위원 ID 오름차순입니다. 공고가 말한 기본안 그대로이며 착수 전 협의로 확정할 항목입니다.
다른 기준이 되면 후보 정렬 비교자만 바뀝니다.
배정일자 — ·
리셋 배치가 없습니다. 슬롯 키에 배정일자가 들어 있어 날짜가 바뀌면 그날의 슬롯이 통째로 비어 있습니다.
일일 한도 카운터 — 전일 / 당일
—
전일 열은 GET /api/reviewers?date=<전일> 로 그 날짜를
다시 조회한 값입니다. 화면이 기억하고 있는 값이 아니라 데이터베이스에 그대로 남아 있는 값입니다.
위원
지역
전일
당일
이 날짜와 전일 모두 배정 0
[배정 1건 보내기]를 누르면 당일 열이 올라가고, [다음 날로 넘기기]를 누르면
당일 열이 0으로 돌아가면서 방금 값이 전일 열로 옮겨 갑니다.
배정 경로
아직 보낸 요청이 없습니다
지역을 고르고 [배정 1건 보내기]를 누르면 단계가 여기에 쌓입니다.
이 날짜의 대기 건
시·도 인접 관계표
—
인접 관계는 규칙이 아니라 데이터입니다. 확정 주체는 도입 조직이며 이 표는 착수 1일차 승인 대상 초안입니다.
시·도
코드
차수
인접 시·도
위원
일 용량
피크 수요
경계 사례
서술은 우리 판단이고, 관측 열의 값은 지금 적재된 데이터에서 그 자리에서 센 것입니다.
#
항목
관측
왜 문제인가
동시성 실험 기록
화면 1에서 발사한 실험이 여기에 쌓입니다(검증 스크립트가 만든 행도 같은 표에 남습니다).
초과 건수는 애플리케이션이 실행 중에 세어 보고한 값이 아니라, 실험이 끝난 뒤 배정 테이블을 위원별로
다시 세어 한도를 넘긴 만큼을 더한 값입니다 — 위원 수가 아니라 행 수입니다.
SELECT SUM(c - 한도) FROM (SELECT COUNT(*) c FROM 배정테이블
WHERE 배정일자 = ? GROUP BY member_id) t WHERE c > 한도.
묶어 세는 것은 데이터베이스가 하고 한도를 빼는 산술은 애플리케이션이 합니다.
아직 실험 기록이 없습니다
화면 1에서 가드를 끄고 한 번, 켜고 한 번 발사하면 대조표가 만들어집니다.
실험 대조표
#
시나리오
가드
요청
동시
동일지역
인접우회
대기
초과
1인 최대
중복키 충돌
p50
p95
p99
최대
전체
배정일자
—
지역별 대기 비대칭
근거
—
엔드포인트 · 멱등 · 에러
API 명세와 멱등키 규약
아래 표는 이 서버가 서빙하는 API 경로(/api/**) 전부입니다.
(같은 서버가 이 화면 파일도 정적으로 함께 서빙합니다 — API 가 아니라 표에서 뺐습니다.)
화면이 실제로 부르는 경로와 명세·검증용으로만 두는 경로가 섞여 있어서
화면 호출 열로 갈라 적었습니다. '관련 요구'는 충족 표시가 아니라 이 경로가 닿는
요구 ID입니다 — 충족 여부는 아래 [이 콘솔에 없는 것]을 함께 보십시오.
엔드포인트
메서드
경로
용도
화면 호출
관련 요구
POST
/api/assignments
신청 1건 배정 요청. 3단 폴백 결과와 시도 경로를 함께 반환
화면 2
R7·R14·R19
GET
/api/assignments/{requestId}
멱등키로 결과 재조회. 없으면 404 ASSIGNMENT_NOT_FOUND
화면 미사용 (명세·검증용)
R34
GET
/api/reviewers?date=&guarded=
위원별 당일 배정 수 실시간 조회(전체)
화면 2 전일 카운터
R8
GET
/api/reviewers/{id}/daily-count?date=
위원 1명의 당일 배정 수·한도
화면 미사용 (명세·검증용)
R8
GET
/api/regions
시·도 · 인접 관계 · 위원 수 · 일 용량
화면 2·3
R11
GET
/api/meta
시드 요약·데이터 출처·경계 사례·동시성 상한. 화면의 시드·실험 수치(위원 수·한도·시·도 수·
인접 쌍·피크 수요·동시 스레드 수)는 전부 여기서 파생된다 — 마크업에 박아 두지 않는다
화면 1~4
R11·R44
POST
/api/lab/runs
동시성 시나리오 실행(피크 재현·단일 위원 집중)
화면 1
R13
GET
/api/lab/runs
실험 결과 목록
화면 3
R13·R24
GET
/api/lab/state?guarded=
선택한 가드 모드의 마지막 실험 + 그 날짜의 위원별 카운터. 화면 1 격자가 이걸 그린다
화면 1
R8·R13
GET
/api/trace/state
규칙 추적 lane 의 배정일자·위원별 카운터·대기 목록
화면 2
R4·R6
POST
/api/trace/next-day
배정일자를 하루 넘긴다(이 콘솔 전용 — 실제 운영에는 없는 조작)
화면 2
R6
공통 인증 헤더 규약 (명세)
위 경로 전부에 같은 규약이 걸립니다. 이 서버는 이 헤더를 검증하지
않습니다 — 누구나 여는 링크라 인증을 걸면 화면이 아무것도 보여 주지 못하기 때문입니다.
아래는 실제 구축에서 적용할 규약이고, 키 발급·회수 화면은 만들지 않았습니다.
인증 수단은 착수 첫날 확인 항목입니다(내부망 IP 제한 · API Key · JWT · mTLS).
아래는 API Key 를 골랐을 때의 규약입니다.
키 — 호출자가 보내는 requestId가 곧 멱등키입니다.
배정 테이블에 UNIQUE (request_id)가 걸려 있어 같은 키의 두 번째 INSERT는 데이터베이스가 거부합니다.
재요청 — 같은 키로 다시 부르면 새 배정을 만들지 않고 저장된 결과를 그대로 돌려주며
idempotent: true가 붙습니다. 대기로 전환된 건도 같습니다.
왜 필요한가 — 기존 사이트의 호출 코드 수정이 범위 밖이라 호출자가 타임아웃 뒤 재호출하면
막을 수 있는 지점이 이 API 안쪽뿐입니다. 화면 2의 [같은 요청 다시 보내기]가 이 경로를 그대로 탑니다.
보관 — 이 서버는 배정 이력을 지우지 않습니다. 실제 구축에서는 보관 기간을 합의해 정합니다.
에러 코드 대응표
데이터베이스 종류가 확정되지 않았으므로 제품별 코드를 프레임워크 계층에서 하나로 받습니다.
이 서버가 실제로 잡는 것은 PostgreSQL 23505이고, 나머지 데이터베이스 행은 제품이 바뀌었을 때 같은 처리로
흡수되는 경로입니다. 맨 위 401 행만 성격이 다릅니다 — 데이터베이스 오류가 아니라 위 인증 규약의 응답이고,
이 서버에는 구현하지 않았습니다.
상황
제품별 코드
수신 형태
이 API 의 처리
인증 헤더 없음·불일치 X-API-Key
해당 없음 — 데이터베이스 오류가 아닙니다
인증 필터
401 UNAUTHORIZED · 본문 없음.
규약만 두었고 이 서버는 검증하지 않습니다
중복 키
PG 23505 · MySQL 1062
DuplicateKeyException
슬롯 충돌이면 다음 슬롯으로. 멱등키 충돌이면 저장된 결과 반환. 재시도 아님
직렬화 실패
PG 40001 · Oracle ORA-08177
CannotSerializeTransaction
트랜잭션 전체 재실행. 재시도 프록시는 트랜잭션 경계 바깥에 둔다
데드락
PG 40P01 · MySQL 1213 · Oracle ORA-00060 · SQL Server 1205
DeadlockLoserDataAccessException
트랜잭션 전체가 롤백된다. 재실행
락 대기 타임아웃
MySQL 1205 · PG 55P03
CannotAcquireLockException
대기한 그 문장만 롤백된다. 롤백 범위가 위와 달라 처리도 달라야 한다
커넥션 고갈
PG 53300 · HikariCP 타임아웃
CannotGetJdbcConnectionException
503 반환. 풀 확대가 아니라 트랜잭션 길이를 먼저 본다
MySQL 1213과 1205의 롤백 범위가 다르다는 점이 이 표의 핵심입니다.
이걸 모르고 같은 재시도 로직으로 묶으면 반쯤 커밋된 상태가 남습니다.
착수 첫날 확인 체크리스트
DBMS 제품·메이저 버전·문자셋 (국산 제품 Tibero·Altibase·CUBRID 여부 포함)
실효 격리수준 — 기본값이 제품마다 다르다 (InnoDB 는 REPEATABLE READ, 나머지 셋은 READ COMMITTED)
신청·기업·위원 테이블의 실제 이름·PK/FK·인덱스 DDL
기업 '지역' 값의 저장 형식 — 시·도 코드인가 자유입력 주소 문자열인가
지역 코드가 어느 시점 기준인가 (경계 사례 8번 — 최근 3년 안에 실제로 바뀌었다)
기존 신청 상태 코드 체계와 '대기'를 기존 화면이 어떤 조건으로 조회하는지
일일 한도 값과 위원별 차등 여부, 리셋 경계(자정 / 영업일)
인증 수단 — 내부망 IP 제한 · API Key · JWT · mTLS 중 가능한 것
이 콘솔에 없는 것
이 서버
—
이 콘솔에서 동작하지 않습니다
실제 구축 시
뺀 이유
다루는 곳
1분 확인 순서
무엇을 보면 되나
이 콘솔이 증명하려는 것은 하나입니다.
같은 요청을 같은 규칙으로 처리하는데 데이터베이스 제약 하나가 있고 없고에 따라
한도 초과 배정이 나느냐 안 나느냐가 갈린다는 것.
1화면 1 배정 시뮬레이터에서 시나리오를
[단일 위원 집중]으로 두고, 가드를 끔으로 바꾼 뒤 발사 버튼을 누릅니다.
초과 카운터가 0이 아닌 수로 올라갑니다.
2같은 화면·같은 시나리오에서 가드만 켬으로 바꾸고 다시 발사합니다.
같은 요청 —건인데 성공은 정확히 —건(= 한도),
초과는 0이 됩니다.
피크 재현 시나리오는 요청 수가 —건이라 성공 수가 다릅니다.
3화면 2 배정 규칙에서 지역을 골라 [배정 1건 보내기]를 반복해
동일 지역 → 인접 지역 → 대기로 넘어가는 단계를 봅니다. 그다음 [다음 날로 넘기기]를 누르면
[일일 한도 카운터] 표의 당일 열이 0으로 돌아가고 방금 값이 전일 열에 남습니다.
리셋 배치를 돌리지 않았습니다.
4화면 3 동시성 실험 기록에서 방금 두 실험의 대조표와
응답 백분위를 봅니다.
5화면 4 API 명세에서 '미지원' 표시를 눌러
무엇을 뺐고 어디서 다루는지 네 칸으로 확인합니다.
가드를 끈 실험은 실행할 때마다 값이 달라집니다. 스레드가 얼마나 겹치느냐에 따라 초과 건수가
변하기 때문입니다. 고정된 것은 '초과가 0이 아니다'라는 사실이고, 가드를 켠 쪽은 구조적으로 항상 0입니다.