10주차 미리보기
W10 미리보기 · 웹소설 본편

STARRY PASS 6 — 두 열쇠가 서로를 기다린 밤

같은 돈을 동시에 고칠 때 생기는 두 사고를, 기다리는 열쇠와 바뀌는 도장으로 나누어 보는 STARRY 마감 이야기

공연이 끝난 STARRY 계산대에는 숫자표가 붙은 작은 금고 두 개와 봉인된 시험 봉투 여섯 장이 놓여 있었다. 히토리는 봉투를 한 줄로 세우다가 첫 장에 적힌 주의문을 소리 내어 읽었다.

“앞 봉투에서 남은 잔액을 다음 봉투로 가져가지 않는다. 시험을 바꿀 때마다 관련 표를 비우고, 그 시험이 요구한 시작값을 새로 만든다.”

“같은 밤에 이어지는 이야기지만 금고 속 데이터는 매번 새 무대라는 뜻이야.” 니지카가 말했다. “그래야 앞 실험의 흔적을 다음 결과로 착각하지 않지.”

키타가 첫 봉투를 열었다. 금고에는 10,000원이 든 카드 한 장이 있었다. 직원 두 명이 각각 1,000원을 빼기로 했다. 둘은 서로가 숫자를 읽을 때까지 기다렸다가, 함께 다음 동작으로 넘어갔다.

첫 직원은 10,000원을 읽었다. 둘째 직원도 10,000원을 읽었다. 첫 직원은 자신이 읽은 수에서 1,000원을 빼 9,000원을 적었다. 둘째 직원도 똑같이 9,000원을 적었다. 두 작업은 모두 완료표를 냈다.

히토리가 완료표 두 장을 겹쳐 들었다. “완료가 두 번이면 8,000원이어야 하는 것 아닌가요?”

료가 마지막 카드를 가리켰다. “완료 두 번. 마지막 잔액 9,000원. 변경 도장 0. 둘째 작업이 실패한 게 아니라, 두 작업이 같은 옛 숫자를 바탕으로 같은 새 숫자를 써서 변화 하나가 덮였어.”

이 실험에서는 평소 카드가 가진 변경 도장 보호를 일부러 건너뛰었다. 읽은 숫자가 아직 최신인지 묻지 않고, 시험 전용 통로에서 잔액만 바로 덮어썼다. 그래서 이 결과는 보호 장치 없는 사고 모양을 보여 주지만, 실제 서비스의 모든 저장 통로가 고장 났다는 뜻은 아니었다.

완료 두 번이지만 최종 잔액 구천원과 버전 영인 lost update 기준선
그림 한눈에: 두 직원이 모두 10,000원을 읽고 모두 9,000원을 써서 완료표는 2장인데 마지막 잔액은 9,000원, 변경 도장은 0이다.
완료 수와 마지막 값을 나누어 확인하는 키타와 히토리의 공식 장면
그림 한눈에: 키타와 히토리가 “작업이 끝났다”와 “변화가 모두 남았다”를 서로 다른 확인 칸으로 나눈다.

첫 실험에는 시간 울타리도 있었다. 각 직원의 작업 봉투는 최대 10초, 두 사람이 모두 읽었다는 신호를 기다리는 장벽도 최대 10초, 바깥에서 각 완료표를 받는 기다림은 최대 20초였다. 그 울타리는 시험이 영원히 매달리지 않게 하는 장치였다. 일부러 느린 금고나 시간 초과 사고를 만들어 검증한 것은 아니었다.

니지카는 결과표 아래에 세 줄을 적었다.

‘직접 본 것: 완료 2, 잔액 9,000, 도장 0.’

‘만들어 낸 조건: 두 직원이 같은 10,000원을 먼저 읽음.’

‘말하면 안 되는 것: 실제 서비스의 모든 동시 변경이 언제나 이 결과가 됨.’

히토리는 마지막 줄에 동그라미를 쳤다. “사고를 일부러 잘 보이게 만든 무대와 실제 영업 창구를 같은 것으로 말하면 안 되는군요.”

키타는 이번에는 마지막 잔액만 가리고 완료표 두 장만 히토리에게 보여 줬다. 히토리는 잠시 고민하다가 “둘 다 끝났다”까지만 답했다. 반대로 완료표를 가리고 9,000원만 보여 주자, 어느 작업이 실패했는지와 둘 다 같은 값을 썼는지를 구분할 수 없었다.

“그래서 동시 작업 영수증은 한 칸으로 끝나면 안 돼.” 니지카가 말했다. “몇 작업을 보냈는지, 각각 끝났는지, 마지막 상태가 무엇인지가 함께 있어야 해. 마지막 숫자 하나만 맞아도 중간 실패가 숨을 수 있고, 완료 수만 맞아도 지금처럼 변화가 사라질 수 있어.”

료는 ‘시작 10,000 / 요청 2장 / 요청당 1,000 / 완료 2장 / 마지막 9,000 / 도장 0’이라고 한 줄로 이어 썼다. 히토리는 그제야 머릿속 계산 8,000과 실제 관찰 9,000이 어느 칸에서 갈라졌는지 찾을 수 있었다.

두 번째 봉투를 열기 전에 키타는 금고를 다시 비우고 10,000원짜리 새 카드를 넣었다. 이번에도 직원 두 명이 각각 1,000원을 빼지만, 카드를 읽기 전에 열쇠부터 받아야 했다.

첫 직원이 열쇠를 쥐고 10,000원을 읽었다. 9,000원을 적고 변경 도장을 하나 올린 뒤 열쇠를 놓았다. 그동안 둘째 직원은 카드 앞에서 기다렸다. 열쇠를 받은 둘째 직원은 옛 10,000원이 아니라 최신 9,000원을 읽었고, 8,000원을 적었다.

완료표는 다시 두 장이었다. 이번 마지막 잔액은 8,000원이었다.

“첫 직원이 읽는 동안 둘째를 줄에 세웠기 때문에, 둘째가 최신 숫자에서 계산했어요.” 키타가 말했다.

“맞아. 이 무대에서는 카드 한 장을 읽을 때부터 작업이 끝날 때까지 열쇠를 쥔 효과가 있었어.” 니지카가 답했다. “다만 오늘 표가 직접 확인한 것은 완료 2와 잔액 8,000까지야. 마지막 변경 도장 숫자를 따로 채점하지 않았고, 누가 먼저 섰는지나 모두에게 공평한 줄인지도 재지 않았어.”

료는 첫 결과와 둘째 결과를 나란히 놓았다. 보호가 없는 통로는 10,000에서 두 번 계산했지만 9,000만 남았다. 열쇠 통로는 10,000에서 9,000, 다시 8,000으로 이어졌다. 같은 시작값이어도 읽는 순간을 보호하는지에 따라 마지막 값이 달라졌다.

