12주차 미리보기
W12 미리보기 · 웹소설 본편

STARRY PASS 8 — 같은 버튼을 쉰 번 누른 손님

손님이 결과를 못 보고 같은 버튼을 다시 눌러도, 돈은 한 번만 움직이게 만드는 접수함 이야기.

별이 거의 보이지 않던 밤, STARRY 계산대의 보내기 화면이 돌아가는 원 하나를 남긴 채 멈춰 있었다. 키타는 A 카드에서 B 카드로 1,000원을 옮기는 버튼을 눌렀다. 영수증은 안 나왔고, 로딩 표시만 돌았다.

“이미 보내진 건가요, 아니면 버튼이 씻힌 건가요?” 키타가 마우스 위에 손가락을 올린 채 물었다.

“손님은 그 둘을 구분할 수 없어.” 니지카가 답했다. “그러니 다시 누르는 게 자연스러워. 잘못은 손님이 아니라, 다시 와도 중복으로 돈을 옮기지 않는 장치가 없는 쪽이야.”

히토리는 화면을 보며 조용히 말했다. “저는 엘리베이터 버튼에 불이 들어왔는데도 다시 눌러요. 기계가 제 절실함을 더 잘 알아줄까 해서…….”

“기계는 절실함 대신 같은 요청을 두 장 받지.” 료가 간식 봉지를 접었다. “오늘은 두 장이 아니라 쉰 장으로 늘려 보자. 쉰 장이 왔을 때 실제 금고 일은 한 번이어야 해.”

네 사람이 계산대에서 같은 요청의 접수 경계를 의논하는 공식 장면
계산대에 모인 네 사람의 공식 장면컷입니다. 이하 접수함 사건은 개인 학습용 비공식 구성입니다.

니지카는 하얀 접수표 쉰 장을 계산대 위에 펼쳤다. 모두 같은 이체 요청이었지만, 번호 하나만 적히진 않았다. 접수표에는 업무 종류, 계산대가 확인한 손님, 손님이 정한 접수번호가 함께 들어갔다.

“접수번호만 보면 안 돼?” 키타가 물었다.

“다른 손님이 우연히 같은 번호를 쓸 수 있고, 다른 업무에서 같은 번호가 나올 수도 있어.” 니지카가 세 칸을 손가락으로 짚었다. “카드 이동 업무, 인증된 손님 customer-1, 접수번호 same-key. 이 세 가지가 한 묶음이야.”

히토리가 접수표 한 장의 손님 칸을 가리켰다. “손님이 직접 글자로 적은 이름을 믿나요?”

“아니. 계산대가 로그인을 통해 확인한 이름을 써야 해.” 니지카가 답했다. “보내는 카드의 실제 주인도 그 손님과 같은지 먼저 확인하고.”

TRANSFER scope와 customer-1 actor와 same-key가 한 namespace를 이루는 그림
업무 종류·인증된 손님·접수번호 세 칸이 한 접수함 주소가 됩니다.

료는 접수번호가 같다는 이유만으로 같은 주문이라고 믿으면 안 된다고 했다. 첫 접수표에는 A에서 B로 1,000원이라고 적히고, 두 번째 표에는 같은 번호와 A에서 B로 2,000원이라고 적힐 수 있었다. 그 둘을 같은 주문으로 취급하면, 2,000원을 보냈다고 믿는 손님에게 과거의 1,000원 영수증을 돌려줄 수 있었다.

“그럼 주문 글을 통째로 비교하면 되지 않을까요?” 키타가 물었다.

료는 같은 주문을 다른 모양으로 적은 두 장을 내밀었다. 한 장은 출발·도착·금액 순서였고, 다른 장은 금액·도착·출발 순서였다. 빈칸 넓이와 줄바꿈도 달랐다. 그러나 뜻은 모두 A에서 B로 1,000원이었다.

“화면 글의 모양을 비교하지 말고, 뜻이 되는 셋을 꺼내.” 료가 새 종이에 적었다. “출발 카드, 도착 카드, 금액. 항상 같은 순서, 같은 줄바꿈, 같은 숫자 표현으로 만들자.”

연습 값은 출발 10, 도착 20, 금액 3000이었다. 첫 줄에는 조리법 번호 v1을 적고, 다음 줄부터 출발 10, 도착 20, 금액 3000을 고정된 순서로 적었다. 마지막 줄 뒤에도 줄바꿈이 하나 있었다. 이 종이를 같은 글자 방식의 바이트로 바꿔 64자리 영어 소문자와 숫자 지문을 만들었다.

“출발을 10에서 11로, 도착을 20에서 21로, 금액을 3000에서 3001로 각각 바꾸면 지문은 다 달라져.” 료가 세 결과를 나란히 놓았다.

“지문은 암호화인가요?” 히토리가 물었다.

“아니. 원문을 복구해 보려는 표가 아니고, 원문을 비밀로 만드는 장치도 아니야. 같은 조리법에서 주문의 뜻이 같은지 비교하는 표식이야.”

v1 from 10 to 20 amount 3000 canonical bytes가 64자리 hash가 되는 그림
v1·출발·도착·금액을 고정해 64자리 지문으로 바꾸는 정확한 연습 값입니다.

니지카는 접수함 서랍을 열었다. 서랍 한 칸에는 접수표 한 장만 들어갈 수 있었다. 업무 종류·인증된 손님·접수번호 세 값이 모두 같은 두 번째 표를 넣으려 하면, 접수함 자체가 거절했다.

“먼저 서랍이 비었는지 보고, 비었으면 넣으면 안 되나요?” 키타가 물었다.

니지카가 표 두 장을 양손에 들었다. “두 계산대가 같은 순간에 봤다고 해 보자. 양쪽 모두 비었다고 볼 수 있어. 그 다음에는 두 장이 함께 들어가려고 하지.”

“보기와 넣기 사이에 틈이 있네.” 료가 말했다. “이런 일은 접수함에게 한 번에 시켜야 해. 새 표를 넣었다면 새 표의 번호를 돌려주고, 충돌했다면 아무 번호도 주지 않는 거야.”

접수함의 금속 규칙은 두 계산대가 아무리 같이 밀어도 결과 한 줄만 남기도록 되어 있었다. 첫 표의 지문이 a 소문자 예순네 개였고, 두 번째는 b 소문자 예순네 개였다. 하지만 세 칸이 같아서 두 번째 표는 거절됐고, 접수함의 행은 하나만 남았다.

같은 scope actor key의 두 번째 insert가 거절되어 한 행만 남는 그림
같은 세 칸의 두 번째 표를 접수함이 거절하고 행 하나만 남기는 규칙입니다.

접수함 규칙을 만들었으니, 이제 쉰 장을 동시에 밀어 넣을 차례였다. 쉰 명의 직원이 각자 같은 업무, customer-50, same-key, a 소문자 예순네 개의 지문을 든 채 출발선에 섰다. 쉰 명이 모두 준비했는지 최대 10초를 확인한 뒤, 니지카가 출발 깃발을 올렸다.

접수함에서 새 번호를 받은 표는 딱 한 장이었다. 그 표만 새 일의 주인이 됐다. 나머지 마흔아홉 장은 새 번호를 받지 못하고 이미 있는 접수라는 결과를 받았다. 모든 결과를 꺼낼 때는 각각 최대 30초를 기다리게 해 두었고, 마지막에 접수함을 세었을 때도 행은 하나였다.

히토리가 손을 번적 들었다. “쉰 명 중 한 명이 주인이면, 실제로 돈을 쉰 번 보낸 시험인 건가요?”

“아니.” 니지카가 즉시 선을 그었다. “지금 쉰 명이 경쟁한 건 접수함의 새 칸 주인 자리야. 실제 카드와 장부를 바꾸는 이체 전체를 쉰 번 부른 게 아니야.”

료는 정상 단어 중간에 따옴표가 든 수상한 손님 이름을 넣었다. 접수함은 그 글을 명령으로 오해하지 않았고, 이름 값 그대로 보관했다. 조회했을 때 그 이름과 다른 행은 0개였다.

claim 오십 건에서 owner 한 명 existing 마흔아홉 명 행 하나인 결과
접수 경쟁 쉰 건의 결과는 새 주인 1, 기존 접수 49, 실제 행 1입니다.

키타는 “기존 접수”라는 표를 받은 마흔아홉 명에게 바로 성공 영수증을 나눠 주려 했다. 니지카가 그 손을 멈춰 세웠다.

“새 번호를 못 받았다는 것은 이미 접수된 표가 있다는 뜻일 뿐이야. 그 표가 지금 요청과 같은 뜻인지, 일을 다 끝냈는지까지는 아직 보지 않았어.”

료가 기존 행에서 세 종류의 정보를 꺼냈다. 처음 접수한 주문의 지문, 처리 중인지 완료인지 나타내는 표시, 완료되었을 때만 들어 있는 결과 숫자였다. “새 주인이 아닌 사람의 다음 일은 이 세 가지를 확인하는 거야. 접수표가 있다는 사실 하나를 성공과 같은 뜻으로 쓰면 안 돼.”

히토리가 고개를 끌덕였다. “그럼 주인 한 명을 뽑는 것은 시작일 뿐이군요. 나머지 사람이 무엇을 받을지는 지문과 상태와 결과를 본 뒤에야 정해져요.”

니지카가 쉰 건 claim 결과를 화면으로 확인하는 공식 장면
니지카가 결과를 확인하는 공식 장면컷입니다. 숫자는 W12 제공 시험에서 따왔습니다.

이제 접수함의 주인이 실제 이체를 시작했다. 출발 카드의 주인이 인증된 손님과 같은지 먼저 확인했다. 이를 붙잡을 두 카드는 번호가 작은 쪽부터 잠그었다. 출발 A에는 10,000원, 도착 B에는 5,000원이 있었고, 1,000원을 옮긴 뒤에는 9,000원과 6,000원이 되었다.

그것만으로 끝이 아니었다. 이동 사건을 적은 표 한 장, A에서 나간 1,000원 장부 한 줄, B로 들어온 1,000원 장부 한 줄이 만들어졌다. 마지막으로 접수함에 완료 도장을 찍고, 이동표 번호와 두 카드의 나중 잔액을 복사 영수증용으로 보관했다. 첫 성공의 응답 숫자 201도 함께 적었다.

키타가 큰 봉투 하나를 가져왔다. “접수표, 카드 두 장, 이동표, 장부 두 줄, 완료 도장을 모두 이 안에 넣어요?”

“바로 그거야.” 니지카가 봉투를 닫았다. “중간에 예상하지 못한 문제가 밖으로 튀어나오면, 봉투 안의 변경은 모두 함께 취소돼야 해. 접수만 완료인데 돈은 취소되거나, 돈은 옮겨졌는데 완료 결과가 없는 거짓 상태를 막는 거야.”

“하나의 일을 많은 일로 쪼개 보이게 하되, 성공과 취소의 경계는 하나로 묶는 군.” 료가 말했다.

claim lock balance transaction ledger complete를 한 transaction으로 묶은 흐름
접수 주인 선정부터 이체·장부 두 줄·완료 결과 보관까지 한 봉투에 묶입니다.

히토리는 봉투의 첫 순서를 다시 보았다. 접수함에 새 표를 넣기 전에 출발 카드의 주인부터 확인하는 것이 눈에 띄었다.

“왜 접수표를 먼저 만들지 않아요? 보내기 요청이 왔으니 접수부터 하는 게 자연스러워 보이는데요.”

니지카가 거절된 손님의 가상 요청을 올려놓았다. “다른 사람의 카드를 보내는 카드라고 적은 요청이 먼저 접수함을 차지해 버리면, 실제 주인의 정상 요청이 같은 번호를 쓸 때 충돌할 수 있어. 출발 카드가 없는지, 그 카드의 주인이 로그인한 사람인지를 접수 전에 확인하는 이유야.”

료는 두 카드 번호를 작은 것부터 나열했다. 손님이 A에서 B로 보내든 B에서 A로 보내든, 잠그는 순서는 작은 번호 먼저였다. “손님이 쓴 방향을 그대로 잠금 순서로 쓰면 반대 방향 요청끼리 서로 다른 열쇠를 먼저 잡고 기다릴 수 있어. 방향이 달라도 순서는 하나로 맞추는 거야.”

완료 도장에도 조건이 있었다. 새 표를 받았을 때 받은 행 번호와 “처리 중” 표시가 여전히 있는 행 하나만 완료로 바꿀 수 있었다. 잘못된 번호나 이미 완료된 행을 다시 덮어쓰려고 하면, 바뀐 행의 수가 하나가 아니라는 이유로 작업을 실패시켰다.

“완료 도장이 있다는 것만 중요한 게 아니네요.” 키타가 말했다. “누가 받은 접수표인지와, 아직 처리 중인 표인지를 확인하고 딱 한 행만 바꿔야 하는 거군요.”

“그래.” 니지카가 답했다. “복사 영수증을 만들 수 있으려면 성공했다는 도장뿐 아니라, 첫 영수증을 다시 구성할 정확한 숫자가 필요해. 그래야 두 번째 요청이 금고 일을 다시 하지 않아도 결과를 보여 줄 수 있어.”