행 잠금으로 만원 구천원 팔천원을 차례로 읽는 두 작업
그림 한눈에: 열쇠가 없으면 두 명이 같은 10,000원을 읽지만, 열쇠 줄에서는 10,000→9,000→8,000 순서로 최신 값을 읽는다.

“그러면 모든 카드에 열쇠를 걸면 끝인가요?” 히토리가 물었다.

니지카는 두 표 사이에 굵은 구분선을 그었다. “두 결과는 같은 금고에서 연달아 돈을 뺀 기록이 아니야. 각각 새로 10,000원을 넣고 시작한 독립 실험이야. 9,000원 결과에서 다시 두 번 빼서 8,000원이 된 게 아니라, 같은 시작선을 두 보호 방식에 따로 준 거지.”

히토리는 첫 표의 마지막 9,000원에서 둘째 표를 이어 그리던 화살표를 지웠다. 그 대신 두 표 위에 각각 ‘새 시작 10,000’을 적었다. 이야기의 장면은 이어져도 데이터의 시작 상태는 이어지지 않는다는 첫 주의문이 다시 필요해진 순간이었다.

료가 금고 두 개를 탁자 위에 올렸다. “카드가 두 장이면 열쇠도 두 개야. 잡는 순서가 제각각이면 새로운 사고가 생겨.”

세 번째 봉투에는 실제 실행표보다 먼저 사고를 상상해 보는 그림 카드가 들어 있었다. A 금고에서 B 금고로 돈을 옮기는 직원과, B에서 A로 옮기는 직원이 마주 보고 있었다.

키타가 A 열쇠를 먼저 집고 B 열쇠를 기다렸다. 히토리는 B 열쇠를 먼저 집고 A 열쇠를 기다리는 역할을 맡았다. 누구도 자기가 가진 열쇠를 놓지 않는다고 가정하자, 기다림 화살표가 A에서 B로, B에서 A로 이어져 동그라미를 만들었다.

“저희가 실제 금고를 이렇게 멈춰 세운 건 아니죠?” 히토리가 그림 카드를 조심스럽게 내려놓았다.

“응. 오늘 제공된 실행표는 일부러 이 사고를 일으켜 잡아내는 표가 아니야.” 니지카가 분명히 말했다. “이 그림은 왜 두 열쇠의 공통 순서가 필요한지 설명하는 위험 모형이야. 실행에서 실제로 사고가 한 번 났다가 자동으로 회복됐다고 쓰면 안 돼.”

A와 B 열쇠를 서로 보유하고 기다리는 가상의 닫힌 원
그림 한눈에: A를 쥔 채 B를 기다리고, B를 쥔 채 A를 기다리면 기다림 화살표가 원이 된다. 이 원은 위험 설명이며 이번 실행에서 일부러 재현한 결과가 아니다.

니지카는 A와 B라는 이름 대신 각 카드의 고유 번호를 보자고 했다. 돈이 A에서 B로 가든 B에서 A로 가든, 항상 번호가 작은 카드의 열쇠를 먼저 받고 큰 카드의 열쇠를 나중에 받는 규칙이었다.

“B에서 A로 보내는 요청인데 A 번호가 더 작으면 A 열쇠부터 받는 거예요?” 키타가 물었다.

“그래. 열쇠 순서와 돈의 방향은 다른 칸이야.” 료가 답했다. “두 카드를 열어 온 뒤 원래 출발 번호와 도착 번호로 다시 찾아. 작은 카드가 언제나 출발 카드인 건 아니야.”

히토리는 ‘작은 카드 = 출발 카드’라고 썼다가 지웠다. 대신 두 줄을 만들었다. 첫 줄은 ‘열쇠: 작은 번호→큰 번호’. 둘째 줄은 ‘돈: 원래 출발 카드에서 빼고 원래 도착 카드에 더함’이었다.

실제 창구는 번호 두 개를 작은 순서로 정리해 금고 담당자에게 넘겼다. 금고 담당자의 조회표에도 번호 오름차순으로 카드를 가져오라는 줄이 있었다. 가져온 두 카드는 번호표로 다시 묶어, 원래 출발과 도착 역할을 복구했다. 카드가 정확히 두 장이 아니면 계산을 이어 가지 않았다.

아이디 잠금 순서와 원래 송금 역할을 분리한 그림
그림 한눈에: 9번에서 3번으로 보내도 열쇠는 3→9 순서로 받고, 두 카드를 받은 뒤 돈의 역할은 원래 요청대로 9번 출발·3번 도착으로 되찾는다.

“종이에 번호만 정렬하고 금고가 실제로 그 순서로 열쇠를 주지 않으면 부족하겠네요.” 히토리가 말했다.

“맞아. 그리고 다른 오래된 창구 하나가 손님이 말한 순서대로 열쇠를 잡아도 원이 다시 생길 수 있어.” 니지카가 답했다. “공통 순서는 관련된 모든 길이 함께 지켜야 의미가 있어.”

이제 실제 실행표를 펼칠 차례였다. 금고 표들을 다시 비운 뒤, 첫째 카드와 둘째 카드에 각각 10,000원을 넣었다. 두 카드의 주인은 모두 같은 손님 번호로 맞췄다.

키타는 A에서 B로 100원을 옮기는 요청 열 개를 만들었다. 표식은 ab-0부터 ab-9였다. 히토리는 B에서 A로 100원을 옮기는 요청 열 개를 만들었고, 표식은 ba-0부터 ba-9였다. 모두 합쳐 스무 작업이었다.

스무 직원은 각자 준비 깃발을 내린 뒤 출발 종을 기다렸다. 바깥 진행자는 최대 10초 동안 준비 깃발 스무 개가 모두 내려오는지 확인했다. 준비가 끝나자 종을 한 번 울렸다. 스무 작업이 완전히 같은 CPU 순간에 움직였다는 뜻은 아니지만, 한꺼번에 경쟁할 기회를 크게 만든 출발선이었다.

각 작업의 결과표는 최대 30초까지 기다려 하나씩 회수했다. 결과표를 받다가 예외가 나거나 시간이 넘으면 그 자리에서 실행은 실패였다. 마지막 합계가 우연히 맞더라도, 받지 못한 결과표를 조용히 버리고 성공으로 부르지 않았다.

히토리는 시험 삼아 결과표 하나를 뒤집어 숨긴 뒤 합계 저울만 내밀었다. 두 방향의 다른 변화가 서로 상쇄되면 총액은 여전히 20,000처럼 보일 수도 있었다. 니지카는 즉시 숨긴 표를 찾아냈다.

“총액 보존은 강한 확인이지만, 모든 요청의 완료를 대신하지는 않아. 반대로 스무 완료표가 있어도 장부 부호가 틀리거나 돈이 새로 생기면 안 되고.”