키타가 같은 customer-1, same-key, A, B, 1,000원으로 다시 보내기를 눌렀다. 이번에는 새 접수번호가 나오지 않았다. 계산대는 기존 행의 지문과 현재 주문의 지문을 비교했다. 둘은 같았고, 접수 상태는 완료였으며, 저장된 결과 세 값도 있었다.

“그럼 A에서 또 1,000원을 빼요?” 히토리가 물었다.

“아니.” 료가 완료 칸에서 과거의 숫자를 읽었다. “이동표 번호, A의 9,000원, B의 6,000원을 다시 보여 주고, 이것은 이미 끝난 결과를 다시 읽었다고 표시해.”

첫 응답에는 새로 만들었다는 201과 “재사용 아님”이, 두 번째에는 기존 결과를 보여 준다는 200과 “재사용 맞음”이 담겼다. 두 응답은 모두 성공이지만 뜻은 달랐다. 두 번째 응답은 새 이체가 아니었다.

다음에는 같은 접수번호로 2,000원을 보냈다. 접수함 주소는 같았지만 지문이 달랐다. 계산대는 과거 성공을 복사하지 않고 접수번호 충돌이라고 거절했다. 오류 종류는 “다른 주문과의 충돌”이었다. 이 오류는 손님에게는 409 응답으로 바뀌도록 준비되어 있었지만, 이 범위의 화면 시험이 직접 409를 눌러 본 것은 아니었다.

보내는 카드만 다르거나 받는 카드만 달라지는 경우도 추가 일을 만들지 않고 충돌했다. 기존 A와 B, 새로 만든 C 카드의 잔액은 각각 9,000원, 11,000원, 5,000원으로 남았다. 기존 이동표는 하나, 이체 장부는 두 줄, 접수함은 한 행이었다.

“같은 지문인데 완료 결과가 아직 없으면요?” 히토리가 물었다.

“완료인 척 영수증을 만들면 안 돼.” 니지카가 답했다. “현재 구현은 ‘처리 중’이라는 별도 충돌로 멈춰. 다른 주문 충돌과 같은 409 계열로 바꾸더라도, 오류 이름은 구분해야 해.”

같은 완료 요청 replay 다른 hash conflict 미완료 in progress의 세 갈래
같은 지문의 완료 결과는 재사용, 다른 지문은 충돌, 미완료는 처리 중으로 나뉘다.

니지카는 봉투가 정말 한꺼번에 취소되는지 보기 위해 두 곳에 고장 버튼을 달았다. 첫 번째는 접수함의 새 주인이 된 직후였다. 두 번째는 카드 잔액을 바꾸고 이동표와 장부를 쓴 직후였다. 둘 모두 예상하지 못한 프로그램 문제를 밖으로 던졌다.

첫 고장 뒤에는 접수 행 0, 이체 이동표 0, 이체 장부 0이었다. 두 번째 고장 뒤에도 같은 세 숫자가 모두 0이었다. 이 두 직접 시험은 카드 잔액을 별도로 언급하지는 않았지만, 더 넓은 누적 시험은 두 지점의 고장 모두에서 10,000원과 10,000원이 그대로임까지 확인했다.

“전원을 뽑아도 모든 순간이 반드시 같다고 주장할 수 있나요?” 히토리가 조심스럽게 물었다.

“안 돼.” 료가 답했다. “지금 넣은 건 동기식 데이터베이스 작업 안에서의 두 프로그램 예외야. 물리적 종료의 모든 순간, 네트워크 단절, 외부 결제 기계까지 자동으로 증명한 게 아니야.”

니지카는 오래된 처리 중 행을 무조건 지우고 다시 실행하는 방법에도 선을 그었다. 외부 기계에서는 돈이 이미 움직였지만 STARRY에 완료 도장만 못 왔을 수 있었다. 확인 없이 지우고 재실행하면 정말 두 번 옮겨질 위험이 있었다. 그 경우에는 외부 결과와 장부를 맞춰 보고 별도의 복구 규칙을 만들어야 했다.

claim 뒤와 business 뒤 RuntimeException의 DB 효과 영과 누적 balance 원복
접수 직후와 업무 변경 직후의 프로그램 고장에서 로컬 DB 효과가 함께 0으로 돌아갑니다.

료는 여러 시험 결과를 한 장의 큰 표로 모았다. 가장 먼저는 10,000원이 든 A와 10,000원이 든 B 사이에서 3,000원을 보냈다. 첫 결과는 새 처리, 두 번째는 결과 재사용이었다. 잔액은 7,000원과 13,000원, 계좌를 열 때 만들어진 두 줄까지 포함한 전체 장부는 4줄, 이체 두 줄의 부호 합은 0이었다.

다음에는 화면 글에서 칸 순서와 공백만 다르고 뜻은 같은 1,000원 요청을 두 번 보냈다. 숫자 세 개를 어설픈 규칙으로 뽑아내는 연습 도구를 써서, 둘은 같은 지문이 됐다. 잔액은 9,000원과 11,000원이고 추가 이체 효과는 하나도 없었다. 하지만 이 도구는 단순한 숫자 세 칸을 찾는 연습용이지, 모든 모양의 화면 글을 안전하게 읽는 일반 도구는 아니었다.

또 다른 시험은 같은 접수번호의 실제 이체 요청 스무 개를 같은 출발선에서 보냈다. 스무 결과 중 새 처리는 1개였고, A와 B는 9,000원과 11,000원, 계좌 열기까지 포함한 장부는 4줄이었다. 이는 실제 이체 전체를 스무 번 부른 시험이었다. 앞의 쉰 건 시험은 접수함만 경쟁시켰다는 차이를 다시 적었다.

반대 방향 이체도 스무 건을 실행했다. A에서 B로 100원씩 열 건, B에서 A로 100원씩 열 건이었다. 모두 끝난 뒤 두 카드는 각각 10,000원, 합은 20,000원, 계좌 열기 두 줄과 이체 스무 건의 두 줄씩을 합한 전체 장부는 42줄이었다. 각 결과에는 최대 20초 기다림이 붙었지만, 이 한 실험이 운영 환경에서 영원히 교착 없음이나 성능을 보장하지는 않았다.

claim 오십 service 동시 스무 opposite 방향 스무 증거의 역할 구분
접수 50·실제 같은-key 이체 20·반대 방향 20의 서로 다른 증거를 정확히 나눈 표입니다.

마지막으로 손님 화면에서 같은 요청을 두 번 보냈다. 출발 카드는 10,000원, 도착 카드는 5,000원, 금액은 1,000원, 접수번호는 W12-REPLAY였다. 첫 응답의 숫자는 201이었다. 같은 본문을 다시 보낸 두 번째 응답은 200이었고, 본문에는 이미 끝난 결과를 재사용했다는 표시가 참으로 들어 있었다.