키타는 스무 칸짜리 체크판에 표를 하나씩 꽂았다. 마지막 한 장까지 들어온 뒤에만 잔액 저울과 장부 저울을 함께 열었다. 세 확인은 서로 경쟁하는 답이 아니라, 서로 다른 구멍을 막는 답이었다.

스무 장을 모두 받은 뒤에야 잔액을 새로 읽었다. 두 카드의 잔액을 더한 값은 20,000원이었다. 이동 장부에서 나간 돈은 음수, 들어온 돈은 양수로 모아 더했더니 0이었다.

반대 방향 스무 작업과 잔액 합 이만원 원장 합 영의 영수증
그림 한눈에: A→B 10건과 B→A 10건, 건당 100원, 작업 20개를 모두 회수한 뒤 두 잔액 합 20,000원과 이동 장부 부호합 0을 확인한다.

“왕복 횟수와 금액이 같으니 각 카드도 10,000원이라고 결과표에 써도 되나요?” 키타가 물었다.

니지카가 고개를 저었다. “그렇게 계산될 것이라고 추론할 수는 있어도, 이 시험의 마지막 채점식은 각 카드가 반드시 10,000원인지 따로 비교하지 않아. 직접 채점한 값은 두 카드의 합 20,000과 장부 합 0이야. 증거표에는 assertion이 본 범위를 써야 해.”

료도 한 줄을 보탰다. “시험 이름에 멈춤이 없다는 말이 들어 있어도, 모든 계좌 조합과 모든 부하에서 영원히 멈춤이 없다는 증명은 아니야. 이 두 카드, 스무 작업, 이 시간 울타리, 이 실행에서 모두 끝났다는 관찰.”

네 사람이 스무 작업의 결과표를 함께 검토하는 공식 장면
그림 한눈에: 네 사람이 성공 이름보다 실제로 회수한 스무 결과표와 두 개의 마지막 합계를 먼저 맞춰 본다.

실행이 끝나자 니지카는 같은 표를 새 표처럼 세 번 세지 못하게 했다. 첫 번째로 이 실행표를 꺼낸 뒤, 다음 봉투에서는 정답을 가리고 번호 순서의 이유를 다시 설명하고 같은 실행표를 돌렸다. 그다음 봉투에서는 결과를 다시 가리고, 모든 완료표의 제한 시간과 총액 보존을 다시 말한 뒤 또 같은 표를 돌렸다.

“세 번 실행했으니까 서로 다른 새 시험 세 개인가요?” 히토리가 물었다.

“아니.” 료가 답했다. “실행 노출은 세 번이지만 이름과 내용이 같은 시험 하나를 세 번 회수한 거야. 반복 학습 횟수와 고유 시험 수를 따로 세야 해.”

반복 봉투 사이에는 애플리케이션 금고와 분리된 SQL 연습장이 두 권 놓여 있었다. 첫 연습장은 거래 한 건마다 그 거래 계좌의 고객 이름을 옆에 붙이는 문제였다.

거래표에는 고객 번호가 직접 없었다. 거래표의 계좌 번호로 계좌표를 찾고, 계좌표의 고객 번호로 고객표를 찾아야 했다. 출발표를 고객표로 잡으면 거래가 없는 고객까지 섞이거나 한 행의 뜻이 바뀔 수 있었다. 그래서 첫 행의 뜻을 ‘거래 한 건’으로 고정하고, 연결 열쇠를 따라 두 번 이어 붙였다.

기본 연습장에는 거래표가 21행 있었다. 계좌와 고객 연결 열쇠가 모두 존재하는 자료이므로 결과도 거래 한 건당 한 행, 21행이었다. 이것은 실행 소스에 들어 있는 정답 파일을 복사한 결과가 아니라, 별도 연습장의 계약을 만족하는 예시와 씨앗 자료의 예상 수였다.

거래에서 계좌와 고객을 이어 거래 스물한 행을 유지하는 조인
그림 한눈에: 거래 21행을 출발점으로 거래의 계좌 번호→계좌의 고객 번호→고객 이름을 따라가며 출력 한 행의 뜻을 거래 한 건으로 유지한다.

두 번째 연습장은 거래가 한 번도 없는 계좌를 찾는 문제였다. 이번에는 모든 계좌를 출발줄에 세웠다. 각 계좌 왼쪽은 반드시 남겨 두고, 오른쪽에 거래표를 붙였다. 붙을 거래가 없어 오른쪽 거래 번호가 빈 계좌만 골랐다.

기본 자료의 계좌는 여덟 개였다. 그중 103번 A-103과 104번 A-104만 거래표의 주 계좌 번호로 한 번도 등장하지 않았다. 결과는 계좌 한 건당 한 행, 정확히 두 행이었다. 계좌 하나에 거래가 여러 건 있더라도 ‘거래 없는 계좌’ 결과가 중복되지 않아야 했다.

거래 없는 계좌 백삼과 백사 두 행을 고르는 안티 조인
그림 한눈에: 계좌 여덟 행을 모두 남겨 거래표를 붙인 뒤 거래 번호가 빈 103(A-103), 104(A-104) 두 계좌만 고른다.

히토리는 두 연습장의 방향을 바꾸어 적었다. 첫 문제는 거래를 잃지 않고 이름을 붙이는 일이므로 거래표에서 출발했다. 둘째 문제는 거래가 없는 계좌까지 남겨야 하므로 계좌표에서 출발했다.

“이어 붙인다는 말은 같아도, 무엇을 한 행으로 보존할지가 다르네요.”

“그게 시작표와 출력 한 행의 뜻을 먼저 적는 이유야.” 니지카가 말했다. “두 문제 모두 제공된 정본 답안 파일은 없어. 예시 문장과 네가 실제로 실행해 남긴 증거를 구분해야 하고.”

마지막 봉투에서는 열쇠 대신 카드의 변경 도장을 사용했다. 금고는 다시 비워졌고, 새 카드에는 잔액 10,000원과 도장 0이 찍혔다. 두 직원은 서로 독립된 작업 봉투를 열고 같은 카드를 읽었다.

둘은 모두 잔액 10,000원과 도장 0을 읽었다. 두 사람이 읽기를 마쳤다는 신호가 모일 때까지 다음 단계로 가지 않았다. 신호가 모이자 둘 다 1,000원을 빼 9,000원으로 만들고 저장을 시도했다.

첫 저장은 ‘현재 도장이 아직 0인가?’라는 조건을 만족했다. 금고는 잔액 9,000원을 저장하고 도장을 1로 바꿨다. 다른 저장은 옛 도장 0을 들고 왔지만, 금고의 현재 도장은 이미 1이었다. 금고는 두 번째 변화를 조용히 덮어쓰지 않고 충돌표를 돌려줬다.

결과 바구니에는 성공 1장과 예상된 도장 충돌 1장이 정확히 들어 있었다. 마지막 잔액은 9,000원, 마지막 도장은 1이었다. 예상 밖 기술 실패가 결과 바구니를 대신 차지했다면 이 순서 없는 두 장 비교를 통과할 수 없었다.

료는 결과 바구니를 세 칸으로 나눴다. 첫 칸은 저장 성공, 둘째 칸은 도장이 이미 바뀌었다는 예상 충돌, 셋째 칸은 그 밖의 고장이었다. 겉 포장지가 긴 오류가 와도 안쪽 원인을 따라가 도장 충돌인지 확인하고, 그것도 아니면 셋째 칸에 넣었다.

“둘 중 어느 직원이 성공했는지도 정해져 있나요?” 키타가 물었다.

“아니. 결과 비교는 순서를 따지지 않아.” 니지카가 답했다. “A가 성공하고 B가 충돌할 수도, 반대일 수도 있어. 직접 약속한 것은 승자의 이름이 아니라 성공 한 장과 예상 충돌 한 장이 함께 있다는 사실이야.”

버전 영을 함께 읽은 두 작업의 커밋 하나 충돌 하나 시간선
그림 한눈에: 도장 0·잔액 10,000원을 함께 읽은 두 작업 중 하나만 9,000·도장 1로 저장하고, 다른 하나는 예상 충돌표를 받는다.

“충돌한 쪽을 자동으로 다시 넣어서 둘 다 성공시키나요?” 히토리가 물었다.

“오늘 상자에는 그런 장치가 없어.” 니지카가 답했다. “이 표가 확인하는 것은 성공 하나, 예상 충돌 하나, 마지막 9,000과 도장 1이야. 다시 시도해서 8,000원이 되는지, 스무 명이나 천 명이 몰렸을 때 어떤지, 최대 몇 번 다시 보낼지는 범위 밖이야.”

각 작업 봉투의 최대 시간은 10초였다. 두 사람이 같은 옛 도장을 읽게 맞추는 신호도 최대 10초를 기다렸다. 바깥에서 각 결과표를 기다리는 시간은 최대 20초였다. 이것 역시 충돌 후 자동 재실행을 검증한 시간이 아니라 시험 정지를 막는 울타리였다.

료는 열쇠 방식과 도장 방식을 한 문장씩만 비교했다. “열쇠 방식은 먼저 기다리게 해서 최신 값을 차례로 읽는다. 도장 방식은 먼저 읽게 두되 저장할 때 옛 값이면 충돌을 드러낸다.”

키타가 뒤를 이었다. “둘 다 조용히 변화 하나가 사라지는 걸 막으려는 방법이지만, 오늘 시험만으로 어느 쪽이 모든 상황에서 더 빠르거나 더 좋은지는 정하지 못해요.”

니지카와 료가 두 충돌 처리 방식을 비교하는 공식 장면
그림 한눈에: 니지카와 료가 ‘기다려서 차례로 처리’와 ‘옛 도장을 거절’의 차이를 결과값 기준으로 정리한다.

세이카는 여섯 봉투를 모두 닫기 전에 계산표 한 장을 요구했다. 첫 봉투의 보호 없는 덮어쓰기 시험 1회, 둘째 봉투의 한 카드 열쇠 시험 1회, 반대 방향 스무 작업 시험 3회, 마지막 도장 충돌 시험 1회였다.

실행 노출은 모두 6회였다.

같은 반대 방향 시험을 세 번 실행했으므로 서로 다른 시험 함수는 4개였다. 그중 이번 주 자료에 새로 들어온 고유 시험은 반대 방향 스무 작업과 도장 충돌, 2개였다. 보호 없는 덮어쓰기와 한 카드 열쇠 시험 2개는 앞 주에 들어온 시험을 다시 꺼낸 것이었다.

여섯 봉투 모두 이미 제공된 시험을 가리고 설명한 뒤 다시 확인하는 범주였다. 새 시험 파일이 둘 생겼다는 것과 학습자가 그 정답을 처음 작성해 소유했다는 것은 다른 축이었다. 이 미리보기의 여섯 범위에서는 제공 자료 확인 표가 여섯 장이고, 학습자가 새로 작성했다고 인정된 표는 0장이었다.

실행 노출 여섯 고유 시험 넷 W10 신규 둘을 구분한 표
그림 한눈에: 월~토 범위의 실행 노출 6회에서 같은 반대 방향 시험의 반복을 합치면 고유 시험 4개, 그중 W10 신규는 2개다.

니지카는 마지막 표를 둘로 나눴다. 왼쪽에는 직접 본 숫자를 적었다. 보호 없는 길의 완료 2·잔액 9,000·도장 0. 한 카드 열쇠 길의 완료 2·잔액 8,000. 반대 방향 건당 100원·스무 작업·합계 20,000·장부 합 0·각 결과표 30초 울타리. 변경 도장 길의 성공 1·예상 충돌 1·잔액 9,000·도장 1. SQL 연습장의 21행과 두 계좌 103·104.

오른쪽에는 아직 말할 수 없는 것을 적었다. 실제 기다림 원을 재현하고 자동으로 다시 시도했다는 주장. 모든 부하에서 멈춤이 없다는 주장. 각 계좌가 반드시 10,000원이라는 직접 채점. 모두에게 공평한 순서와 처리 속도. 충돌 뒤 재실행 성공. 스무 명·천 명의 도장 경쟁. SQL 문제의 제공 정본 답안.

히토리는 두 칸을 오래 바라보다가 말했다. “안전하다는 큰 단어보다, 어떤 시작값을 두고 몇 장의 결과표를 받았는지를 말하는 편이 오히려 이해하기 쉽네요.”

“그게 오늘의 열쇠야.” 니지카가 웃었다. “열쇠를 잡는 순서도, 우리가 말하는 근거의 순서도 뒤섞지 않는 것.”

료가 다음 상자를 계산대 아래에서 밀어냈다. 상자에는 ‘같은 카드에 출금 요청 20개’라고 적혀 있었다. 이번에는 반대 방향으로 돈을 되돌리는 짝이 없었다. 성공, 정상 거절, 예상 밖 오류를 하나도 잃지 않고 세어야 했다.

키타가 스무 장의 빈 결과표를 나누었다. “이번 밤에는 원을 만들 수 있는 순서를 정리했으니, 다음에는 몰려온 결과를 정확히 분류하네요.”

히토리는 마지막으로 작은 메모를 남겼다.

‘두 작업이 끝났다고 두 변화가 모두 남은 것은 아니다. 기다리게 할지, 옛 도장을 거절할지는 서로 다른 선택이다. 어느 쪽을 보더라도 시작값, 결과 수, 마지막 값, 그리고 시험이 직접 보지 않은 범위를 함께 적는다.’

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