“이 화면 시험 하나로 다른 네 가지 화면 규칙까지 모두 증명했다고 하면 안 돼.” 니지카가 주의를 적었다. “직접 눌러 본 건 첫 201, 재사용 200, 재사용 표시 참이야. 다른 기능은 이 이야기 뒤의 범위야.”

“숫자가 작으면 증거가 약해 보여도, 본 것만 말하는 게 더 강한 문서야.” 료가 말했다.

첫 HTTP 이백일 재생 이백 replayed true의 직접 assertion
화면 시험이 직접 확인한 것은 첫 201, 재사용 200, 재사용 표시 참입니다.

히토리는 벽 하나를 여섯 칸으로 나눈 검사표를 보았다. 하루별 실행 묶음은 여섯 개였지만, 네 번째 묶음이 두 검사 대상을 함께 부르므로 최종 실행 대상은 일곱 개였다. 그 일곱 대상 안의 서로 다른 작은 검사를 세면 최종 성공 실행은 열여덟 개였다.

검사 묶음의 출처표를 기준으로 현재 작은 검사를 나누면, 이번 주 표시가 붙은 묶음 안에 일곱 개, 이전에서 발전해 이어진 묶음 안에 열한 개가 있었다. 이는 각 작은 검사가 최초로 쓰인 날을 뜻하지는 않았다. 발전해 이어진 묶음의 작은 검사들도 이름과 구성이 바뀌어 있었다. 시작할 때 의도적으로 실패하는 연습이 있는 칸은 네 곳이었다. 그 실패 실행까지 포함해 작은 검사가 물리적으로 실행된 횟수를 세면 스물여섯이었다. 열여덟과 스물여섯은 다른 의미였다.

“여섯, 일곱, 열여덟, 스물여섯…… 숫자가 왜 이렇게 많아요?” 히토리가 눈을 돌렸다.

“문을 연 횟수와, 문 안에서 확인한 항목 수와, 실패하는 예행연습까지 포함한 횟수가 다르기 때문이야.” 니지카가 각 숫자 옆에 이름을 붙였다. “최종에 문을 연 하루 명령은 여섯, 그 안의 검사 대상 클래스는 일곱이야. 실패 예행연습까지 더하면 하루 명령은 열, 검사 대상 클래스 횟수는 열둘, 작은 검사 실행은 스물여섯이 돼.”

코드 파일도 나누어 보았다. 이번 주의 최종 해답 파일은 네 개였다. 주문 지문, 접수함, 이체 중심, 화면 연결이었다. 그 네 파일을 확인하는 시험 파일 일곱 개와 화면 오류·응답·고장 도우미 네 개가 함께 열렸다. 그 외에 이전 주차에서 만든 접수함의 금속 규칙 파일 하나를 재검증했으므로, 이 범위에서 연 정본 파일은 총 열여섯 개였다. 새 해답 네 개와 정본 열여섯은 다른 집계였다.

키타가 스물여섯 번째 체크를 하고 펜을 내려놓았다. “이제 버튼을 다시 눌러도 무조건 다시 이체하는 게 아니라, 같은 접수인지와 같은 뜻인지, 완료 결과가 있는지를 순서대로 보는 거네요.”

“그리고 다른 뜻이면 성공인 척하지 않고 충돌이라고 말해.” 료가 덧붙였다.

“아직 끝나지 않았으면 끝났다고 말하지 않고.” 히토리가 따라 말했다. “자신 있게 다시 돈을 움직이는 것보다, 확실하지 않으면 멈추는 게 더 정직한 거네요.”

니지카는 그날의 마지막 규칙을 벽에 붙였다. 요청은 여러 번 도착할 수 있다. 같은 주문의 데이터베이스 효과는 제공된 동기식 범위에서 한 번이어야 한다. 그러나 이 한 줄은 모든 외부 기계와 모든 고장 순간, 모든 운영 규모의 영원한 보장을 뜻하지는 않았다.

네 사람이 직접 증거와 비보장을 나누어 검토하는 공식 장면
네 사람이 직접 본 것과 아직 보지 않은 것을 나누어 확인하는 공식 장면컷입니다.

세이카가 계산대 뒤에서 첫 201 영수증과 재사용 200 영수증을 바라보았다. 두 장은 모양이 달랐지만 같은 이동표 번호와 같은 잔액을 가리켰다. 새 이체는 한 번이었고, 두 번째 표는 과거의 결과를 정직하게 다시 보여 주었다.

“손님이 버튼을 쉰 번 누르지 않게 만들 수는 없어도,” 키타가 말했다. “쉰 번이 와도 누가 실제 일을 맡을지는 접수함이 한 번 결정할 수 있어요.”

“실제 이체 전체의 스무 요청에서도 새 효과는 하나였고.” 니지카가 덧붙였다. “하지만 쉰과 스무를 섞어 말하지는 말자.”

히토리는 조용히 고개를 끌덕였다. “같은 후회를 쉰 번 해도 인생이 한 번만 줄어드는 접수함이 있으면 좋겠네요.”

료가 고개를 저었다. “그건 이번 범위 밖.”

그래도 STARRY의 접수함은 제 자리에 서 있었다. 같은 요청을 억지로 새 일로 바꾸지 않고, 다른 요청을 과거의 성공으로 속이지 않으며, 아직 모르는 상태를 완료라고 장담하지 않는 접수함이었다.

이 문서의 공식 장면컷은 TV 애니메이션 《봇치 더 록!》 공식 사이트 자료입니다. ©하마지 아키·호분샤/애니플렉스. 접수함 사건과 개념도는 개인 학습용 비공식 구성이며, 재배포·상업 이용 권리를 뜻하지 않습니다.

W12 미리보기 · 히토리 질문 노트

히토리의 질문 노트 — W12 중복 요청의 경계

본편의 쉽게 풀어 쓴 접수함 비유를 실제 클래스·SQL·테스트 이름으로 연결합니다. 각 답은 직접 assertion한 사실, 코드에 구현된 사실, 월~토 범위가 보장하지 않는 것을 구분합니다.

1. 이번 미리보기의 정확한 원문 범위는 어디까지인가요?

원문 PDF 362쪽의 W12 overview부터 415쪽 끝까지, manifest day 1~6만 포함합니다. 416쪽에서 일 모듈 | W12 멱등성 누적 Gate 표제가 시작하므로 그 줄부터 day 7입니다. 424쪽의 주간 통합과 425쪽 해설도 제외합니다.

기존 W12 audit가 적은 362~425쪽은 일요일·주간 통합 열 쪽을 더한 오류입니다. 그 범위에만 있는 final four-method HTTP contract, day-7 TransferControllerTest, 누적 Gate의 전체 주장은 이 미리보기의 증거로 쓰지 않습니다.