히토리의 질문 노트 — W10 잠금 순서와 변경 도장

본문의 쉬운 비유를 실제 테스트·코드·SQL 이름에 연결합니다. 각 답은 “직접 확인한 것”과 “이번 범위가 확인하지 않은 것”을 함께 적습니다.

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

원문 PDF의 W10 시작표가 있는 263쪽부터 299쪽의 일 모듈 표제가 나오기 직전까지입니다. 299쪽 상단에는 여섯 번째 모듈의 마무리 문장이 이어지므로 그 부분까지 범위 안입니다. 범위 안 selector는 보호 없는 덮어쓰기 시험 1회, 한 카드 행 잠금 시험 1회, 같은 정렬 이체 시험 3회, 변경 도장 시험 1회로 총 6회 노출됩니다. 같은 299쪽에서 일 모듈이 시작된 뒤의 누적 실행과 이후 주간 해설·면접 확장은 포함하지 않습니다.

따라서 전체 주차 요약의 더 넓은 숫자를 이 문서의 실행 수로 가져오지 않습니다. 이 미리보기는 6회 노출, 고유 @Test 4개, W10에서 직접 새로 들어온 고유 시험 2개를 기준으로 설명합니다.

실제 기술 이름W10 day 1–6 selector scope, PDF pages 263–298, cumulative-Gate exclusion

2. 보호 없는 첫 시험의 실제 메서드와 결과는 무엇인가요?

메서드는 LostUpdateBaselineIT.test_only_versionless_unconditional_updates_commit_twice_and_lose_one_update입니다. 각 시험 전 관련 네 표를 비우고 LOST-UPDATE 계좌를 10,000원으로 엽니다. 두 독립 트랜잭션이 모두 10,000원을 읽은 뒤 각각 1,000원을 뺀 9,000원을 조건 없이 씁니다.

직접 assertion은 완료된 commit 수 2, 최종 balance 9,000, 최종 version 0입니다. “두 번 성공했으니 8,000원”이라는 기대와 달리 같은 옛 값에서 만든 9,000원이 서로를 덮어 변화 하나가 사라지는 기준선입니다.

실제 기술 이름LostUpdateBaselineIT.test_only_versionless_unconditional_updates_commit_twice_and_lose_one_update, lost update baseline

완료 두 번이지만 최종 잔액 구천원과 버전 영인 lost update 기준선
그림 한눈에: commit 2개와 최종 변화 1개를 따로 세어야 완료 수만으로 lost update를 놓치지 않는다.

3. 두 스레드가 같은 10,000원을 읽도록 만든 장벽은 데이터베이스 잠금인가요?

아닙니다. CountDownLatch(2)는 두 작업이 모두 읽기를 마칠 때까지 시험 코드 안에서 다음 동작을 늦추는 조정 장치입니다. readBarrier.countDown()으로 도착을 알리고, 두 작업 모두 await(readBarrier)를 통과한 뒤 쓰기 때문에 사고 순서를 재현하기 쉽습니다.

이 장벽은 애플리케이션 메모리의 동시성 도구이며 계좌 행에 데이터베이스 lock을 걸지 않습니다. 다른 트랜잭션의 읽기·쓰기를 보호하는 기능도 없고, 두 스레드가 정확히 같은 CPU 나노초에 실행됐다고 증명하지도 않습니다.

실제 기술 이름CountDownLatch, deterministic concurrency barrier, test coordination versus database locking

4. 왜 첫 시험을 실제 운영의 변경 도장 실패라고 부르면 안 되나요?

첫 시험은 JPA 엔티티 갱신 대신 UPDATE account SET balance=:balance WHERE id=:id라는 시험 전용 원시 SQL을 사용합니다. version=:oldVersion 조건도 없고 version=version+1도 없습니다. 그래서 최종 버전이 0인 채 잔액만 덮어써집니다.

이 경로의 목적은 보호 장치를 제거했을 때 lost update가 어떤 숫자로 보이는지 만드는 것입니다. 실제 Account 엔티티의 @Version 동작이 깨졌다는 증거, 모든 격리 수준에서 늘 같은 결과라는 증거, production 저장 경로 전체의 결론이 아닙니다.

실제 기술 이름versionless unconditional raw SQL, test-only fixture, optimistic-version non-coverage

5. 한 카드에 열쇠를 건 두 번째 시험은 무엇을 직접 확인하나요?

메서드는 LostUpdateBaselineIT.production_pessimistic_lock_path_commits_twice_and_preserves_both_updates입니다. 시작 잔액은 다시 만든 10,000원입니다. 두 독립 트랜잭션이 SELECT balance FROM account WHERE id=:id FOR UPDATE로 같은 행을 읽고 각각 1,000원을 뺍니다.

먼저 lock을 얻은 작업이 10,000→9,000으로 저장한 뒤 다음 작업이 9,000→8,000으로 이어갑니다. 직접 assertion은 commit 2개와 최종 잔액 8,000입니다. SQL이 version=version+1을 실행하지만 이 메서드는 마지막 version 숫자를 assertion하지 않으므로 결과 계약에 별도 숫자로 추가하지 않습니다.

실제 기술 이름LostUpdateBaselineIT.production_pessimistic_lock_path_commits_twice_and_preserves_both_updates, SELECT … FOR UPDATE

행 잠금으로 만원 구천원 팔천원을 차례로 읽는 두 작업
그림 한눈에: 둘째 작업이 첫째 작업 뒤 최신 9,000원을 읽어 최종 8,000원을 남기는 순차화 결과다.

6. FOR UPDATE가 실제로 하는 일은 무엇인가요?

PostgreSQL의 SELECT … FOR UPDATE는 조회된 행에 갱신용 row lock을 획득합니다. 첫 트랜잭션이 그 행을 잡고 있는 동안 경쟁하는 갱신용 잠금 요청은 기다리고, 첫 트랜잭션이 끝난 뒤 최신 상태를 읽어 이어서 처리할 수 있습니다.

하지만 “무조건 공평한 순서”, “모든 부하에서 빠름”, “여러 행을 잡아도 교착 없음”을 자동 보장하는 주문은 아닙니다. 행을 얼마나 오래 잡는지, 여러 자원을 어떤 순서로 잡는지, timeout과 실패를 어떻게 다루는지는 별도의 설계와 시험이 필요합니다.

실제 기술 이름PostgreSQL row-level lock, pessimistic write locking, transaction-held lock

7. 첫 두 시험의 10초와 20초는 어떤 경계인가요?

두 시험이 만드는 TransactionTemplatePROPAGATION_REQUIRES_NEW이고 timeout은 10초입니다. 보호 없는 시험의 읽기 장벽도 최대 10초 기다립니다. 바깥 테스트는 각 Future<Boolean>future.get(20, TimeUnit.SECONDS)로 최대 20초 기다립니다.

이 숫자는 시험이 영원히 멈추지 않도록 둔 시간 울타리입니다. 일부러 느린 SQL을 만들어 timeout을 재현했다거나, lock timeout 뒤 자동 복구·재시도에 성공했다는 시험이 아닙니다. 정상 fixture의 작업이 주어진 제한 안에 완료됐다는 범위로 읽어야 합니다.

실제 기술 이름transaction timeout 10 seconds, latch timeout 10 seconds, Future.get timeout 20 seconds

8. 두 계좌가 서로를 기다리는 원은 실제 테스트에서 재현됐나요?

아닙니다. A→B 경로가 A를 잡고 B를 기다리는 동시에 B→A 경로가 B를 잡고 A를 기다리면 wait-for graph에 cycle이 생길 수 있다는 위험 모형입니다. 이것이 deadlock을 설명하는 전형적인 모양이지만, W10 월~토의 제공 테스트는 입력 순서로 일부러 잠그고 실제 deadlock victim을 관찰하지 않습니다.

따라서 “deadlock이 발생했고 retry로 회복했다”, “lock timeout 예외를 직접 분류했다”는 문장은 근거 밖입니다. 확인한 것은 정렬된 생산 경로로 준비된 반대 방향 작업들이 이 fixture의 제한 안에서 완료됐다는 사실입니다.

실제 기술 이름deadlock wait-for cycle, hypothetical risk model, deadlock-reproduction non-coverage

A와 B 열쇠를 서로 보유하고 기다리는 가상의 닫힌 원
그림 한눈에: 닫힌 기다림 원은 공통 잠금 순서가 필요한 이유를 설명하지만, 이번 테스트의 실제 발생 영수증은 아니다.

9. 실제 TransferService는 두 계좌의 잠금 순서를 어떻게 만드나요?

W7D5 solution에서 이어 온 누적 production TransferServicetransferfromAccountIdtoAccountIdList.of(...).stream().sorted().toList()로 정렬한 뒤 accounts.findAllForUpdateOrderById(...)에 넘깁니다. 예를 들어 from=9, to=3이어도 잠금 요청 목록은 [3, 9]입니다.

조회 결과가 두 행이 아니면 ACCOUNT_NOT_FOUND로 중단합니다. 그리고 조회한 계좌를 ID Map에 넣은 뒤 원래 fromAccountIdtoAccountId로 다시 찾아 출금·입금 역할을 복구합니다. 정렬은 lock 획득 순서이지 송금 방향 변경이 아닙니다.

실제 기술 이름TransferService.transfer, sorted account IDs, global lock ordering, role restoration by ID Map

아이디 잠금 순서와 원래 송금 역할을 분리한 그림
그림 한눈에: 잠금 순서 3→9와 송금 역할 9→3을 분리해야 정렬 때문에 출발·도착을 뒤집지 않는다.

10. Java에서 목록을 정렬하기만 하면 데이터베이스 잠금 순서가 보장되나요?

입력 목록 정렬만으로는 충분하지 않습니다. W6D2 scaffold에서 이어 온 AccountRepository.findAllForUpdateOrderById@Lock(LockModeType.PESSIMISTIC_WRITE)select a from Account a where a.id in :ids order by a.id를 함께 사용합니다. 실제 조회 결과가 ID 오름차순으로 오고, 그 행들에 비관적 쓰기 잠금을 요구하는 경로입니다.

다만 모든 관련 코드 경로가 같은 규칙을 써야 cycle 위험을 줄일 수 있습니다. 다른 repository나 오래된 이체 경로가 입력 순서로 같은 자원을 잡거나, 다른 종류의 자원을 반대 순서로 잡는다면 이 한 메서드의 정렬만으로 시스템 전체 deadlock 부재를 증명할 수 없습니다.

실제 기술 이름AccountRepository.findAllForUpdateOrderById, PESSIMISTIC_WRITE, JPQL order by a.id

11. 작은 ID가 항상 보내는 계좌인가요?

아닙니다. from=9, to=3 요청에서 작은 ID 3은 받는 계좌일 수 있습니다. 서비스는 잠금을 [3, 9] 순서로 얻은 다음 byId.get(9)를 from, byId.get(3)을 to로 다시 지정합니다. 이후에만 from에서 출금하고 to에 입금합니다.

작은 ID를 무조건 from으로 사용하면 돈의 방향을 바꾸는 심각한 오류가 됩니다. 또 조회 결과 크기가 2인지 확인하지 않으면 한 계좌가 없을 때 Map에서 null을 꺼내 엉뚱한 예외를 낼 수 있습니다.

실제 기술 이름lock-order versus business-role separation, HashMap<Long, Account>, exact two-account guard

12. 반대 방향 이체 시험의 fixture 값은 정확히 무엇인가요?

메서드는 SortedLockTransferIT.oppositeDirectionsFinishWithoutDeadlockAndPreserveTotal입니다. SORT-ASORT-B를 각각 10,000원으로 열고, pairs=10, tasks=pairs*2로 스무 작업을 만듭니다. A→B는 ab-0부터 ab-9, B→A는 ba-0부터 ba-9, 각 금액은 100원입니다.

ready 장벽의 크기는 20, start 장벽은 1, thread pool 크기는 20입니다. 주체는 모두 customer-1이고 두 방향 모두 같은 production TransferService.transfer를 호출합니다.

실제 기술 이름SortedLockTransferIT.oppositeDirectionsFinishWithoutDeadlockAndPreserveTotal, 10 opposite-direction pairs, 20 tasks

13. 스무 작업의 완료와 마지막 불변식은 어떻게 확인하나요?

테스트는 최대 10초 동안 스무 작업이 준비됐는지 확인한 뒤 시작 신호를 엽니다. 저장한 모든 Future<?>에 대해 하나씩 future.get(30, TimeUnit.SECONDS)를 호출하므로, 한 작업이라도 예외를 던지거나 30초를 넘기면 메서드는 Green이 되지 않습니다.

모두 끝난 뒤 두 계좌의 잔액 합이 20,000원인지 확인합니다. 이어서 entry_type LIKE 'TRANSFER_%'인 원장 행의 signed_amount 합을 구하고 COALESCE(...,0) 결과가 0인지 확인합니다. 개별 완료와 전체 돈 보존을 함께 보는 구조입니다.

실제 기술 이름bounded Future.get(30 seconds), balance conservation invariant 20,000, signed ledger sum 0

반대 방향 스무 작업과 잔액 합 이만원 원장 합 영의 영수증
그림 한눈에: 작업 20개를 모두 기다린 뒤에야 잔액 총합 20,000과 이체 원장 부호합 0을 채점한다.