실제 기술 이름W12 strict PDF scope p362–p415, manifest days 1–6 only, Sunday begins p416

2. 요청이 여러 번 와도 업무 효과를 한 번만 남기려는 계약의 이름은 무엇인가요?

idempotency입니다. 네트워크 timeout, 사용자의 재클릭, client retry로 같은 의미의 요청이 반복해도 DB business effect를 추가로 만들지 않고 기존 결과를 재사용하는 계약입니다.

하지만 “무조건 성공”이 아닙니다. 같은 key에 다른 payload가 오면 충돌, 같은 payload이어도 완료 결과가 없으면 처리 중으로 멈춰야 합니다. 같음·다름·미확정을 정직하게 나누는 것이 핵심입니다.

실제 기술 이름idempotency, replay, semantic conflict, in-progress state

3. 접수번호 하나만 비교하지 않고 세 값을 묶는 이유는 무엇인가요?

현재 namespace는 scope='TRANSFER', 인증된 actorId, idempotencyKey의 조합입니다. 다른 customer가 같은 key를 써도 actor가 다르면 다른 접수입니다. 업무 종류가 다르면 scope가 달라 또 다른 접수가 됩니다.

actorId는 request body에서 받은 임의 글자가 아니라 Principal.getName()으로 확인한 사용자입니다. TransferService도 출금 계좌의 ownerId를 찾아 command actor와 같은지 먼저 확인합니다. key 하나를 전역 고유값으로 두는 설계와 다릅니다.

실제 기술 이름idempotency namespace (scope, actor_id, idempotency_key), authenticated actor binding

TRANSFER scope와 customer-1 actor와 same-key가 한 namespace를 이루는 그림
그림 한눈에: key 하나가 아니라 operation·actor·key 세 값이 함께 접수함 주소를 만듭니다.

4. 10·20·3000 요청의 canonical bytes는 정확히 어떤 모양인가요?

RequestHasher.canonicalBytes(10, 20, 3000)의 UTF-8 text는 v1\nfrom=10\nto=20\namount=3000\n과 정확히 같아야 합니다. 버전 v1, from, to, amount 순서, Long.toString 십진수 표현, 마지막 newline까지 계약의 일부입니다.

solution은 세 숫자 중 하나라도 0 이하면 IllegalArgumentException 예외를 던지지만, RequestHasherTest의 두 @Test는 양수 경계만 씁니다. 따라서 양수 검사는 구현에 있는 사실이고, 이 test가 직접 증명한 사실과는 구분합니다.

실제 기술 이름RequestHasher.VERSION, versioned canonical serialization, UTF-8 fixed-order decimal bytes

v1 from 10 to 20 amount 3000 canonical bytes가 64자리 hash가 되는 그림
그림 한눈에: 표현 모양이 아니라 의미 필드 셋을 고정된 조리법으로 바꿉니다.

5. 64자리 hash가 직접 보장하는 것과 보장하지 않는 것은 무엇인가요?

hash(10,20,3000)SHA-256 digest를 소문자 hex로 표현한 64자리 문자열입니다. 두 번째 test는 from을 10→11, to를 20→21, amount를 3000→3001로 각각 바꾸면 기준 hash와 다르다고 assertion합니다.

이 것은 모든 가능한 입력이 수학적으로 절대 충돌하지 않음, 원문 복구, 암호화, 원문 비밀성을 보장하지 않습니다. actor·key·currency는 현재 hash 입력에 없습니다. 새 semantic field를 더하면 version 계약과 기존 재사용 정책을 함께 검토해야 합니다.

실제 기술 이름SHA-256 lowercase hexadecimal request fingerprint and its non-guarantees

6. DB의 중복 금지 규칙은 무엇을 직접 확인하나요?

IdempotencySchemaIT.uniqueScopeActorAndKeyAreEnforcedByV001idempotency_request를 비운 뒤 같은 TRANSFER/customer-1/same-key로 hash a×64를 먼저 INSERT합니다. 그 다음 hash만 b×64로 바꾼 두 번째 INSERT가 DataIntegrityViolationException을 내고, 최종 row count가 1인지 봅니다.

재검증하는 schema는 W6D1 solution에서 이어진 V001__common.sql이며 UNIQUE (scope, actor_id, idempotency_key)를 포함합니다. 이 test는 다른 actor와 다른 key가 허용되는지, 문자열 최대 길이, status 값의 제한, 동시 claim 50건을 한꺼번에 증명하지는 않습니다.

실제 기술 이름PostgreSQL composite UNIQUE (scope, actor_id, idempotency_key), IdempotencySchemaIT

같은 scope actor key의 두 번째 insert가 거절되어 한 행만 남는 그림
그림 한눈에: hash만 바꾼 두 번째 행도 같은 namespace라면 DB가 거절합니다.

7. 왜 existsinsert 두 단계 대신 SQL 한 문장으로 claim하나요?

두 transaction이 동시에 exists=false를 보면 둘 모두 새 주인이라고 착각할 틈이 생깁니다. solution은 INSERT ... ON CONFLICT (scope, actor_id, idempotency_key) DO NOTHING RETURNING id를 native query 한 문장으로 실행합니다.

새 행을 넣은 호출은 RETURNING id의 행 하나를 받아 Optional.of(id)를 돌려줍니다. 충돌한 호출은 result list가 비어 Optional.empty()를 돌려줍니다. empty는 DB 오류가 아니라 “이미 접수한 요청이 있음”이라는 정상 분기입니다.

실제 기술 이름atomic claim with INSERT ON CONFLICT DO NOTHING RETURNING id, Optional<Long> owner result

8. 쉰 건 시험이 정확히 경쟁시킨 것과 결과는 무엇인가요?

AtomicClaim50IT.fifty_concurrent_claims_have_one_owner_and_forty_nine_existing_results는 50-thread pool, task 50개, ready latch 50, start latch 1을 씁니다. ready는 최대 10초를 기다린 뒤 동시 출발하고, 각 Future<Optional<Long>>을 읽을 때는 각각 30초 timeout을 씁니다. 하나의 전체 30초 deadline이 아닙니다.

직접 assertion은 present owner 1, empty existing 49, idempotency_request row 1입니다. 모든 입력은 TRANSFER/customer-50/same-key/a×64로 같습니다. 이것은 IdempotencyStore.claim repository 경쟁이지, 실제 이체 service를 50번 호출한 시험이 아닙니다.

실제 기술 이름AtomicClaim50IT, owner 1 / existing 49 / table row 1

claim 오십 건에서 owner 한 명 existing 마흔아홉 명 행 하나인 결과
그림 한눈에: 50은 claim 소유권 경쟁의 task 수이며, 50건 송금이 아닙니다.