14. 이 테스트가 각 계좌 10,000원과 영원한 deadlock 0을 보장하나요?

아닙니다. 같은 금액의 왕복이 모두 성공했으므로 각 계좌가 시작값으로 돌아올 것이라고 논리적으로 추론할 수 있지만, 마지막 assertion은 firstBalance + secondBalance == 20_000이지 두 값을 각각 10,000과 비교하는 두 assertion이 아닙니다.

또한 이 실행은 두 계좌, 20 task, 건당 100원, 준비 10초·각 Future 30초라는 fixture입니다. 모든 규모와 계좌 조합의 deadlock 부재, 공평한 대기, 각 요청 latency, 처리량, 실제 deadlock 발생·victim 선정·retry는 직접 보장하지 않습니다. CountDownLatch도 완전히 같은 CPU 시각을 보장하지 않습니다.

실제 기술 이름assertion boundary, per-account equality non-coverage, universal deadlock-freedom non-coverage

15. 같은 정렬 이체 시험이 세 번 나오면 실행 수와 고유 시험 수는 어떻게 세나요?

세 번째·네 번째·다섯 번째 모듈 selector는 모두 com.example.financialcore.transfer.SortedLockTransferIT입니다. 그 클래스의 @Test는 하나이므로 같은 메서드가 세 번 노출됩니다. 뒤 두 번은 새 기능이나 새 테스트가 아니라 정답과 이전 로그를 가리고 같은 계약을 다시 설명·실행하는 지연 회수입니다.

전체 여섯 모듈을 세면 LostUpdate의 서로 다른 메서드 2회, SortedLock의 같은 메서드 3회, Optimistic의 메서드 1회로 실행 노출 6회입니다. 중복을 합치면 고유 @Test는 4개입니다.

실제 기술 이름selector exposure count 6, unique @Test count 4, delayed retrieval repetition

16. W10에서 새로 들어온 시험 2개와 제공 자료 소유권은 어떻게 다른가요?

W10에 직접 새로 들어온 테스트 파일은 SortedLockTransferITOptimisticAccountIT이고 각각 @Test가 하나이므로 W10 신규 고유 시험은 2개입니다. LostUpdateBaselineIT의 두 메서드는 W9D2에서 들어온 누적 파일을 다시 회수합니다.

하지만 여섯 모듈의 gateOwnership은 모두 PROVIDED_FIXTURE_VERIFIED입니다. “W10에 새 테스트 2개가 도입됐다”는 출처 시점과 “학습자가 정답을 처음 작성해 소유했다”는 증거 분류는 다른 축입니다. 월~토 범위에서 제공 자료 검증 모듈은 6개이고 learner-authored Green은 0개입니다.

실제 기술 이름W10 direct tests 2, carried LostUpdate tests 2, PROVIDED_FIXTURE_VERIFIED

실행 노출 여섯 고유 시험 넷 W10 신규 둘을 구분한 표
그림 한눈에: 노출 6회·고유 4개·W10 신규 2개를 같은 숫자로 뭉개지 않는다.

17. 변경 도장 시험의 실제 메서드와 최종 값은 무엇인가요?

메서드는 OptimisticAccountIT.production_versioned_entity_has_one_commit_and_one_optimistic_failure입니다. 시험 전 표를 비우고 OPTIMISTIC 계좌를 10,000원으로 엽니다. 두 thread가 같은 계좌를 읽고 CountDownLatch(2)에서 서로를 기다린 뒤 각각 1,000원을 출금하고 accounts.flush()를 호출합니다.

결과 목록은 순서와 무관하게 정확히 COMMIT 1개와 OPTIMISTIC_FAILURE 1개여야 합니다. 마지막 JDBC 조회는 balance 9,000과 version 1을 직접 assertion합니다.

실제 기술 이름OptimisticAccountIT.production_versioned_entity_has_one_commit_and_one_optimistic_failure, one commit and one optimistic failure

버전 영을 함께 읽은 두 작업의 커밋 하나 충돌 하나 시간선
그림 한눈에: 같은 version 0에서 출발한 두 변경 중 하나만 version 1로 확정되고 다른 하나는 충돌로 드러난다.

18. @Version은 왜 조용한 덮어쓰기를 막을 수 있나요?

W6D2 Green solution에서 이어 온 누적 지원 파일 Account.java에는 @Version private long version 필드가 있습니다. 이것은 W10 신규 파일이나 D6 학습자 대상이라고 다시 분류하지 않습니다. JPA가 버전이 있는 엔티티를 갱신할 때는 일반적으로 읽었던 옛 버전을 UPDATE 조건에 포함하고, 성공하면 버전을 증가시킵니다. 두 작업이 모두 version 0을 읽었더라도 먼저 저장한 하나가 version 1을 만들면 둘째의 version 0 조건은 더 이상 맞지 않습니다.

둘째 변경은 성공한 것처럼 덮어쓰이지 않고 영향을 준 행이 없다는 충돌로 번역됩니다. “미리 row lock을 잡지 않는다”와 “충돌을 무시한다”는 다른 말입니다. 이 시험은 충돌을 명시적 결과로 분류합니다.

실제 기술 이름JPA @Version, optimistic locking, stale-version update detection

19. 예상된 변경 도장 충돌과 진짜 기술 실패를 어떻게 나누나요?

시험의 OutcomeCOMMIT, OPTIMISTIC_FAILURE, TECHNICAL_FAILURE 세 종류입니다. OptimisticLockingFailureException은 바로 예상 충돌로 분류합니다. 겉이 다른 RuntimeException이어도 원인 사슬의 클래스 이름에 OptimisticLock이 있는지 끝까지 따라가 같으면 예상 충돌로 분류합니다.

그 밖의 runtime 오류는 TECHNICAL_FAILURE입니다. 최종 assertion이 결과 목록을 COMMITOPTIMISTIC_FAILURE 두 개와 정확히 비교하므로 기술 실패가 하나라도 대신 들어가면 통과할 수 없습니다. 그렇다고 모든 가능한 예외 종류를 발생시켜 보았다는 뜻은 아닙니다.

실제 기술 이름Outcome classification, OptimisticLockingFailureException, optimistic cause-chain inspection

20. 변경 도장 시험에 자동 재시도나 대규모 stress가 들어 있나요?

없습니다. 각 독립 트랜잭션 timeout은 10초, 읽기 장벽은 최대 10초, 각 Future 대기는 최대 20초입니다. 코드는 충돌 결과를 반환하고 끝나며, 새 트랜잭션을 열어 최신 값을 다시 읽은 뒤 출금을 재수행하는 retry loop가 없습니다.

따라서 “충돌한 둘째 요청도 재시도로 성공해 최종 8,000원이 됐다”, “20건 또는 1,000건에서 안정성을 검증했다”, “backoff와 jitter를 구현했다”는 주장은 모두 범위 밖입니다. 그런 정책은 별도 요구사항·멱등성·부하 시험과 함께 결정해야 합니다.

실제 기술 이름retry non-implementation, two-thread optimistic fixture, stress-test non-coverage

21. 비관적 방식과 낙관적 방식 중 어느 쪽이 더 좋은가요?

이번 두 시험은 승자를 정하지 않습니다. 비관적 방식은 읽을 때 row lock을 잡아 경쟁 작업을 기다리게 하고, 이 fixture에서는 두 출금을 모두 보존해 8,000원을 만듭니다. 낙관적 방식은 두 작업이 먼저 읽도록 두되 저장 시점의 version 충돌을 드러내고, 이 fixture에서는 성공 1·충돌 1·9,000·version 1을 만듭니다.

실제 선택은 충돌 빈도, 트랜잭션 길이, 기다림 비용, 재계산 비용, 사용자 오류 계약에 달렸습니다. W10 테스트는 처리량·p95 latency·공평성·재시도 성공률을 측정하지 않으므로 성능 우열을 결론 낼 수 없습니다.

실제 기술 이름pessimistic-versus-optimistic concurrency control, contention trade-off, performance non-coverage

22. SQL Q11은 어느 표에서 시작하고 한 행은 무엇을 뜻하나요?

Q11은 앱의 V001 표를 검증하는 Java 시험이 아니라 별도 SQL workbook 문제 W10-SQL-Q11입니다. 요구사항은 거래와 고객명을 함께 조회하는 것입니다. 출력 한 행의 grain을 거래 한 건으로 유지해야 하므로 business_tx에서 시작합니다.

business_tx.account_idaccount.account_id를 찾고, 이어서 account.customer_idcustomer.customer_id를 찾습니다. 거래표에는 고객 번호가 직접 없기 때문에 account를 건너뛸 수 없습니다. 두 연결 열은 FK이고 기본 fixture에서는 모든 거래가 유효한 계좌와 고객에 연결됩니다.

실제 기술 이름W10-SQL-Q11, business_tx → account → customer, INNER JOIN, transaction-row grain

거래에서 계좌와 고객을 이어 거래 스물한 행을 유지하는 조인
그림 한눈에: 거래를 출발 행으로 보존하며 두 FK를 따라 고객 이름을 붙인다.

23. Q11의 예시 SQL과 기본 fixture 결과는 무엇인가요?

계약을 만족하는 예시는 SELECT t.tx_id, c.customer_name FROM business_tx t INNER JOIN account a ON a.account_id=t.account_id INNER JOIN customer c ON c.customer_id=a.customer_id ORDER BY t.tx_id;입니다. 필요한 출력 열은 학습자의 명시적 계약에 따라 더 넣을 수 있지만, join 방향과 거래 한 행 grain은 유지해야 합니다.

기본 workbook seed의 business_tx는 21행이고 각 거래의 두 FK가 존재하므로 이 예시 결과도 21행입니다. reference project에는 Q11 canonical learner answer 파일이 없습니다. 이 SQL은 예시 답안이며, 실제 통과 증거는 evidence/w10/sql-q11.sql에 시작 표·예상 21행·반례와 실행 결과를 함께 남겨야 합니다.

실제 기술 이름Q11 example query, expected cardinality 21, prompt-only SQL evidence

24. SQL Q12는 왜 계좌표에서 시작해야 하나요?

Q12 W10-SQL-Q12의 요구사항은 거래가 한 번도 없는 계좌입니다. 거래가 없는 행을 남겨야 하므로 모든 account를 왼쪽에 두고 business_txLEFT JOIN합니다. 그런 다음 오른쪽의 실제 PK인 t.tx_id IS NULL만 고릅니다.

business_tx에서 시작하면 거래가 없는 계좌는 애초에 입력에 나타나지 않습니다. COUNT로 바꾸는 방법도 가능하지만 group 기준과 중복을 주의해야 합니다. 가장 직접적인 anti-join은 계좌 한 행 grain을 유지하는 LEFT JOIN + NULL 필터입니다.

실제 기술 이름W10-SQL-Q12, left anti-join, LEFT JOIN plus t.tx_id IS NULL, account-row grain

거래 없는 계좌 백삼과 백사 두 행을 고르는 안티 조인
그림 한눈에: 거래가 없어 오른쪽 PK가 빈 계좌만 남기려면 계좌 여덟 행을 왼쪽 출발점으로 둔다.

25. Q12의 예시 SQL과 정확한 두 결과 계좌는 무엇인가요?

예시는 SELECT a.account_id, a.account_no FROM account a LEFT JOIN business_tx t ON t.account_id=a.account_id WHERE t.tx_id IS NULL ORDER BY a.account_id;입니다. 기본 fixture 결과는 103 / A-103, 104 / A-104 두 행입니다. 107과 108은 별도 업무 거래가 없어 보일 수 있지만 각자 OPENING 거래가 있으므로 결과에 들어가지 않습니다.

한 계좌에 거래가 여러 개 있어도 NULL 필터를 통과하지 않으므로 결과 계좌가 중복되지 않습니다. Q12 역시 canonical learner answer 파일이 없으며, evidence/w10/sql-q12.sql에 예상 2행과 중복 반례·실행 결과를 남기는 prompt-only 과제입니다.

실제 기술 이름Q12 example anti-join, expected accounts 103 and 104, expected cardinality 2

26. W10 월~토가 보장하는 것과 보장하지 않는 것을 한 번에 정리하면요?

직접 확인한 것은 네 고유 시험 계약입니다. 보호 없는 시험 전용 갱신은 commit 2·잔액 9,000·version 0, 한 행 비관적 잠금은 commit 2·잔액 8,000, 정렬 이체 fixture는 20 task·건당 100·각 Future 30초 안 완료·잔액 합 20,000·이체 장부 합 0, 변경 도장 fixture는 COMMIT 1·OPTIMISTIC_FAILURE 1·잔액 9,000·version 1입니다. SQL workbook 예시는 Q11 21행과 Q12의 103·104 두 행입니다.

직접 확인하지 않은 것은 실제 deadlock 재현과 victim 예외, 자동 retry·backoff·jitter, 모든 부하의 deadlock 부재, 각 계좌 10,000 개별 assertion, 공평함·처리량·latency, 변경 도장의 20/1,000건 stress, Q11/Q12의 제공 정본 답안입니다. 실행 노출 6회·고유 시험 4개·W10 신규 2개도 구분해야 합니다.

실제 기술 이름W10 day 1–6 guarantee matrix, assertion-driven scope, non-guarantee boundary