9. 따옴표가 든 actor 문자열 시험은 어디까지 보여 주나요?

named_parameters_do_not_turn_actor_text_into_sql은 actor로 owner' OR '1'='1을 넣고 probe-key, b×64로 claim합니다. claim result가 present이고, actor_id <> :probe인 row count가 0인지 assertion하여 따옴표 포함 text가 SQL 문법이 아니라 parameter 값으로 저장되었음을 보여 줍니다.

이 한 test로 앱 전체의 모든 SQL injection 경로, HTTP input validation, 동적 table·column name 조립의 안전까지 보장하지는 않습니다. 확인한 것은 해당 claim query의 named parameter 경계입니다.

실제 기술 이름JPA native query named parameters, parameterized SQL probe boundary

10. 새 claim의 주인이 된 뒤 이체는 어떤 순서로 진행되나요?

TransferService.transfer 순서는 command validation → from·to·amount hash → source account owner 확인 → claim('TRANSFER', actorId, idempotencyKey, hash) → after-claim hook → 두 account id 오름차순 lock → withdraw/deposit → business_tx 1행 → TRANSFER_OUT·TRANSFER_IN ledger 2행 → after-business hook → response body 저장 → claim COMPLETED입니다.

response body는 현재 txId,fromBalance,toBalance의 comma 구분 문자열이고 response_status=201과 함께 저장됩니다. 이는 내부의 제한된 복원 포맷이지, 임의 JSON body를 보관하는 일반 장치가 아닙니다. outer @Transactional에 claim·business·complete가 함께 참여합니다.

실제 기술 이름TransferService.transfer, single Spring transaction from claim through complete

claim lock balance transaction ledger complete를 한 transaction으로 묶은 흐름
그림 한눈에: claim을 먼저 별도 commit하지 않고 이체·원장·완료와 같은 transaction에 묶습니다.

11. 월~토 W12 정본 파일은 몇 개이고, 어떤 역할로 나뉘나요?

starter와 day 7을 빼고 월~토에서 사용하는 정본은 누적 V001__common.sql까지 포함해 16개입니다. learner solution 4개는 RequestHasher.java, IdempotencyStore.java, TransferService.java, TransferController.java입니다. selector body인 test source는 7개입니다. provided main support 4개는 ApiExceptionHandler.java, ErrorCode.java, TransferFailureHook.java, TransferResponse.java입니다. 여기에 누적 schema 1개를 더하면 4+7+4+1=16입니다.

V001__common.sql은 W12 모듈 폴더에 새 solution으로 들어온 파일이 아니라 W6D1에서 누적된 learner target입니다. W12 day 2는 현재 파일의 composite unique를 IdempotencySchemaIT로 재검증합니다. “이번 주 새 solution 4개”와 “범위에서 연 정본 16개”를 섞지 않습니다.

기존 D3 Green evidence의 target hash c9852d0b...는 현재 IdempotencyStore solution hash ce070bd1...와 다르고, 구 evidence에는 현재 manifest가 요구하는 W12D3_RED_EXPECTED_ATOMIC_CLAIM marker도 비어 있습니다. 따라서 그 historical evidence는 보조 자료일 뿐 canonical 근거로 쓰지 않고, current manifest·solution·selector body를 따릅니다.

실제 기술 이름W12 canonical artifact inventory: solution 4 + selector test 7 + provided main support 4 + cumulative V001 1 = 16

12. 같은 key와 같은 payload가 완료 뒤 다시 오면 코드는 어떤 검사를 하나요?

claim이 empty이면 replay(command, requestHash)로 갑니다. find로 기존 request_hash, status, response_status, response_body를 읽은 뒤, 먼저 hash가 같은지 봅니다. 같고 status가 COMPLETED이며 body가 null이 아니면 comma로 나눈 세 값을 Result로 복원하고 replayed=true를 설정합니다.

이 경로는 account lock, withdraw, deposit, transaction save, ledger save를 다시 실행하지 않습니다. replay는 “이체를 두 번 실행했지만 우연히 같은 결과”가 아니라, 저장한 첫 결과를 복원한 것입니다.

실제 기술 이름IdempotencyStore.find, completed response replay, TransferService.Result.replayed

13. 같은 key에 amount·from·to 중 하나가 달라지면 어떤 충돌인가요?

새 request hash가 기존 hash와 다르면 BusinessException(ErrorCode.IDEMPOTENCY_CONFLICT)입니다. key만 같다는 이유로 과거의 1,000원 결과를 새 2,000원 요청에 돌려주면 손님은 다른 업무가 성공했다고 오해하게 됩니다.

월~토 source의 ApiExceptionHandlerIDEMPOTENCY_CONFLICTIDEMPOTENCY_IN_PROGRESS를 모두 HTTP 409로 mapping합니다. 하지만 D1~D6의 HTTP ReplayTest는 409를 직접 호출하지 않습니다. service 충돌 test, handler의 구현, HTTP 409 직접 test는 서로 다른 층입니다. 마지막 층은 p416 이후 day 7이므로 제외합니다.

실제 기술 이름IDEMPOTENCY_CONFLICT, service-level semantic conflict, handler mapping versus HTTP assertion boundary

14. hash는 같지만 status가 아직 COMPLETED가 아니면 어떻게 되나요?

existing.status()COMPLETED가 아니거나 responseBody()가 null이면 BusinessException(ErrorCode.IDEMPOTENCY_IN_PROGRESS)를 던집니다. 같은 payload라고 해도 완료 결과가 없는데 성공 body를 추측하지 않습니다.

현재 동기식 DB-only 흐름은 claim부터 complete까지 하나의 transaction으로 묶습니다. 그래도 일반화해 오래된 PROCESSING row를 자동 삭제·재실행하면 안 됩니다. 외부 결제망이 있다면 외부 effect는 로컬 DB transaction에 자동 참여하지 않으므로 reconciliation·recovery policy가 따로 필요합니다.

실제 기술 이름IDEMPOTENCY_IN_PROGRESS, PROCESSING recovery and reconciliation boundary

같은 완료 요청 replay 다른 hash conflict 미완료 in progress의 세 갈래
그림 한눈에: same hash+COMPLETED, different hash, same hash+incomplete를 서로 다른 결과로 보냅니다.

15. 고장 주입 위치 두 곳은 무엇을 직접 확인하나요?

TransferFailurePointITafterClaimRuntimeExceptionRollsBackClaimafterBusinessRuntimeExceptionRollsBackEveryEffect는 각각 AFTER_CLAIM, AFTER_BUSINESS에서 RuntimeException을 던집니다. 두 경우 모두 idempotency_request 0행, TRANSFER business_tx 0행, TRANSFER_% ledger_entry 0행을 직접 assertion합니다. 이 test 자체는 balance를 직접 assertion하지 않습니다.

이월되어 확장된 TransferIntegrationTest의 두 runtime failure test는 같은 지점에서 from/to balance가 10,000/10,000, 접수 0, 이체 거래 0, 이체 원장 0임까지 확인합니다. 증거가 어느 test에 있는지를 분리합니다.

실제 기술 이름TransferFailurePointIT, evolved TransferIntegrationTest, transaction rollback at two injected runtime-failure points

claim 뒤와 business 뒤 RuntimeException의 DB 효과 영과 누적 balance 원복
그림 한눈에: 두 RuntimeException 지점의 DB 효과 0과, 더 넓은 누적 test의 balance 원복을 구분합니다.

16. 순차 replay와 변경 amount 충돌을 함께 보는 직접 test의 값은 무엇인가요?

TransferIdempotencyIT.samePayloadReplaysAndChangedPayloadConflictsWithoutExtraEffect는 from 10,000, to 5,000으로 시작합니다. customer-1/same-key/from/to/1,000의 첫 result는 replayed=false, 똑같은 두 번째 result는 replayed=true입니다. amount만 2,000으로 바꾼 세 번째 호출은 BusinessExceptionIDEMPOTENCY_CONFLICT입니다.

최종에 직접 assertion하는 DB 수는 TRANSFER business_tx 1행, TRANSFER_% ledger_entry 2행입니다. 이 test는 최종 balance, HTTP 409 status, HTTP error body, 동시 burst를 직접 assertion하지 않습니다.

실제 기술 이름TransferIdempotencyIT.samePayloadReplaysAndChangedPayloadConflictsWithoutExtraEffect

17. 첫 이체와 같은-key replay 시나리오의 정확한 잔액·원장 값은 무엇인가요?

TransferIntegrationTest.transfer_and_same_key_replay_once는 A 10,000, B 10,000으로 계좌를 열고 key-1, amount 3,000을 두 번 호출합니다. first replayed=false, replay replayed=true, 최종 balance 7,000/13,000을 assertion합니다.

ledger.count()는 4입니다. 이 숫자에는 계좌 개설 때 생긴 opening ledger 2행과 이체 OUT/IN 2행이 함께 들어 있습니다. entry_type LIKE 'TRANSFER_%'로 필터한 signed_amount 합은 0입니다. “이체 ledger가 4행”이라고 쓰면 opening과 transfer를 섞은 오류입니다.

실제 기술 이름transfer_and_same_key_replay_once, balance 7,000/13,000, total ledger 4 including opening rows

18. JSON 필드 순서와 공백이 달라도 재사용되는 test는 일반 JSON canonicalizer를 증명하나요?

아닙니다. json_field_order_and_whitespace_variant_replays_without_extra_effectfrom,to,amount 순서의 첫 문자열과, amount,to,from 순서에 공백·newline이 다른 두 번째 문자열을 씁니다. 두 문자열에서 각 숫자를 Pattern regex로 뽑아 Command를 만듭니다.

첫 result는 false, 두 번째는 true, balance는 9,000/11,000, business_tx 1, transfer ledger 2, idempotency row 1입니다. 하지만 문자열·소수·음수·중첩 object·escape·duplicate key를 포함한 모든 JSON 표현을 안전하게 해석하는 일반 parser나 canonicalizer가 아닙니다.

실제 기술 이름json_field_order_and_whitespace_variant_replays_without_extra_effect, limited regex fixture parser

19. 실제 이체 전체를 같은 key로 동시 호출한 수는 몇 개인가요?

same_key_concurrent_requests_change_business_once는 task 20개, 20-thread pool, ready latch 20, start latch 1을 씁니다. customer-1/burst-key/from/to/1,000을 20번 동시 호출하고 ready는 최대 5초, 각 future는 각각 15초 timeout으로 읽습니다.

직접 assertion은 !replayed result 1개, balance 9,000/11,000, total ledger 4, transfer transaction 1, transfer ledger 2, idempotency row 1입니다. 앞의 50건은 repository claim 경쟁이고, 여기의 20건이 실제 service 이체 burst입니다. 서로 바꿔 말하지 않습니다.

실제 기술 이름same_key_concurrent_requests_change_business_once, service burst 20 with one non-replay effect

20. 반대 방향 20건 이체 test의 정확한 fixture와 한계는 무엇인가요?

opposite_direction_transfers_preserve_total_balance는 A·B를 각각 10,000으로 엽니다. A→B 100원씩 10건과 B→A 100원씩 10건, 총 task 20개에 서로 다른 ab-0..9, ba-0..9 key를 씁니다. 각 future는 각각 최대 20초를 기다립니다.

최종 from balance 10,000, to balance 10,000, 합 20,000, total ledger 42를 assertion합니다. 42는 opening 2행 + 20 transfer×2행입니다. 이 fixture가 끝났다는 사실은 모든 scheduler·DB 부하·운영 규모에서 deadlock이 절대 없음, 재시도 구현, 성능 SLA를 보장하지 않습니다.

실제 기술 이름opposite_direction_transfers_preserve_total_balance, 20 tasks, amount 100, balances 10,000/10,000, ledger 42

21. amount 말고 from·to를 바꾼 충돌 test는 어떤 추가 효과가 없음을 확인하나요?

same_key_with_changed_from_conflicts_without_extra_effectsame_key_with_changed_to_conflicts_without_extra_effect는 제3의 account C를 5,000으로 엽니다. 첫 A→B 1,000을 완료한 뒤, 같은 key로 from만 C로 또는 to만 C로 바꿔 IDEMPOTENCY_CONFLICT를 확인합니다.

최종 A/B/C balance는 9,000/11,000/5,000이고 helper assertOneTransferEffect()가 transfer transaction 1, transfer ledger 2, idempotency row 1을 확인합니다. amount 변경 test도 같은 effect count를 확인합니다. 다른 actor가 같은 key를 쓰는 namespace 분리는 이 세 test의 직접 fixture가 아닙니다.

실제 기술 이름changed-from and changed-to semantic idempotency conflict regressions

22. D6 HTTP test가 직접 assertion한 것은 딱 무엇인가요?

TransferControllerReplayTest.firstIs201AndReplayIs200transactionId='W12-REPLAY', amount 1,000, from 10,000, to 5,000 상태에서 같은 JSON body를 두 번 POST합니다. httpBasic('customer-1','password')/api/transfers를 호출합니다.

직접 assertion은 첫 response status 201, 두 번째 response status 200, 두 번째 body가 "replayed":true를 포함한다는 세 가지입니다. 두 body의 businessTransactionId가 같은지, 두 balance field가 같은지, DB effect가 하나인지, 변경 payload가 409인지는 이 test의 direct assertion이 아닙니다.

실제 기술 이름TransferControllerReplayTest.firstIs201AndReplayIs200, direct HTTP assertions 201 / 200 / replayed true

첫 HTTP 이백일 재생 이백 replayed true의 직접 assertion
그림 한눈에: D6 실행 test가 본 세 assertion만 표시하고, 본문 전체 동일성은 더 넓히지 않습니다.

23. D6 교재의 공개 계약과 실제 test assertion은 왜 분리해야 하나요?

D6 설명은 첫 201·replay 200에 더해 두 응답의 businessTransactionId·잔액 동일성을 완성품 계약으로 설명합니다. TransferService.replay의 구현도 저장한 tx id·balance를 복원합니다. 하지만 실제 TransferControllerReplayTest의 assertion은 Q22의 세 개뿐입니다.

따라서 “구현이 복원하는 필드”, “PDF가 요구하는 완성 계약”, “D6 test가 직접 assertion한 증거”를 섞어 하나의 보장으로 말하지 않습니다. day-7 TransferControllerTest를 끌어와 공백을 메우는 것도 금지합니다.

실제 기술 이름implementation versus published exercise contract versus direct executable assertion

24. day run 수·selector 수·@Test 수를 정확히 어떻게 나누나요?

월~토 day block은 6개입니다. 그러나 day 4 selector list에 TransferIdempotencyITTransferFailurePointIT 두 클래스가 있어 final-Green selector target은 1+1+1+2+1+1=7개입니다. 각 selector class의 @Test를 펼치면 day별 2+1+2+3+9+1=18개가 실행됩니다.

D1~D6에서 서로 다른 @Test 메서드도 18개입니다. 동일 메서드를 다른 day에서 다시 selector하는 구성이 없기 때문입니다. 하루 수 6, selector target 7, final-Green method execution 18, unique method 18은 서로 다른 집계입니다.

실제 기술 이름6 day blocks / 7 selector targets / 18 final-Green @Test executions / 18 unique methods

claim 오십 service 동시 스무 opposite 방향 스무 증거의 역할 구분
그림 한눈에: 실행 수와 동시 task 수는 각 시험의 역할과 함께 읽어야 합니다.

25. 18개 test의 W12 신규·이월 구분과 Red 포함 물리 실행 수는 어떻게 되나요?

manifest introducedAt클래스 단위 provenance로 현재 메서드를 묶으면, W12로 표시된 다섯 클래스의 현재 메서드는 RequestHasherTest 2 + IdempotencySchemaIT 1 + AtomicClaim50IT 2 + TransferIdempotencyIT 1 + TransferControllerReplayTest 1 = 7개입니다. carried/evolved로 표시된 두 클래스의 현재 메서드는 TransferFailurePointIT 2 + TransferIntegrationTest 9 = 11개입니다. 합은 18입니다. 이 7/11은 각 현재 메서드의 생성 시점을 뜻하지 않습니다. evolved 클래스의 메서드 구성과 이름은 이전 주차에서 달라졌습니다.

redExpected=true인 day는 D1·D3·D4·D6입니다. Red에서 실행되는 method 수는 각각 2, 2, 3, 1이므로 final-Green 18에 8을 더한 물리 @Test execution은 26입니다. day command 단위로는 final Green 6개 + Red 4개 = 10개입니다. selector-class entry 단위로는 final Green 7개 + Red의 5개(D1 1, D3 1, D4 2, D6 1) = 12개입니다. 메서드 실행 26, selector-class entry 12, day command 10을 섞지 않습니다.

실제 기술 이름manifest class-provenance grouping 7/11; physical method 26 / selector-class entry 12 / day command 10

26. SQL workbook Q15·Q16은 이 W12 월~토 범위에 있나요?

없습니다. curriculum ledger는 W12가 learner-authored Red/Green 모듈 네 개를 소유해 시간이 부족하다고 명시하고, 필수 문제를 삭제하지 않은 채 Q15를 W13 day 1 인덱스 0, Q16을 W15 day 1 인덱스 0으로 이월합니다. 즉 W12 미리보기에 SQL 해답이나 실행 결과를 만들면 범위 오염입니다.

Q15 prompt는 “계좌별 총 입금액·총 출금액”, 핵심은 CASE 조건부 집계, 통과 규칙은 원장 방향을 한 번만 계산하는 것입니다. Q16 prompt는 “일별 성공/실패 건수”, 핵심은 DATE·FILTER/CASE, 통과 규칙은 0으로 나누지 않는 것입니다. 이것은 이월 metadata일 뿐, W12 canonical SQL answer·seed result가 아닙니다.

실제 기술 이름deferred SQL ownership: W13-SQL-Q15 and W15-SQL-Q16, no W12 SQL execution or canonical answer

27. W12 월~토 test가 보장하지 않는 것을 한 번에 정리하면?

외부 결제망을 포함한 exactly-once, 전원 차단·process hard kill의 모든 시점, stale PROCESSING 자동 복구, 모든 JSON canonicalization, SHA-256의 절대 무충돌, hash의 암호화·비밀성, 운영 규모 burst·TPS·latency SLA는 보장하지 않습니다.

또한 50 claim test는 50번 이체가 아니고, opposite-direction fixture 한 번의 완료는 모든 deadlock 불가능 증명이 아니며, retry logic은 W12 D1~D6 정본에 구현되지 않았습니다. HTTP 409·businessTransactionId/balance 동일성·DB effect 1을 D6 replay test 하나가 직접 assertion했다고 확장해도 안 됩니다.

실제 기술 이름explicit W12 non-guarantees and evidence-layer boundaries

28. 이 주차를 작업 순서 한 줄로 기억하려면 어떻게 말하면 되나요?

“인증된 actor·scope·key로 접수를 구분하고, from·to·amount를 versioned canonical hash로 비교하며, DB unique와 한 문장 claim으로 주인 하나를 뽑고, claim·이체·원장·complete를 한 transaction으로 묶은 뒤, 같은 완료 요청은 replay하고 다른 요청과 미완료 요청은 서로 다른 충돌로 멈춘다”입니다.

숫자는 역할과 함께 기억합니다. claim 경쟁 50 = owner 1/existing 49/row 1, 실제 same-key service burst 20 = new effect 1, HTTP direct proof = 201/200/replayed true, 범위 집계 = day 6/selector 7/final-Green test 18/Red 포함 physical test 26입니다. 숫자를 섞지 않는 것이 계약을 정직하게 설명하는 첫 단계입니다.

실제 기술 이름W12 end-to-end idempotent transfer reasoning chain and evidence ledger