W21 · Linux·Docker 배포 준비
21주차 코드 뒤풀이: image 검증부터 DB-only Compose·host JVM teardown까지
완전한 packaged PowerShell source 2개와 learner-owned 참조 입력·PDF template을 출처 등급별로 분리해 읽습니다. D3가 DB-only Compose와 host JVM의 lifecycle owner라는 점, D6·D7은 그 manifest를 읽는 검토 단계라는 점을 연결하고, PDF 안의 ProjectRoot·포트·종료 방식 차이도 그대로 비교합니다. Q33·Q34는 workbook에 정답 query가 배포되지 않아 가정을 표시한 비정본 예시로만 제공합니다.
01W21 Linux 진단 골격 — process·port·health·SIGTERM
illustrative/shell/W21-linux-diagnostics.sh
학습용 예시 · PDF 학습 골격 · 정본 답안 아님 · 그대로 실행 금지 · 학습용 예시 · 정본 답안 아님 · W21-F014줄 연결4줄 번역2 chunks
W21 Linux 진단 골격 — process·port·health·SIGTERM
illustrative/shell/W21-linux-diagnostics.sh
학습용 예시 · PDF 학습 골격 · 정본 답안 아님 · 그대로 실행 금지 · 학습용 예시 · 정본 답안 아님 · W21-F01STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다.
- `java PID`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `완전한 실행 script가 아니다`과 어떻게 연결되는가?
java PIDLISTEN 8080health UPbounded SIGTERM exitSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
W21 Linux 진단 골격 — process·port·health·SIGTERM를 무대 운영표로 바꾸기
ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다.
핵심값 java PID, LISTEN 8080, health UP, bounded SIGTERM exit을 원본 줄로 따라가되, `완전한 실행 script가 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
process and listening port
1~2줄을 한 덩어리로 읽어 1번째 움직임을 본다. ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다.
- 코드 연결
1~2줄- 비유
- 가게의 직원(process), 열린 문(port), 영업 상태(health), 폐점 요청(signal)을 같은 순서로 확인하는 점검표
- 비유의 끝
- 완전한 실행 script가 아니다
health and SIGTERM
3~4줄을 한 덩어리로 읽어 2번째 움직임을 본다. ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다.
- 코드 연결
3~4줄- 비유
- 가게의 직원(process), 열린 문(port), 영업 상태(health), 폐점 요청(signal)을 같은 순서로 확인하는 점검표
- 비유의 끝
- 실제 PID를 채워야 한다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `java PID` 맞아?
-
니지카
ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다.
-
료
첫 source 값은 `java PID`로 잡자.
-
키타
단, `완전한 실행 script가 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`LISTEN 8080`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. java process를 찾는다. → 8080 LISTEN socket을 찾는다.
-
료
그 다음은 aggregate health가 응답하는지 읽는다. → 실제 PID에 SIGTERM을 보내고 종료 시간을 관찰한다.
-
키타
관찰값과 `실제 PID를 채워야 한다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 4줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F01-L01 | ps -ef | grep '[ |
직원·문·영업등·폐점 요청을 순서대로 보는 운영 점검표 | 전체 process에서 현재 java instance만 찾아 PID 확인의 출발점을 만든다.
|
| 2줄F01-L02 | ss -lntp | grep 8080 |
직원·문·영업등·폐점 요청을 순서대로 보는 운영 점검표 | LISTEN TCP socket 중 8080을 골라 실제 port open 상태를 확인한다.
|
| 3줄F01-L03 | curl -fsS http: |
직원·문·영업등·폐점 요청을 순서대로 보는 운영 점검표 | localhost aggregate health를 호출하고 HTTP 실패면 nonzero로 종료한다.
|
| 4줄F01-L04 | kill -TERM <PID> |
직원·문·영업등·폐점 요청을 순서대로 보는 운영 점검표 | 실제 PID에 SIGTERM 정상 종료 요청을 보낸다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `bounded SIGTERM exit`이야.
-
키타
`kill -9은 graceful proof가 아니다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 2개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
ps -ef | grep '[j]ava'
ss -lntp | grep 8080
curl -fsS http://localhost:8080/actuator/health
kill -TERM <PID>
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 4줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | ps -ef | grep '[j]ava' | 전체 process에서 현재 java instance만 찾아 PID 확인의 출발점을 만든다. |
| 2 | ss -lntp | grep 8080 | LISTEN TCP socket 중 8080을 골라 실제 port open 상태를 확인한다. |
| 3 | curl -fsS http://localhost:8080/actuator/health | localhost aggregate health를 호출하고 HTTP 실패면 nonzero로 종료한다. |
| 4 | kill -TERM <PID> | 실제 PID에 SIGTERM 정상 종료 요청을 보낸다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다. 다만 완전한 실행 script가 아니다
문법 해부
- pipe는 앞 명령의 출력을 grep에 넘겨 원하는 process·port만 남긴다.
- `kill -TERM`은 PID에 정상 종료 요청 signal을 보내며 강제 종료와 의미가 다르다.
실행 순서
- java process를 찾는다.
- 8080 LISTEN socket을 찾는다.
- aggregate health가 응답하는지 읽는다.
- 실제 PID에 SIGTERM을 보내고 종료 시간을 관찰한다.
W21 조각별 정밀 해설
F01-C01 · process and listening port
- 문법 해부
- 1~2줄의 `process and listening port`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `java PID`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `java PID`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 완전한 실행 script가 아니다.
- 착각 방지
- `process and listening port`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 완전한 실행 script가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F01-C02 · health and SIGTERM
- 문법 해부
- 3~4줄의 `health and SIGTERM`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `LISTEN 8080`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `LISTEN 8080`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 실제 PID를 채워야 한다.
- 착각 방지
- `health and SIGTERM`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 실제 PID를 채워야 한다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
PID는 있지만 8080이 닫혀 있을 수 있다.
-
료
왜 틀렸는지: port와 health는 별도 상태다.
-
키타
수정: ps·ss·curl을 모두 관찰한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F01-T01 실행 중 | java app + 8080 | ps·ss·curl | PID/LISTEN/UP | 세 관찰은 서로 대체하지 않는다 |
| F01-T02 정상 종료 | actual PID | kill -TERM | 유한 시간 안에 종료 | 강제 kill과 다르다 |
| F01-T03 골격 그대로 | <PID> placeholder | 복사 실행 | 실패 또는 오작동 | 실제 값과 환경을 먼저 채워야 한다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `Linux 진단 순서 학습 골격`이야.
-
료
대표 경계는 `D3의 Stop-Process -Force와 같은 증거가 아니다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
kernel이 실행 instance에 PID를 붙인다.
PID 존재만으로 요청 가능 상태는 모른다.ss가 LISTEN 상태와 port를 읽는다.
socket open만으로 dependency 정상은 모른다.SIGTERM handler와 JVM shutdown hook이 정상 종료 기회를 얻는다.
D3의 Stop-Process -Force와 같은 proof가 아니다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ process가 보이면 서비스는 정상이다
왜 틀리나 port와 health는 별도 상태다.
바르게 읽기 ps·ss·curl을 모두 관찰한다.
반례 PID는 있지만 8080이 닫혀 있을 수 있다.
❌ 8080이 열리면 내 app이다
왜 틀리나 다른 process가 선점할 수 있다.
바르게 읽기 PID와 owning process를 함께 묶는다.
반례 기존 process가 8080을 쓰면 잘못된 Green이다.
❌ kill -9도 graceful shutdown 증거다
왜 틀리나 SIGKILL은 정리 기회를 주지 않는다.
바르게 읽기 SIGTERM과 bounded exit를 기록한다.
반례 DB pool·요청 drain이 생략된다.
❌ <PID>를 그대로 실행해도 된다
왜 틀리나 placeholder는 실제 process ID가 아니다.
바르게 읽기 현재 실행에서 얻은 양의 PID로 바꾼다.
반례 shell이 syntax error를 내거나 잘못된 target을 겨냥한다.
❌ D1과 D3 종료는 같은 방식이다
왜 틀리나 D3 source는 Stop-Process -Force를 사용한다.
바르게 읽기 D1 graceful proof와 D3 cleanup proof를 분리한다.
반례 강제 cleanup으로 SIGTERM 처리 시간을 주장할 수 없다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
완전한 실행 script가 아니다
이 책임을 맡는 곳: 별도 selector/실행실제 PID를 채워야 한다
이 책임을 맡는 곳: secret·운영 정책kill -9은 graceful proof가 아니다
이 책임을 맡는 곳: concurrency·infrastructureD3의 Stop-Process -Force와 같은 증거가 아니다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: ps·ss·curl·SIGTERM을 같은 진단 순서로 묶은 PDF 학습 골격이다.
2단계 · 코드 조각 재조립
- process and listening port
- health and SIGTERM
3단계 · 파일 전체 다시 쓰기
4개 물리 줄을 원본 순서로 복원하고 SHA-256 1f906b734946f4de1c7374ae4bf5a3a4fdfb5c0e1ee92262d55a2f1f0020b839와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
ps -ef | grep '[j]ava'
ss -lntp | grep 8080
curl -fsS http://localhost:8080/actuator/health
kill -TERM <PID>
02Dockerfile — build 도구는 builder에, 실행은 UID 10001로
project/Dockerfile
학습용 예시 · 참조 프로젝트 입력 · learner-owned · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F0213줄 연결13줄 번역3 chunks
Dockerfile — build 도구는 builder에, 실행은 UID 10001로
project/Dockerfile
학습용 예시 · 참조 프로젝트 입력 · learner-owned · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F02STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
- `stage_count=2`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `multi-stage만으로 완전 격리가 아니다`과 어떻게 연결되는가?
stage_count=2COPY --from=builderUSER 10001/app/app.jarSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
Dockerfile — build 도구는 builder에, 실행은 UID 10001로를 무대 운영표로 바꾸기
Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
핵심값 stage_count=2, COPY --from=builder, USER 10001, /app/app.jar을 원본 줄로 따라가되, `multi-stage만으로 완전 격리가 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
builder base and workdir
1~2줄을 한 덩어리로 읽어 1번째 움직임을 본다. Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
- 코드 연결
1~2줄- 비유
- 공사 도구는 작업장(builder)에 남기고 완성된 jar만 매장(runtime)으로 옮긴 뒤 직원에게 제한 출입증 10001을 주는 과정
- 비유의 끝
- multi-stage만으로 완전 격리가 아니다
Gradle bootJar build
3~6줄을 한 덩어리로 읽어 2번째 움직임을 본다. Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
- 코드 연결
3~6줄- 비유
- 공사 도구는 작업장(builder)에 남기고 완성된 jar만 매장(runtime)으로 옮긴 뒤 직원에게 제한 출입증 10001을 주는 과정
- 비유의 끝
- health는 이 파일이 증명하지 않는다
non-root runtime image
7~14줄을 한 덩어리로 읽어 3번째 움직임을 본다. Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
- 코드 연결
7~14줄- 비유
- 공사 도구는 작업장(builder)에 남기고 완성된 jar만 매장(runtime)으로 옮긴 뒤 직원에게 제한 출입증 10001을 주는 과정
- 비유의 끝
- runner가 대신 생성하지 않는다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `stage_count=2` 맞아?
-
니지카
Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
-
료
첫 source 값은 `stage_count=2`로 잡자.
-
키타
단, `multi-stage만으로 완전 격리가 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`COPY --from=builder`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. JDK builder에서 project를 복사한다. → Wrapper로 clean bootJar를 만든다.
-
료
그 다음은 JRE runtime에 uid 10001 app user를 만든다. → jar를 복사하고 USER·ENTRYPOINT를 고정한다.
-
키타
관찰값과 `health는 이 파일이 증명하지 않는다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 13줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F02-L01 | FROM eclipse-temurin: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 새 Docker stage의 base image를 선택한다.
|
| 2줄F02-L02 | WORKDIR / |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 뒤 directive와 runtime command의 작업 directory를 고정한다.
|
| 3줄F02-L03 | COPY . |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner project를 builder filesystem에 복사한다.
|
| 4줄F02-L04 | RUN sed -i 's/ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Gradle Wrapper의 CRLF를 정리하고 executable로 만든 뒤 bootJar build를 시작한다.
|
| 5줄F02-L05 | && chmod + |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Gradle Wrapper에 실행 권한을 부여한다.
|
| 6줄F02-L06 | && . |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | daemon 없이 clean bootJar를 실행해 새 jar를 만든다.
|
| 8줄F02-L08 | FROM eclipse-temurin: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 새 Docker stage의 base image를 선택한다.
|
| 9줄F02-L09 | RUN useradd --system --uid 10001 app |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | runtime image에 system uid 10001 app user를 만든다.
|
| 10줄F02-L10 | WORKDIR / |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 뒤 directive와 runtime command의 작업 directory를 고정한다.
|
| 11줄F02-L11 | COPY --from= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | builder stage에서 만든 jar만 runtime stage로 복사한다.
|
| 12줄F02-L12 | USER 10001 |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 이후 runtime process의 기본 uid를 10001로 고정한다.
|
| 13줄F02-L13 | EXPOSE 8080 |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | image가 의도하는 application port를 metadata로 기록한다.
|
| 14줄F02-L14 | ENTRYPOINT [ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | container start 시 java -jar /app/app.jar를 실행한다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `/app/app.jar`이야.
-
키타
`runner가 대신 생성하지 않는다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /src
COPY . .
RUN sed -i 's/\r$//' gradlew \
&& chmod +x gradlew \
&& ./gradlew clean bootJar --no-daemon
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 10001 app
WORKDIR /app
COPY --from=builder /src/build/libs/*.jar /app/app.jar
USER 10001
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 13줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | FROM eclipse-temurin:21-jdk AS builder | 새 Docker stage의 base image를 선택한다. |
| 2 | WORKDIR /src | 뒤 directive와 runtime command의 작업 directory를 고정한다. |
| 3 | COPY . . | learner project를 builder filesystem에 복사한다. |
| 4 | RUN sed -i 's/\r$//' gradlew \ | Gradle Wrapper의 CRLF를 정리하고 executable로 만든 뒤 bootJar build를 시작한다. |
| 5 | && chmod +x gradlew \ | Gradle Wrapper에 실행 권한을 부여한다. |
| 6 | && ./gradlew clean bootJar --no-daemon | daemon 없이 clean bootJar를 실행해 새 jar를 만든다. |
| 8 | FROM eclipse-temurin:21-jre | 새 Docker stage의 base image를 선택한다. |
| 9 | RUN useradd --system --uid 10001 app | runtime image에 system uid 10001 app user를 만든다. |
| 10 | WORKDIR /app | 뒤 directive와 runtime command의 작업 directory를 고정한다. |
| 11 | COPY --from=builder /src/build/libs/*.jar /app/app.jar | builder stage에서 만든 jar만 runtime stage로 복사한다. |
| 12 | USER 10001 | 이후 runtime process의 기본 uid를 10001로 고정한다. |
| 13 | EXPOSE 8080 | image가 의도하는 application port를 metadata로 기록한다. |
| 14 | ENTRYPOINT ["java","-jar","/app/app.jar"] | container start 시 java -jar /app/app.jar를 실행한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다. 다만 multi-stage만으로 완전 격리가 아니다
문법 해부
- 두 `FROM`은 builder와 runtime이라는 서로 다른 filesystem stage를 만든다.
- `COPY --from=builder`는 첫 stage의 jar만 final stage로 가져온다.
실행 순서
- JDK builder에서 project를 복사한다.
- Wrapper로 clean bootJar를 만든다.
- JRE runtime에 uid 10001 app user를 만든다.
- jar를 복사하고 USER·ENTRYPOINT를 고정한다.
W21 조각별 정밀 해설
F02-C01 · builder base and workdir
- 문법 해부
- 1~2줄의 `builder base and workdir`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `stage_count=2`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `stage_count=2`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: multi-stage만으로 완전 격리가 아니다.
- 착각 방지
- `builder base and workdir`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: multi-stage만으로 완전 격리가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F02-C02 · Gradle bootJar build
- 문법 해부
- 3~6줄의 `Gradle bootJar build`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `COPY --from=builder`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `COPY --from=builder`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: health는 이 파일이 증명하지 않는다.
- 착각 방지
- `Gradle bootJar build`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: health는 이 파일이 증명하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F02-C03 · non-root runtime image
- 문법 해부
- 7~14줄의 `non-root runtime image`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `USER 10001`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `USER 10001`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: runner가 대신 생성하지 않는다.
- 착각 방지
- `non-root runtime image`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: runner가 대신 생성하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
실수로 source를 다시 COPY할 수 있다.
-
료
왜 틀렸는지: COPY와 layer 내용에 달려 있다.
-
키타
수정: final image path와 builder tool 부재를 probe한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F02-T01 stage | Java 21 project | builder bootJar | build/libs jar | 빌드 성공은 runtime 검증 전이다 |
| F02-T02 runtime | builder jar | COPY --from | /app/app.jar | source 전체를 옮기지 않는다 |
| F02-T03 process | image start | USER 10001 + java -jar | non-root JVM | capability/read-only까지 자동 보장하지 않는다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `learner-owned multi-stage image recipe`이야.
-
료
대표 경계는 `read-only filesystem proof가 아니다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
각 directive가 immutable layer를 만든다.
잘못 복사한 secret은 뒤 layer에서 지워도 남을 수 있다.USER가 container process의 uid를 10001로 고정한다.
non-root만으로 sandbox가 완성되지는 않는다.ENTRYPOINT가 /app/app.jar를 시작한다.
health/DB readiness는 별도 lifecycle 책임이다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ multi-stage면 source가 절대 새지 않는다
왜 틀리나 COPY와 layer 내용에 달려 있다.
바르게 읽기 final image path와 builder tool 부재를 probe한다.
반례 실수로 source를 다시 COPY할 수 있다.
❌ non-root면 image가 완전히 안전하다
왜 틀리나 권한·capability·filesystem·network 경계가 더 있다.
바르게 읽기 W21 직접 검사 범위만 말한다.
반례 writable path나 과도한 capability는 남을 수 있다.
❌ runner가 Dockerfile을 만들어 준다
왜 틀리나 runner는 learner input을 소비한다.
바르게 읽기 learner가 먼저 exact file을 작성한다.
반례 파일이 없으면 build 전에 실패한다.
❌ EXPOSE가 host port를 연다
왜 틀리나 EXPOSE는 image metadata다.
바르게 읽기 실제 publish/listen을 별도로 확인한다.
반례 container start만으로 host 8080이 열리지 않는다.
❌ JDK가 final image에도 필요하다
왜 틀리나 runtime은 이미 만든 jar만 실행한다.
바르게 읽기 JRE stage로 builder tool을 떼어낸다.
반례 Gradle·javac가 final image에 있으면 검사 실패다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
multi-stage만으로 완전 격리가 아니다
이 책임을 맡는 곳: 별도 selector/실행health는 이 파일이 증명하지 않는다
이 책임을 맡는 곳: secret·운영 정책runner가 대신 생성하지 않는다
이 책임을 맡는 곳: concurrency·infrastructureread-only filesystem proof가 아니다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: Gradle builder와 JRE runtime을 분리하고 final image를 UID 10001로 실행하는 learner-owned Dockerfile이다.
2단계 · 코드 조각 재조립
- builder base and workdir
- Gradle bootJar build
- non-root runtime image
3단계 · 파일 전체 다시 쓰기
14개 물리 줄을 원본 순서로 복원하고 SHA-256 047be48ded4d1ed05471fe106e106723e2a9f28dc78f2fa11e31c93b6d9e207a와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /src
COPY . .
RUN sed -i 's/\r$//' gradlew \
&& chmod +x gradlew \
&& ./gradlew clean bootJar --no-daemon
FROM eclipse-temurin:21-jre
RUN useradd --system --uid 10001 app
WORKDIR /app
COPY --from=builder /src/build/libs/*.jar /app/app.jar
USER 10001
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]
03.dockerignore — build context에서 증거·환경 파일 제외
project/.dockerignore
학습용 예시 · 참조 프로젝트 입력 · learner-owned · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F037줄 연결7줄 번역2 chunks
.dockerignore — build context에서 증거·환경 파일 제외
project/.dockerignore
학습용 예시 · 참조 프로젝트 입력 · learner-owned · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F03STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다.
- `.git excluded`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `secret manager가 아니다`과 어떻게 연결되는가?
.git excludedbuild excludedevidence excluded*.env/.env excludedSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
.dockerignore — build context에서 증거·환경 파일 제외를 무대 운영표로 바꾸기
VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다.
핵심값 .git excluded, build excluded, evidence excluded, *.env/.env excluded을 원본 줄로 따라가되, `secret manager가 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
VCS cache build evidence exclusions
1~4줄을 한 덩어리로 읽어 1번째 움직임을 본다. VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다.
- 코드 연결
1~4줄- 비유
- 이삿짐 상자에 넣지 말아야 할 `.git`·증거·환경 파일 목록을 문 앞에 붙이는 규칙
- 비유의 끝
- secret manager가 아니다
environment and log exclusions
5~7줄을 한 덩어리로 읽어 2번째 움직임을 본다. VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다.
- 코드 연결
5~7줄- 비유
- 이삿짐 상자에 넣지 말아야 할 `.git`·증거·환경 파일 목록을 문 앞에 붙이는 규칙
- 비유의 끝
- 이미 image layer에 들어간 secret을 지우지 않는다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `.git excluded` 맞아?
-
니지카
VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다.
-
료
첫 source 값은 `.git excluded`로 잡자.
-
키타
단, `secret manager가 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`build excluded`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. build context root를 정한다. → ignore pattern과 path를 비교한다.
-
료
그 다음은 일치 파일을 daemon 전송에서 뺀다. → 남은 context로 Dockerfile COPY를 수행한다.
-
키타
관찰값과 `이미 image layer에 들어간 secret을 지우지 않는다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 7줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F03-L01 | . |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `.git` pattern에 맞는 path를 제외한다.
|
| 2줄F03-L02 | . |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `.gradle` pattern에 맞는 path를 제외한다.
|
| 3줄F03-L03 | build |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `build` pattern에 맞는 path를 제외한다.
|
| 4줄F03-L04 | evidence |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `evidence` pattern에 맞는 path를 제외한다.
|
| 5줄F03-L05 | * |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `*.env` pattern에 맞는 path를 제외한다.
|
| 6줄F03-L06 | . |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `.env` pattern에 맞는 path를 제외한다.
|
| 7줄F03-L07 | * |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Docker build context에서 `*.log` pattern에 맞는 path를 제외한다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `*.env/.env excluded`이야.
-
키타
`runner는 일부 exact pattern만 검사한다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 2개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
.git
.gradle
build
evidence
*.env
.env
*.log
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 7줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | .git | Docker build context에서 `.git` pattern에 맞는 path를 제외한다. |
| 2 | .gradle | Docker build context에서 `.gradle` pattern에 맞는 path를 제외한다. |
| 3 | build | Docker build context에서 `build` pattern에 맞는 path를 제외한다. |
| 4 | evidence | Docker build context에서 `evidence` pattern에 맞는 path를 제외한다. |
| 5 | *.env | Docker build context에서 `*.env` pattern에 맞는 path를 제외한다. |
| 6 | .env | Docker build context에서 `.env` pattern에 맞는 path를 제외한다. |
| 7 | *.log | Docker build context에서 `*.log` pattern에 맞는 path를 제외한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다. 다만 secret manager가 아니다
문법 해부
- 각 줄은 Docker build context에서 제외할 path 또는 glob pattern이다.
- `*.env`와 `.env`는 이름 모양이 다르므로 둘 다 적는다.
실행 순서
- build context root를 정한다.
- ignore pattern과 path를 비교한다.
- 일치 파일을 daemon 전송에서 뺀다.
- 남은 context로 Dockerfile COPY를 수행한다.
W21 조각별 정밀 해설
F03-C01 · VCS cache build evidence exclusions
- 문법 해부
- 1~4줄의 `VCS cache build evidence exclusions`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `.git excluded`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `.git excluded`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: secret manager가 아니다.
- 착각 방지
- `VCS cache build evidence exclusions`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: secret manager가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F03-C02 · environment and log exclusions
- 문법 해부
- 5~7줄의 `environment and log exclusions`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `build excluded`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `build excluded`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 이미 image layer에 들어간 secret을 지우지 않는다.
- 착각 방지
- `environment and log exclusions`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 이미 image layer에 들어간 secret을 지우지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
Git history의 유출값은 남는다.
-
료
왜 틀렸는지: 전송 제외 목록일 뿐이다.
-
키타
수정: secret은 외부 secret store·trusted terminal에서 공급한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F03-T01 Git | .git | exact exclusion | context 밖 | VCS secret 전체를 대체하지 않는다 |
| F03-T02 증거 | evidence | directory exclusion | learner proof 밖 | 이미 복사된 layer는 별도 문제다 |
| F03-T03 환경 | prod.env/.env | glob+exact exclusion | context 밖 | 노출됐으면 rotate가 먼저다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `Docker build-context 제외 규칙`이야.
-
료
대표 경계는 `노출 secret은 revoke/rotate가 먼저다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
ignore를 적용한 context archive를 만든다.
pattern이 빠진 파일은 daemon으로 전송될 수 있다.context 변화가 COPY cache key에 영향을 준다.
ignore가 secret lifecycle을 관리하지 않는다.runner가 /app 금지 경로 부재를 다시 확인한다.
모든 filesystem path를 전수 검사하지는 않는다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ .dockerignore는 secret manager다
왜 틀리나 전송 제외 목록일 뿐이다.
바르게 읽기 secret은 외부 secret store·trusted terminal에서 공급한다.
반례 Git history의 유출값은 남는다.
❌ *.env면 .env도 자동 포함된다
왜 틀리나 dotfile/exact name matching을 따로 고정하는 편이 안전하다.
바르게 읽기 두 pattern을 모두 둔다.
반례 `.env`가 context에 들어갈 수 있다.
❌ ignore 뒤에는 source 검사가 필요 없다
왜 틀리나 Dockerfile이 다른 path를 복사할 수 있다.
바르게 읽기 final image forbidden-path probe를 함께 둔다.
반례 /app/evidence가 남으면 실패다.
❌ build directory는 넣어도 빠르다
왜 틀리나 오래된 jar가 섞일 수 있다.
바르게 읽기 build를 제외하고 container 안에서 새로 만든다.
반례 로컬 stale artifact가 final jar가 될 수 있다.
❌ 노출 secret은 ignore 추가로 해결된다
왜 틀리나 이미 노출된 값은 회수되지 않는다.
바르게 읽기 먼저 revoke/rotate하고 history/image를 조사한다.
반례 기존 token은 계속 유효할 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
secret manager가 아니다
이 책임을 맡는 곳: 별도 selector/실행이미 image layer에 들어간 secret을 지우지 않는다
이 책임을 맡는 곳: secret·운영 정책runner는 일부 exact pattern만 검사한다
이 책임을 맡는 곳: concurrency·infrastructure노출 secret은 revoke/rotate가 먼저다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: VCS·build 산출물·evidence·환경 파일·log를 Docker build context에서 제외한다.
2단계 · 코드 조각 재조립
- VCS cache build evidence exclusions
- environment and log exclusions
3단계 · 파일 전체 다시 쓰기
7개 물리 줄을 원본 순서로 복원하고 SHA-256 75468239aae8dcb6a4344c8322494e83a3550c89689254491588bc9352c95c2a와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
.git
.gradle
build
evidence
*.env
.env
*.log
04run-w21-image.ps1 — image build·검사·hash evidence owner
scripts/run-w21-image.ps1
원문 정본 · 완전한 packaged PowerShell source · 정본 · W21-F0485줄 연결85줄 번역5 chunks
run-w21-image.ps1 — image build·검사·hash evidence owner
scripts/run-w21-image.ps1
원문 정본 · 완전한 packaged PowerShell source · 정본 · W21-F04STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
- `W21_IMAGE_GREEN`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `Dockerfile을 생성하지 않는다`과 어떻게 연결되는가?
W21_IMAGE_GREENruntime_uid=10001builder_tools_absent=truenative_exit=0STEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
run-w21-image.ps1 — image build·검사·hash evidence owner를 무대 운영표로 바꾸기
learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
핵심값 W21_IMAGE_GREEN, runtime_uid=10001, builder_tools_absent=true, native_exit=0을 원본 줄로 따라가되, `Dockerfile을 생성하지 않는다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
parameters and learner inputs
1~16줄을 한 덩어리로 읽어 1번째 움직임을 본다. learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
- 코드 연결
1~16줄- 비유
- 세관 검사원이 learner 상자를 완성한 뒤 신분증·금지 물품·내용물 hash를 확인하고 learner-owned 영수증에 통과 도장을 찍는 과정
- 비유의 끝
- Dockerfile을 생성하지 않는다
static Dockerfile and ignore guards
17~28줄을 한 덩어리로 읽어 2번째 움직임을 본다. learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
- 코드 연결
17~28줄- 비유
- 세관 검사원이 learner 상자를 완성한 뒤 신분증·금지 물품·내용물 hash를 확인하고 learner-owned 영수증에 통과 도장을 찍는 과정
- 비유의 끝
- Compose/DB lifecycle을 실행하지 않는다
build inspect and runtime probes
29~48줄을 한 덩어리로 읽어 3번째 움직임을 본다. learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
- 코드 연결
29~48줄- 비유
- 세관 검사원이 learner 상자를 완성한 뒤 신분증·금지 물품·내용물 hash를 확인하고 learner-owned 영수증에 통과 도장을 찍는 과정
- 비유의 끝
- application health를 증명하지 않는다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `W21_IMAGE_GREEN` 맞아?
-
니지카
learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
-
료
첫 source 값은 `W21_IMAGE_GREEN`로 잡자.
-
키타
단, `Dockerfile을 생성하지 않는다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`runtime_uid=10001`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. absolute path·필수 파일·placeholder를 검증한다. → Dockerfile stage/USER와 ignore pattern을 정적으로 검사한다.
-
료
그 다음은 image build 뒤 user·image id·runtime uid·금지 경로를 probe한다. → JAR/source hash evidence를 temporary file에 쓴 뒤 atomic replace하고 cleanup한다.
-
키타
관찰값과 `Compose/DB lifecycle을 실행하지 않는다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 85줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F04-L01 | param( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | runner의 mandatory/optional PowerShell parameter 계약을 연다.
|
| 2줄F04-L02 | [ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | caller가 반드시 넘겨야 할 absolute learner/evidence path parameter를 선언한다.
|
| 3줄F04-L03 | [ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | caller가 반드시 넘겨야 할 absolute learner/evidence path parameter를 선언한다.
|
| 4줄F04-L04 | [ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 검사할 local image tag 기본값을 선언한다.
|
| 5줄F04-L05 | ) |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
| 6줄F04-L06 | $ErrorActionPreference= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | PowerShell cmdlet error를 즉시 terminating error로 바꾼다.
|
| 7줄F04-L07 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 상대 path를 거절해 learner root/evidence owner를 명확히 한다.
|
| 8줄F04-L08 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 상대 path를 거절해 learner root/evidence owner를 명확히 한다.
|
| 9줄F04-L09 | $project= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner project path를 resolve해 canonical absolute path로 만든다.
|
| 10줄F04-L10 | $target= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | evidence target을 normalized absolute path로 만든다.
|
| 11줄F04-L11 | $wrapper= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner Gradle Wrapper path를 계산한다.
|
| 12줄F04-L12 | $buildFile= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner build.gradle path를 계산한다.
|
| 13줄F04-L13 | $dockerfile= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner Dockerfile path를 계산한다.
|
| 14줄F04-L14 | $dockerignore= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner .dockerignore path를 계산한다.
|
| 15줄F04-L15 | foreach( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 모든 필수 learner input file이 실제 leaf인지 검사한다.
|
| 16줄F04-L16 | $dockerfileBody= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Dockerfile raw text를 static guard 입력으로 읽는다.
|
| 17줄F04-L17 | $dockerignoreBody= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | .dockerignore raw text를 static guard 입력으로 읽는다.
|
| 18줄F04-L18 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 미해결 TODO/placeholder가 있으면 build 전에 중단한다.
|
| 19줄F04-L19 | $fromCount= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | Dockerfile의 FROM directive 수를 계산한다.
|
| 20줄F04-L20 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | builder/runtime 최소 두 stage가 아니면 거절한다.
|
| 21줄F04-L21 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | COPY --from 또는 exact USER 10001 계약이 없으면 거절한다.
|
| 22줄F04-L22 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | COPY --from 또는 exact USER 10001 계약이 없으면 거절한다.
|
| 23줄F04-L23 | foreach( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 필수 ignore token 네 개를 하나씩 검사한다.
|
| 24줄F04-L24 | $pattern= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 token의 exact-line regex pattern을 만든다.
|
| 25줄F04-L25 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 ignore token이 없으면 fail-closed로 중단한다.
|
| 26줄F04-L26 | } |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
| 27줄F04-L27 | Push-Location $project |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | native docker/Gradle 상대 path 기준을 learner root로 바꾼다.
|
| 28줄F04-L28 | $createdContainer= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | finally cleanup에 사용할 inspection container ID를 초기화한다.
|
| 29줄F04-L29 | $copiedJar= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | finally cleanup에 사용할 임시 jar path를 초기화한다.
|
| 30줄F04-L30 | try{ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | build·inspect·evidence 단계와 반드시 실행할 cleanup 범위를 연다.
|
| 31줄F04-L31 | & docker build --file $dockerfile --tag $Tag $project |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 검증된 Dockerfile로 learner image를 build한다.
|
| 32줄F04-L32 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 33줄F04-L33 | $user= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | image Config.User 값을 docker inspect로 읽는다.
|
| 34줄F04-L34 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 35줄F04-L35 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 35번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 36줄F04-L36 | $imageId= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | immutable image ID를 inspect해 sha256 형태로 읽는다.
|
| 37줄F04-L37 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 38줄F04-L38 | $runtimeUid= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 실제 container shell의 `id -u`를 읽는다.
|
| 39줄F04-L39 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 40줄F04-L40 | & docker run --rm --entrypoint sh $Tag -c 'test ! -e / |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | final image에 금지 path와 builder tool이 없는지 probe한다.
|
| 41줄F04-L41 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 42줄F04-L42 | $createdContainer= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | finally cleanup에 사용할 inspection container ID를 초기화한다.
|
| 43줄F04-L43 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 44줄F04-L44 | $copiedJar= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | finally cleanup에 사용할 임시 jar path를 초기화한다.
|
| 45줄F04-L45 | & docker cp "${ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | final image의 /app/app.jar를 host temporary file로 복사한다.
|
| 46줄F04-L46 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 바로 앞 native docker command가 실패했으면 즉시 중단한다.
|
| 47줄F04-L47 | $jarHash= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 실제 packaged jar SHA-256을 계산한다.
|
| 48줄F04-L48 | $buildHash= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 learner build.gradle SHA-256을 계산한다.
|
| 49줄F04-L49 | $dockerfileHash= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 learner Dockerfile SHA-256을 계산한다.
|
| 50줄F04-L50 | $dockerignoreHash= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 learner .dockerignore SHA-256을 계산한다.
|
| 51줄F04-L51 | New-Item -ItemType Directory -Force ( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | learner-owned evidence parent directory가 없으면 만든다.
|
| 52줄F04-L52 | $tmp= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 부분 receipt가 final path에 보이지 않도록 temporary path를 정한다.
|
| 53줄F04-L53 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | evidence extension에 따라 JSON 또는 text receipt 형식을 선택한다.
|
| 54줄F04-L54 | [ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 검사 결과와 source/JAR/image hash를 ordered JSON receipt에 담는다.
|
| 55줄F04-L55 | }else{ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 뒤의 선언·검증 문장을 묶을 block을 연다.
|
| 56줄F04-L56 | @( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | text evidence에 쓸 안정된 marker line 배열을 연다.
|
| 57줄F04-L57 | '# W21 Docker image evidence', |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 57번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 58줄F04-L58 | 'status: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 58번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 59줄F04-L59 | "image: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 59번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 60줄F04-L60 | "image_id: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 60번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 61줄F04-L61 | "user: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 61번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 62줄F04-L62 | 'source: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 62번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 63줄F04-L63 | "build_gradle_sha256: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 63번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 64줄F04-L64 | "jar_sha256: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 64번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 65줄F04-L65 | "dockerfile_sha256: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 65번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 66줄F04-L66 | "dockerignore_sha256: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 66번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 67줄F04-L67 | "stage_count: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 67번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 68줄F04-L68 | "runtime_uid: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 68번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 69줄F04-L69 | 'builder_tools_absent: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 69번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 70줄F04-L70 | 'forbidden_paths_absent: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 70번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 71줄F04-L71 | 'native_exit: |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 71번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 72줄F04-L72 | )|Set-Content -Encoding utf8 $tmp |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 72번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 73줄F04-L73 | } |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
| 74줄F04-L74 | Move-Item -Force -LiteralPath $tmp -Destination $target |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 완성 temporary evidence를 final target으로 atomic replace한다.
|
| 75줄F04-L75 | $saved= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 방금 쓴 evidence를 다시 읽어 marker를 재검증한다.
|
| 76줄F04-L76 | foreach( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 필수 marker/hash/value가 저장본에 모두 있는지 검사한다.
|
| 77줄F04-L77 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 정본 image evidence producer에서 77번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 78줄F04-L78 | } |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
| 79줄F04-L79 | $hash= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | final evidence file 자체 SHA-256을 계산한다.
|
| 80줄F04-L80 | "W21_IMAGE_GREEN user= |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | image evidence 성공 marker와 native exit 0을 출력한다.
|
| 81줄F04-L81 | }finally{ |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 성공·실패와 무관하게 임시 resource cleanup을 시작한다.
|
| 82줄F04-L82 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 만든 inspection container만 강제로 제거한다.
|
| 83줄F04-L83 | if( |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 복사한 temporary jar가 있으면 제거한다.
|
| 84줄F04-L84 | Pop-Location |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | caller의 원래 working directory를 복원한다.
|
| 85줄F04-L85 | } |
공사장과 완성 매장을 분리해 포장·검수하는 image 체크리스트 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `native_exit=0`이야.
-
키타
`application health를 증명하지 않는다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 5개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
param(
[Parameter(Mandatory=$true)][string]$ProjectRoot,
[Parameter(Mandatory=$true)][string]$EvidencePath,
[string]$Tag='financial-core:local'
)
$ErrorActionPreference='Stop'
if(-not[IO.Path]::IsPathRooted($ProjectRoot)){throw 'ProjectRoot must be an absolute learner project path'}
if(-not[IO.Path]::IsPathRooted($EvidencePath)){throw 'EvidencePath must be an absolute learner-owned path'}
$project=(Resolve-Path -LiteralPath $ProjectRoot).Path
$target=[IO.Path]::GetFullPath($EvidencePath)
$wrapper=Join-Path $project 'gradlew.bat'
$buildFile=Join-Path $project 'build.gradle'
$dockerfile=Join-Path $project 'Dockerfile'
$dockerignore=Join-Path $project '.dockerignore'
foreach($path in @($wrapper,$buildFile,$dockerfile,$dockerignore)){if(!(Test-Path -LiteralPath $path -PathType Leaf)){throw "W21 learner build input missing: $path"}}
$dockerfileBody=Get-Content -Raw -LiteralPath $dockerfile
$dockerignoreBody=Get-Content -Raw -LiteralPath $dockerignore
if($dockerfileBody-cmatch'(?i)TODO|TBD|FILL_ME|PLACEHOLDER|REPLACE_WITH_'){throw 'W21 Dockerfile contains an unresolved placeholder'}
$fromCount=([regex]::Matches($dockerfileBody,'(?im)^\s*FROM\s+')).Count
if($fromCount-lt2){throw "W21 Dockerfile must contain builder and runtime stages actual=$fromCount"}
if($dockerfileBody-cnotmatch'(?im)^\s*COPY\s+--from=[^\s]+\s+' ){throw 'W21 Dockerfile must copy the jar from the builder stage'}
if($dockerfileBody-cnotmatch'(?im)^\s*USER\s+10001\s*$'){throw 'W21 runtime stage must declare USER 10001'}
foreach($token in @('.git','evidence','build','*.env')){
$pattern='(?im)^\s*'+[regex]::Escape($token)+'(?:\s*|/?)$'
if($dockerignoreBody-cnotmatch$pattern){throw "W21 .dockerignore missing exact exclusion: $token"}
}
Push-Location $project
$createdContainer=''
$copiedJar=''
try{
& docker build --file $dockerfile --tag $Tag $project
if($LASTEXITCODE-ne 0){throw "W21 docker build exit=$LASTEXITCODE"}
$user=((& docker inspect $Tag --format '{{.Config.User}}')-join'').Trim()
if($LASTEXITCODE-ne 0){throw "W21 docker inspect exit=$LASTEXITCODE"}
if($user-cne'10001'){throw "W21 image user must be 10001 actual=$user"}
$imageId=((& docker inspect $Tag --format '{{.Id}}')-join'').Trim()
if($LASTEXITCODE-ne0-or$imageId-cnotmatch'^sha256:[0-9a-f]{64}$'){throw "W21 invalid image id: $imageId"}
$runtimeUid=((& docker run --rm --entrypoint sh $Tag -c 'id -u')-join'').Trim()
if($LASTEXITCODE-ne0-or$runtimeUid-cne'10001'){throw "W21 runtime uid mismatch actual=$runtimeUid"}
& docker run --rm --entrypoint sh $Tag -c 'test ! -e /app/.git -a ! -e /app/evidence -a ! -e /app/.env; ! command -v gradle >/dev/null 2>&1; ! command -v javac >/dev/null 2>&1'
if($LASTEXITCODE-ne0){throw 'W21 final image leaks source/evidence/secret or builder tools'}
$createdContainer=((& docker create $Tag)-join'').Trim()
if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($createdContainer)){throw 'W21 could not create image inspection container'}
$copiedJar=Join-Path ([IO.Path]::GetTempPath()) ("w21-app-"+[guid]::NewGuid().ToString('N')+'.jar')
& docker cp "${createdContainer}:/app/app.jar" $copiedJar
if($LASTEXITCODE-ne0-or!(Test-Path -LiteralPath $copiedJar -PathType Leaf)){throw 'W21 runtime app.jar extraction failed'}
$jarHash=(Get-FileHash -Algorithm SHA256 $copiedJar).Hash.ToLowerInvariant()
$buildHash=(Get-FileHash -Algorithm SHA256 $buildFile).Hash.ToLowerInvariant()
$dockerfileHash=(Get-FileHash -Algorithm SHA256 $dockerfile).Hash.ToLowerInvariant()
$dockerignoreHash=(Get-FileHash -Algorithm SHA256 $dockerignore).Hash.ToLowerInvariant()
New-Item -ItemType Directory -Force (Split-Path -Parent $target)|Out-Null
$tmp="$target.tmp"
if([IO.Path]::GetExtension($target)-ieq'.json'){
[ordered]@{status='W21_IMAGE_GREEN';source='LEARNER_PROJECT';image=$Tag;image_id=$imageId;user=$user;stage_count=$fromCount;runtime_uid=$runtimeUid;builder_tools_absent=$true;forbidden_paths_absent=$true;build_gradle_sha256=$buildHash;jar_sha256=$jarHash;dockerfile_sha256=$dockerfileHash;dockerignore_sha256=$dockerignoreHash;native_exit=0}|ConvertTo-Json|Set-Content -Encoding utf8 $tmp
}else{
@(
'# W21 Docker image evidence',
'status: W21_IMAGE_GREEN',
"image: $Tag",
"image_id: $imageId",
"user: $user",
'source: LEARNER_PROJECT',
"build_gradle_sha256: $buildHash",
"jar_sha256: $jarHash",
"dockerfile_sha256: $dockerfileHash",
"dockerignore_sha256: $dockerignoreHash",
"stage_count: $fromCount",
"runtime_uid: $runtimeUid",
'builder_tools_absent: true',
'forbidden_paths_absent: true',
'native_exit: 0'
)|Set-Content -Encoding utf8 $tmp
}
Move-Item -Force -LiteralPath $tmp -Destination $target
$saved=Get-Content -Raw -LiteralPath $target
foreach($marker in @('W21_IMAGE_GREEN','LEARNER_PROJECT',$Tag,$imageId,'10001',$fromCount,$buildHash,$jarHash,$dockerfileHash,$dockerignoreHash,'native_exit')){
if($saved-cnotmatch[regex]::Escape([string]$marker)){throw "W21 evidence marker missing: $marker"}
}
$hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
"W21_IMAGE_GREEN user=10001 image_id=$imageId evidence_sha256=$hash native_exit=0"
}finally{
if($createdContainer){& docker rm -f $createdContainer 2>$null|Out-Null}
if($copiedJar-and(Test-Path -LiteralPath $copiedJar)){Remove-Item -LiteralPath $copiedJar -Force}
Pop-Location
}
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 85줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | param( | runner의 mandatory/optional PowerShell parameter 계약을 연다. |
| 2 | [Parameter(Mandatory=$true)][string]$ProjectRoot, | caller가 반드시 넘겨야 할 absolute learner/evidence path parameter를 선언한다. |
| 3 | [Parameter(Mandatory=$true)][string]$EvidencePath, | caller가 반드시 넘겨야 할 absolute learner/evidence path parameter를 선언한다. |
| 4 | [string]$Tag='financial-core:local' | 검사할 local image tag 기본값을 선언한다. |
| 5 | ) | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
| 6 | $ErrorActionPreference='Stop' | PowerShell cmdlet error를 즉시 terminating error로 바꾼다. |
| 7 | if(-not[IO.Path]::IsPathRooted($ProjectRoot)){throw 'ProjectRoot must be an absolute learner project path'} | 상대 path를 거절해 learner root/evidence owner를 명확히 한다. |
| 8 | if(-not[IO.Path]::IsPathRooted($EvidencePath)){throw 'EvidencePath must be an absolute learner-owned path'} | 상대 path를 거절해 learner root/evidence owner를 명확히 한다. |
| 9 | $project=(Resolve-Path -LiteralPath $ProjectRoot).Path | learner project path를 resolve해 canonical absolute path로 만든다. |
| 10 | $target=[IO.Path]::GetFullPath($EvidencePath) | evidence target을 normalized absolute path로 만든다. |
| 11 | $wrapper=Join-Path $project 'gradlew.bat' | learner Gradle Wrapper path를 계산한다. |
| 12 | $buildFile=Join-Path $project 'build.gradle' | learner build.gradle path를 계산한다. |
| 13 | $dockerfile=Join-Path $project 'Dockerfile' | learner Dockerfile path를 계산한다. |
| 14 | $dockerignore=Join-Path $project '.dockerignore' | learner .dockerignore path를 계산한다. |
| 15 | foreach($path in @($wrapper,$buildFile,$dockerfile,$dockerignore)){if(!(Test-Path -LiteralPath $path -PathType Leaf)){throw "W21 learner build input missing: $path"}} | 모든 필수 learner input file이 실제 leaf인지 검사한다. |
| 16 | $dockerfileBody=Get-Content -Raw -LiteralPath $dockerfile | Dockerfile raw text를 static guard 입력으로 읽는다. |
| 17 | $dockerignoreBody=Get-Content -Raw -LiteralPath $dockerignore | .dockerignore raw text를 static guard 입력으로 읽는다. |
| 18 | if($dockerfileBody-cmatch'(?i)TODO|TBD|FILL_ME|PLACEHOLDER|REPLACE_WITH_'){throw 'W21 Dockerfile contains an unresolved placeholder'} | 미해결 TODO/placeholder가 있으면 build 전에 중단한다. |
| 19 | $fromCount=([regex]::Matches($dockerfileBody,'(?im)^\s*FROM\s+')).Count | Dockerfile의 FROM directive 수를 계산한다. |
| 20 | if($fromCount-lt2){throw "W21 Dockerfile must contain builder and runtime stages actual=$fromCount"} | builder/runtime 최소 두 stage가 아니면 거절한다. |
| 21 | if($dockerfileBody-cnotmatch'(?im)^\s*COPY\s+--from=[^\s]+\s+' ){throw 'W21 Dockerfile must copy the jar from the builder stage'} | COPY --from 또는 exact USER 10001 계약이 없으면 거절한다. |
| 22 | if($dockerfileBody-cnotmatch'(?im)^\s*USER\s+10001\s*$'){throw 'W21 runtime stage must declare USER 10001'} | COPY --from 또는 exact USER 10001 계약이 없으면 거절한다. |
| 23 | foreach($token in @('.git','evidence','build','*.env')){ | 필수 ignore token 네 개를 하나씩 검사한다. |
| 24 | $pattern='(?im)^\s*'+[regex]::Escape($token)+'(?:\s*|/?)$' | 현재 token의 exact-line regex pattern을 만든다. |
| 25 | if($dockerignoreBody-cnotmatch$pattern){throw "W21 .dockerignore missing exact exclusion: $token"} | 현재 ignore token이 없으면 fail-closed로 중단한다. |
| 26 | } | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
| 27 | Push-Location $project | native docker/Gradle 상대 path 기준을 learner root로 바꾼다. |
| 28 | $createdContainer='' | finally cleanup에 사용할 inspection container ID를 초기화한다. |
| 29 | $copiedJar='' | finally cleanup에 사용할 임시 jar path를 초기화한다. |
| 30 | try{ | build·inspect·evidence 단계와 반드시 실행할 cleanup 범위를 연다. |
| 31 | & docker build --file $dockerfile --tag $Tag $project | 검증된 Dockerfile로 learner image를 build한다. |
| 32 | if($LASTEXITCODE-ne 0){throw "W21 docker build exit=$LASTEXITCODE"} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 33 | $user=((& docker inspect $Tag --format '{{.Config.User}}')-join'').Trim() | image Config.User 값을 docker inspect로 읽는다. |
| 34 | if($LASTEXITCODE-ne 0){throw "W21 docker inspect exit=$LASTEXITCODE"} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 35 | if($user-cne'10001'){throw "W21 image user must be 10001 actual=$user"} | 정본 image evidence producer에서 35번째 원문 문장을 앞뒤 단계와 연결한다. |
| 36 | $imageId=((& docker inspect $Tag --format '{{.Id}}')-join'').Trim() | immutable image ID를 inspect해 sha256 형태로 읽는다. |
| 37 | if($LASTEXITCODE-ne0-or$imageId-cnotmatch'^sha256:[0-9a-f]{64}$'){throw "W21 invalid image id: $imageId"} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 38 | $runtimeUid=((& docker run --rm --entrypoint sh $Tag -c 'id -u')-join'').Trim() | 실제 container shell의 `id -u`를 읽는다. |
| 39 | if($LASTEXITCODE-ne0-or$runtimeUid-cne'10001'){throw "W21 runtime uid mismatch actual=$runtimeUid"} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 40 | & docker run --rm --entrypoint sh $Tag -c 'test ! -e /app/.git -a ! -e /app/evidence -a ! -e /app/.env; ! command -v gradle >/dev/null 2>&1; ! command -v javac >/dev/null 2>&1' | final image에 금지 path와 builder tool이 없는지 probe한다. |
| 41 | if($LASTEXITCODE-ne0){throw 'W21 final image leaks source/evidence/secret or builder tools'} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 42 | $createdContainer=((& docker create $Tag)-join'').Trim() | finally cleanup에 사용할 inspection container ID를 초기화한다. |
| 43 | if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($createdContainer)){throw 'W21 could not create image inspection container'} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 44 | $copiedJar=Join-Path ([IO.Path]::GetTempPath()) ("w21-app-"+[guid]::NewGuid().ToString('N')+'.jar') | finally cleanup에 사용할 임시 jar path를 초기화한다. |
| 45 | & docker cp "${createdContainer}:/app/app.jar" $copiedJar | final image의 /app/app.jar를 host temporary file로 복사한다. |
| 46 | if($LASTEXITCODE-ne0-or!(Test-Path -LiteralPath $copiedJar -PathType Leaf)){throw 'W21 runtime app.jar extraction failed'} | 바로 앞 native docker command가 실패했으면 즉시 중단한다. |
| 47 | $jarHash=(Get-FileHash -Algorithm SHA256 $copiedJar).Hash.ToLowerInvariant() | 실제 packaged jar SHA-256을 계산한다. |
| 48 | $buildHash=(Get-FileHash -Algorithm SHA256 $buildFile).Hash.ToLowerInvariant() | 현재 learner build.gradle SHA-256을 계산한다. |
| 49 | $dockerfileHash=(Get-FileHash -Algorithm SHA256 $dockerfile).Hash.ToLowerInvariant() | 현재 learner Dockerfile SHA-256을 계산한다. |
| 50 | $dockerignoreHash=(Get-FileHash -Algorithm SHA256 $dockerignore).Hash.ToLowerInvariant() | 현재 learner .dockerignore SHA-256을 계산한다. |
| 51 | New-Item -ItemType Directory -Force (Split-Path -Parent $target)|Out-Null | learner-owned evidence parent directory가 없으면 만든다. |
| 52 | $tmp="$target.tmp" | 부분 receipt가 final path에 보이지 않도록 temporary path를 정한다. |
| 53 | if([IO.Path]::GetExtension($target)-ieq'.json'){ | evidence extension에 따라 JSON 또는 text receipt 형식을 선택한다. |
| 54 | [ordered]@{status='W21_IMAGE_GREEN';source='LEARNER_PROJECT';image=$Tag;image_id=$imageId;user=$user;stage_count=$fromCount;runtime_uid=$runtimeUid;builder_tools_absent=$true;forbidden_paths_absent=$true;build_gradle_sha256=$buildHash;jar_sha256=$jarHash;dockerfile_sha256=$dockerfileHash;dockerignore_sha256=$dockerignoreHash;native_exit=0}|ConvertTo-Json|Set-Content -Encoding utf8 $tmp | 검사 결과와 source/JAR/image hash를 ordered JSON receipt에 담는다. |
| 55 | }else{ | 뒤의 선언·검증 문장을 묶을 block을 연다. |
| 56 | @( | text evidence에 쓸 안정된 marker line 배열을 연다. |
| 57 | '# W21 Docker image evidence', | 정본 image evidence producer에서 57번째 원문 문장을 앞뒤 단계와 연결한다. |
| 58 | 'status: W21_IMAGE_GREEN', | 정본 image evidence producer에서 58번째 원문 문장을 앞뒤 단계와 연결한다. |
| 59 | "image: $Tag", | 정본 image evidence producer에서 59번째 원문 문장을 앞뒤 단계와 연결한다. |
| 60 | "image_id: $imageId", | 정본 image evidence producer에서 60번째 원문 문장을 앞뒤 단계와 연결한다. |
| 61 | "user: $user", | 정본 image evidence producer에서 61번째 원문 문장을 앞뒤 단계와 연결한다. |
| 62 | 'source: LEARNER_PROJECT', | 정본 image evidence producer에서 62번째 원문 문장을 앞뒤 단계와 연결한다. |
| 63 | "build_gradle_sha256: $buildHash", | 정본 image evidence producer에서 63번째 원문 문장을 앞뒤 단계와 연결한다. |
| 64 | "jar_sha256: $jarHash", | 정본 image evidence producer에서 64번째 원문 문장을 앞뒤 단계와 연결한다. |
| 65 | "dockerfile_sha256: $dockerfileHash", | 정본 image evidence producer에서 65번째 원문 문장을 앞뒤 단계와 연결한다. |
| 66 | "dockerignore_sha256: $dockerignoreHash", | 정본 image evidence producer에서 66번째 원문 문장을 앞뒤 단계와 연결한다. |
| 67 | "stage_count: $fromCount", | 정본 image evidence producer에서 67번째 원문 문장을 앞뒤 단계와 연결한다. |
| 68 | "runtime_uid: $runtimeUid", | 정본 image evidence producer에서 68번째 원문 문장을 앞뒤 단계와 연결한다. |
| 69 | 'builder_tools_absent: true', | 정본 image evidence producer에서 69번째 원문 문장을 앞뒤 단계와 연결한다. |
| 70 | 'forbidden_paths_absent: true', | 정본 image evidence producer에서 70번째 원문 문장을 앞뒤 단계와 연결한다. |
| 71 | 'native_exit: 0' | 정본 image evidence producer에서 71번째 원문 문장을 앞뒤 단계와 연결한다. |
| 72 | )|Set-Content -Encoding utf8 $tmp | 정본 image evidence producer에서 72번째 원문 문장을 앞뒤 단계와 연결한다. |
| 73 | } | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
| 74 | Move-Item -Force -LiteralPath $tmp -Destination $target | 완성 temporary evidence를 final target으로 atomic replace한다. |
| 75 | $saved=Get-Content -Raw -LiteralPath $target | 방금 쓴 evidence를 다시 읽어 marker를 재검증한다. |
| 76 | foreach($marker in @('W21_IMAGE_GREEN','LEARNER_PROJECT',$Tag,$imageId,'10001',$fromCount,$buildHash,$jarHash,$dockerfileHash,$dockerignoreHash,'native_exit')){ | 필수 marker/hash/value가 저장본에 모두 있는지 검사한다. |
| 77 | if($saved-cnotmatch[regex]::Escape([string]$marker)){throw "W21 evidence marker missing: $marker"} | 정본 image evidence producer에서 77번째 원문 문장을 앞뒤 단계와 연결한다. |
| 78 | } | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
| 79 | $hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() | final evidence file 자체 SHA-256을 계산한다. |
| 80 | "W21_IMAGE_GREEN user=10001 image_id=$imageId evidence_sha256=$hash native_exit=0" | image evidence 성공 marker와 native exit 0을 출력한다. |
| 81 | }finally{ | 성공·실패와 무관하게 임시 resource cleanup을 시작한다. |
| 82 | if($createdContainer){& docker rm -f $createdContainer 2>$null|Out-Null} | 만든 inspection container만 강제로 제거한다. |
| 83 | if($copiedJar-and(Test-Path -LiteralPath $copiedJar)){Remove-Item -LiteralPath $copiedJar -Force} | 복사한 temporary jar가 있으면 제거한다. |
| 84 | Pop-Location | caller의 원래 working directory를 복원한다. |
| 85 | } | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다. 다만 Dockerfile을 생성하지 않는다
문법 해부
- PowerShell native command 뒤 `$LASTEXITCODE`를 즉시 읽어 실패를 fail-closed로 바꾼다.
- `try/finally`는 검사 중 실패해도 임시 container와 JAR를 정리한다.
실행 순서
- absolute path·필수 파일·placeholder를 검증한다.
- Dockerfile stage/USER와 ignore pattern을 정적으로 검사한다.
- image build 뒤 user·image id·runtime uid·금지 경로를 probe한다.
- JAR/source hash evidence를 temporary file에 쓴 뒤 atomic replace하고 cleanup한다.
W21 조각별 정밀 해설
F04-C01 · parameters and learner inputs
- 문법 해부
- 1~16줄의 `parameters and learner inputs`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `W21_IMAGE_GREEN`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `W21_IMAGE_GREEN`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: Dockerfile을 생성하지 않는다.
- 착각 방지
- `parameters and learner inputs`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: Dockerfile을 생성하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F04-C02 · static Dockerfile and ignore guards
- 문법 해부
- 17~28줄의 `static Dockerfile and ignore guards`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `runtime_uid=10001`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `runtime_uid=10001`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: Compose/DB lifecycle을 실행하지 않는다.
- 착각 방지
- `static Dockerfile and ignore guards`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: Compose/DB lifecycle을 실행하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F04-C03 · build inspect and runtime probes
- 문법 해부
- 29~48줄의 `build inspect and runtime probes`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `builder_tools_absent=true`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `builder_tools_absent=true`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: application health를 증명하지 않는다.
- 착각 방지
- `build inspect and runtime probes`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: application health를 증명하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F04-C04 · JAR and source hash receipt
- 문법 해부
- 49~64줄의 `JAR and source hash receipt`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `native_exit=0`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `native_exit=0`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: cloud 배포 Green이 아니다.
- 착각 방지
- `JAR and source hash receipt`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: cloud 배포 Green이 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F04-C05 · atomic publish marker and cleanup
- 문법 해부
- 65~85줄의 `atomic publish marker and cleanup`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `W21_IMAGE_GREEN`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `W21_IMAGE_GREEN`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: Dockerfile을 생성하지 않는다.
- 착각 방지
- `atomic publish marker and cleanup`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: Dockerfile을 생성하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
root image도 build는 성공한다.
-
료
왜 틀렸는지: user·tool·path·jar hash는 아직 모른다.
-
키타
수정: 성공 뒤 inspect/run/cp를 순서대로 한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F04-T01 input guard | ProjectRoot/EvidencePath | absolute+file checks | 검사 가능 path | learner input을 생성하지 않는다 |
| F04-T02 image | Dockerfile/.dockerignore | build+inspect+run | uid10001·tools/path absent | health는 보지 않는다 |
| F04-T03 receipt | image/JAR/source hashes | tmp -> target | W21_IMAGE_GREEN | marker는 Docker 범위만 닫는다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `정본 image evidence producer`이야.
-
료
대표 경계는 `cloud 배포 Green이 아니다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
strict errors와 native exit checks가 제어 흐름을 중단한다.
cmdlet 성공과 docker native 성공을 섞으면 안 된다.build·inspect·run·create·cp가 서로 다른 관찰을 제공한다.
한 단계 성공이 다음 단계 성공을 보장하지 않는다.learner source와 extracted jar hash를 같은 receipt에 묶는다.
receipt hash가 learner authorship을 대신하지 않는다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ docker build 0이면 모든 검증이 끝난다
왜 틀리나 user·tool·path·jar hash는 아직 모른다.
바르게 읽기 성공 뒤 inspect/run/cp를 순서대로 한다.
반례 root image도 build는 성공한다.
❌ inspect를 build 실패 뒤에도 해도 된다
왜 틀리나 오래된 같은 tag를 읽을 수 있다.
바르게 읽기 failed build 즉시 throw한다.
반례 이전 image가 거짓 Green을 만든다.
❌ non-root user 문자열만 보면 충분하다
왜 틀리나 runtime 실제 uid도 확인해야 한다.
바르게 읽기 Config.User와 `id -u`를 둘 다 본다.
반례 entrypoint가 user를 바꿀 수 있다.
❌ evidence 파일이 있으면 현재 source와 같다
왜 틀리나 stale receipt일 수 있다.
바르게 읽기 현재 build/Dockerfile/.dockerignore hash를 저장·재대조한다.
반례 파일만 남은 이전 실행이 통과처럼 보인다.
❌ W21_IMAGE_GREEN은 app health Green이다
왜 틀리나 runner는 app을 health probe하지 않는다.
바르게 읽기 D3 host lifecycle과 분리한다.
반례 DB·API가 죽어도 image 검사는 통과할 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
Dockerfile을 생성하지 않는다
이 책임을 맡는 곳: 별도 selector/실행Compose/DB lifecycle을 실행하지 않는다
이 책임을 맡는 곳: secret·운영 정책application health를 증명하지 않는다
이 책임을 맡는 곳: concurrency·infrastructurecloud 배포 Green이 아니다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: learner Docker input을 검증해 image를 build·inspect하고 JAR/source hash가 묶인 evidence를 atomic하게 만든다.
2단계 · 코드 조각 재조립
- parameters and learner inputs
- static Dockerfile and ignore guards
- build inspect and runtime probes
- JAR and source hash receipt
- atomic publish marker and cleanup
3단계 · 파일 전체 다시 쓰기
85개 물리 줄을 원본 순서로 복원하고 SHA-256 023c9ed2cabb937e6804c26344f650efbb7b8a64635157375fa16c29ee1bbdef와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
원문 정본 전체 source 확인하기
param(
[Parameter(Mandatory=$true)][string]$ProjectRoot,
[Parameter(Mandatory=$true)][string]$EvidencePath,
[string]$Tag='financial-core:local'
)
$ErrorActionPreference='Stop'
if(-not[IO.Path]::IsPathRooted($ProjectRoot)){throw 'ProjectRoot must be an absolute learner project path'}
if(-not[IO.Path]::IsPathRooted($EvidencePath)){throw 'EvidencePath must be an absolute learner-owned path'}
$project=(Resolve-Path -LiteralPath $ProjectRoot).Path
$target=[IO.Path]::GetFullPath($EvidencePath)
$wrapper=Join-Path $project 'gradlew.bat'
$buildFile=Join-Path $project 'build.gradle'
$dockerfile=Join-Path $project 'Dockerfile'
$dockerignore=Join-Path $project '.dockerignore'
foreach($path in @($wrapper,$buildFile,$dockerfile,$dockerignore)){if(!(Test-Path -LiteralPath $path -PathType Leaf)){throw "W21 learner build input missing: $path"}}
$dockerfileBody=Get-Content -Raw -LiteralPath $dockerfile
$dockerignoreBody=Get-Content -Raw -LiteralPath $dockerignore
if($dockerfileBody-cmatch'(?i)TODO|TBD|FILL_ME|PLACEHOLDER|REPLACE_WITH_'){throw 'W21 Dockerfile contains an unresolved placeholder'}
$fromCount=([regex]::Matches($dockerfileBody,'(?im)^\s*FROM\s+')).Count
if($fromCount-lt2){throw "W21 Dockerfile must contain builder and runtime stages actual=$fromCount"}
if($dockerfileBody-cnotmatch'(?im)^\s*COPY\s+--from=[^\s]+\s+' ){throw 'W21 Dockerfile must copy the jar from the builder stage'}
if($dockerfileBody-cnotmatch'(?im)^\s*USER\s+10001\s*$'){throw 'W21 runtime stage must declare USER 10001'}
foreach($token in @('.git','evidence','build','*.env')){
$pattern='(?im)^\s*'+[regex]::Escape($token)+'(?:\s*|/?)$'
if($dockerignoreBody-cnotmatch$pattern){throw "W21 .dockerignore missing exact exclusion: $token"}
}
Push-Location $project
$createdContainer=''
$copiedJar=''
try{
& docker build --file $dockerfile --tag $Tag $project
if($LASTEXITCODE-ne 0){throw "W21 docker build exit=$LASTEXITCODE"}
$user=((& docker inspect $Tag --format '{{.Config.User}}')-join'').Trim()
if($LASTEXITCODE-ne 0){throw "W21 docker inspect exit=$LASTEXITCODE"}
if($user-cne'10001'){throw "W21 image user must be 10001 actual=$user"}
$imageId=((& docker inspect $Tag --format '{{.Id}}')-join'').Trim()
if($LASTEXITCODE-ne0-or$imageId-cnotmatch'^sha256:[0-9a-f]{64}$'){throw "W21 invalid image id: $imageId"}
$runtimeUid=((& docker run --rm --entrypoint sh $Tag -c 'id -u')-join'').Trim()
if($LASTEXITCODE-ne0-or$runtimeUid-cne'10001'){throw "W21 runtime uid mismatch actual=$runtimeUid"}
& docker run --rm --entrypoint sh $Tag -c 'test ! -e /app/.git -a ! -e /app/evidence -a ! -e /app/.env; ! command -v gradle >/dev/null 2>&1; ! command -v javac >/dev/null 2>&1'
if($LASTEXITCODE-ne0){throw 'W21 final image leaks source/evidence/secret or builder tools'}
$createdContainer=((& docker create $Tag)-join'').Trim()
if($LASTEXITCODE-ne0-or[string]::IsNullOrWhiteSpace($createdContainer)){throw 'W21 could not create image inspection container'}
$copiedJar=Join-Path ([IO.Path]::GetTempPath()) ("w21-app-"+[guid]::NewGuid().ToString('N')+'.jar')
& docker cp "${createdContainer}:/app/app.jar" $copiedJar
if($LASTEXITCODE-ne0-or!(Test-Path -LiteralPath $copiedJar -PathType Leaf)){throw 'W21 runtime app.jar extraction failed'}
$jarHash=(Get-FileHash -Algorithm SHA256 $copiedJar).Hash.ToLowerInvariant()
$buildHash=(Get-FileHash -Algorithm SHA256 $buildFile).Hash.ToLowerInvariant()
$dockerfileHash=(Get-FileHash -Algorithm SHA256 $dockerfile).Hash.ToLowerInvariant()
$dockerignoreHash=(Get-FileHash -Algorithm SHA256 $dockerignore).Hash.ToLowerInvariant()
New-Item -ItemType Directory -Force (Split-Path -Parent $target)|Out-Null
$tmp="$target.tmp"
if([IO.Path]::GetExtension($target)-ieq'.json'){
[ordered]@{status='W21_IMAGE_GREEN';source='LEARNER_PROJECT';image=$Tag;image_id=$imageId;user=$user;stage_count=$fromCount;runtime_uid=$runtimeUid;builder_tools_absent=$true;forbidden_paths_absent=$true;build_gradle_sha256=$buildHash;jar_sha256=$jarHash;dockerfile_sha256=$dockerfileHash;dockerignore_sha256=$dockerignoreHash;native_exit=0}|ConvertTo-Json|Set-Content -Encoding utf8 $tmp
}else{
@(
'# W21 Docker image evidence',
'status: W21_IMAGE_GREEN',
"image: $Tag",
"image_id: $imageId",
"user: $user",
'source: LEARNER_PROJECT',
"build_gradle_sha256: $buildHash",
"jar_sha256: $jarHash",
"dockerfile_sha256: $dockerfileHash",
"dockerignore_sha256: $dockerignoreHash",
"stage_count: $fromCount",
"runtime_uid: $runtimeUid",
'builder_tools_absent: true',
'forbidden_paths_absent: true',
'native_exit: 0'
)|Set-Content -Encoding utf8 $tmp
}
Move-Item -Force -LiteralPath $tmp -Destination $target
$saved=Get-Content -Raw -LiteralPath $target
foreach($marker in @('W21_IMAGE_GREEN','LEARNER_PROJECT',$Tag,$imageId,'10001',$fromCount,$buildHash,$jarHash,$dockerfileHash,$dockerignoreHash,'native_exit')){
if($saved-cnotmatch[regex]::Escape([string]$marker)){throw "W21 evidence marker missing: $marker"}
}
$hash=(Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
"W21_IMAGE_GREEN user=10001 image_id=$imageId evidence_sha256=$hash native_exit=0"
}finally{
if($createdContainer){& docker rm -f $createdContainer 2>$null|Out-Null}
if($copiedJar-and(Test-Path -LiteralPath $copiedJar)){Remove-Item -LiteralPath $copiedJar -Force}
Pop-Location
}
05compose.yaml — PostgreSQL db 하나만 소유하는 Compose
project/compose.yaml
학습용 예시 · 참조 프로젝트 입력 · learner-owned · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F0518줄 연결18줄 번역4 chunks
compose.yaml — PostgreSQL db 하나만 소유하는 Compose
project/compose.yaml
학습용 예시 · 참조 프로젝트 입력 · learner-owned · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F05STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
- `service=db only`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `application container는 없다`과 어떻게 연결되는가?
service=db onlypostgres:17.10-alpinepg_isreadyfinancial-core-dbSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
compose.yaml — PostgreSQL db 하나만 소유하는 Compose를 무대 운영표로 바꾸기
W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
핵심값 service=db only, postgres:17.10-alpine, pg_isready, financial-core-db을 원본 줄로 따라가되, `application container는 없다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
db service image and port
1~4줄을 한 덩어리로 읽어 1번째 움직임을 본다. W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
- 코드 연결
1~4줄- 비유
- 무대 뒤 창고는 DB 하나만 열고 배우인 application은 host 무대에서 따로 서게 하는 운영 배치표
- 비유의 끝
- application container는 없다
database environment
5~9줄을 한 덩어리로 읽어 2번째 움직임을 본다. W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
- 코드 연결
5~9줄- 비유
- 무대 뒤 창고는 DB 하나만 열고 배우인 application은 host 무대에서 따로 서게 하는 운영 배치표
- 비유의 끝
- FCL_DB_PASSWORD가 필요하다
readiness healthcheck
10~14줄을 한 덩어리로 읽어 3번째 움직임을 본다. W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
- 코드 연결
10~14줄- 비유
- 무대 뒤 창고는 DB 하나만 열고 배우인 application은 host 무대에서 따로 서게 하는 운영 배치표
- 비유의 끝
- host port는 환경 변수로 정한다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `service=db only` 맞아?
-
니지카
W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
-
료
첫 source 값은 `service=db only`로 잡자.
-
키타
단, `application container는 없다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`postgres:17.10-alpine`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. exact service 이름 db를 읽는다. → PostgreSQL image와 host-port mapping을 정한다.
-
료
그 다음은 DB/user/password를 환경에서 주입한다. → pg_isready healthcheck와 named volume을 연결한다.
-
키타
관찰값과 `FCL_DB_PASSWORD가 필요하다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 18줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F05-L01 | services: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | Compose service map을 연다.
|
| 2줄F05-L02 | db: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | W21이 소유할 유일한 db service를 선언한다.
|
| 3줄F05-L03 | image: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | PostgreSQL 17.10 Alpine image를 고정한다.
|
| 4줄F05-L04 | ports: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | host-container port mapping 목록을 연다.
|
| 5줄F05-L05 | - "${ |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | 환경 변수 host port를 container 5432에 publish하고 없으면 5432를 쓴다.
|
| 6줄F05-L06 | environment: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | PostgreSQL process에 전달할 DB/user/password 환경 값을 연다.
|
| 7줄F05-L07 | POSTGRES_DB: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | database 이름을 financial_core로 고정한다.
|
| 8줄F05-L08 | POSTGRES_USER: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | application DB user를 app으로 고정한다.
|
| 9줄F05-L09 | POSTGRES_PASSWORD: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | 현재 terminal의 필수 FCL_DB_PASSWORD를 전달한다.
|
| 10줄F05-L10 | healthcheck: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | DB readiness를 판단할 container healthcheck를 연다.
|
| 11줄F05-L11 | test: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | pg_isready로 app@financial_core 접속 수락 상태를 검사한다.
|
| 12줄F05-L12 | interval: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | healthcheck 반복 간격을 2초로 정한다.
|
| 13줄F05-L13 | timeout: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | 한 번의 healthcheck timeout을 2초로 정한다.
|
| 14줄F05-L14 | retries: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | 최대 30번 재시도해 startup 시간을 허용한다.
|
| 15줄F05-L15 | volumes: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | service 또는 project volume mapping을 연다.
|
| 16줄F05-L16 | - financial-core-db: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | PostgreSQL data용 named volume을 선언하거나 연결한다.
|
| 18줄F05-L18 | volumes: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | service 또는 project volume mapping을 연다.
|
| 19줄F05-L19 | financial-core-db: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | PostgreSQL data용 named volume을 선언하거나 연결한다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `financial-core-db`이야.
-
키타
`host port는 환경 변수로 정한다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
services:
db:
image: postgres:17.10-alpine
ports:
- "${FCL_DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: financial_core
POSTGRES_USER: app
POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- financial-core-db:/var/lib/postgresql/data
volumes:
financial-core-db:
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 18줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | services: | Compose service map을 연다. |
| 2 | db: | W21이 소유할 유일한 db service를 선언한다. |
| 3 | image: postgres:17.10-alpine | PostgreSQL 17.10 Alpine image를 고정한다. |
| 4 | ports: | host-container port mapping 목록을 연다. |
| 5 | - "${FCL_DB_PORT:-5432}:5432" | 환경 변수 host port를 container 5432에 publish하고 없으면 5432를 쓴다. |
| 6 | environment: | PostgreSQL process에 전달할 DB/user/password 환경 값을 연다. |
| 7 | POSTGRES_DB: financial_core | database 이름을 financial_core로 고정한다. |
| 8 | POSTGRES_USER: app | application DB user를 app으로 고정한다. |
| 9 | POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal} | 현재 terminal의 필수 FCL_DB_PASSWORD를 전달한다. |
| 10 | healthcheck: | DB readiness를 판단할 container healthcheck를 연다. |
| 11 | test: ["CMD-SHELL", "pg_isready -U app -d financial_core"] | pg_isready로 app@financial_core 접속 수락 상태를 검사한다. |
| 12 | interval: 2s | healthcheck 반복 간격을 2초로 정한다. |
| 13 | timeout: 2s | 한 번의 healthcheck timeout을 2초로 정한다. |
| 14 | retries: 30 | 최대 30번 재시도해 startup 시간을 허용한다. |
| 15 | volumes: | service 또는 project volume mapping을 연다. |
| 16 | - financial-core-db:/var/lib/postgresql/data | PostgreSQL data용 named volume을 선언하거나 연결한다. |
| 18 | volumes: | service 또는 project volume mapping을 연다. |
| 19 | financial-core-db: | PostgreSQL data용 named volume을 선언하거나 연결한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다. 다만 application container는 없다
문법 해부
- Compose YAML indentation이 service·environment·healthcheck·volume 소속을 정한다.
- `${NAME:?message}`는 필수 환경 변수가 없으면 config 단계에서 중단시킨다.
실행 순서
- exact service 이름 db를 읽는다.
- PostgreSQL image와 host-port mapping을 정한다.
- DB/user/password를 환경에서 주입한다.
- pg_isready healthcheck와 named volume을 연결한다.
W21 조각별 정밀 해설
F05-C01 · db service image and port
- 문법 해부
- 1~4줄의 `db service image and port`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `service=db only`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `service=db only`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: application container는 없다.
- 착각 방지
- `db service image and port`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: application container는 없다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F05-C02 · database environment
- 문법 해부
- 5~9줄의 `database environment`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `postgres:17.10-alpine`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `postgres:17.10-alpine`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: FCL_DB_PASSWORD가 필요하다.
- 착각 방지
- `database environment`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: FCL_DB_PASSWORD가 필요하다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F05-C03 · readiness healthcheck
- 문법 해부
- 10~14줄의 `readiness healthcheck`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `pg_isready`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `pg_isready`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: host port는 환경 변수로 정한다.
- 착각 방지
- `readiness healthcheck`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: host port는 환경 변수로 정한다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F05-C04 · named database volume
- 문법 해부
- 15~19줄의 `named database volume`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `financial-core-db`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `financial-core-db`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: production HA 구성이 아니다.
- 착각 방지
- `named database volume`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: production HA 구성이 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
application container claim은 실패다.
-
료
왜 틀렸는지: exact service는 db 하나다.
-
키타
수정: app은 host Gradle bootRun으로 실행한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F05-T01 config | FCL_DB_PASSWORD/FCL_DB_PORT | Compose interpolation | db service spec | password를 파일에 쓰지 않는다 |
| F05-T02 startup | postgres image | up --wait + pg_isready | ready db | app은 container가 아니다 |
| F05-T03 teardown | Compose project | down -v | owned container/volume absent | 다른 사용자 volume까지 지우지 않는다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `reference DB-only Compose dependency`이야.
-
료
대표 경계는 `production HA 구성이 아니다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
service가 db 하나인지 config --services로 확인할 수 있다.
파일 존재만으로 실행 상태는 모른다.pg_isready가 접속 수락 가능성을 health로 보고한다.
schema migration·business query는 별도다.named volume이 DB data directory를 보존한다.
D3 disposable lifecycle은 own project volume을 제거한다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ Compose가 app도 띄운다
왜 틀리나 exact service는 db 하나다.
바르게 읽기 app은 host Gradle bootRun으로 실행한다.
반례 application container claim은 실패다.
❌ 5432를 고정해도 된다
왜 틀리나 host port 충돌 위험이 있다.
바르게 읽기 FCL_DB_PORT로 disposable port를 주입한다.
반례 기존 PostgreSQL이 선점할 수 있다.
❌ healthcheck면 schema도 준비됐다
왜 틀리나 pg_isready는 연결 수락만 본다.
바르게 읽기 migration/health/API를 뒤에서 확인한다.
반례 테이블이 없는데 DB health는 healthy일 수 있다.
❌ down만 하면 volume도 사라진다
왜 틀리나 `-v`가 없으면 named volume이 남는다.
바르게 읽기 owned lifecycle에서 down -v와 absent를 확인한다.
반례 stale data가 다음 실행을 오염시킨다.
❌ 비밀번호 기본값을 YAML에 넣으면 편하다
왜 틀리나 source/history에 secret이 남는다.
바르게 읽기 필수 환경 변수로 fail-fast한다.
반례 공유 repo에 credential이 노출된다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
application container는 없다
이 책임을 맡는 곳: 별도 selector/실행FCL_DB_PASSWORD가 필요하다
이 책임을 맡는 곳: secret·운영 정책host port는 환경 변수로 정한다
이 책임을 맡는 곳: concurrency·infrastructureproduction HA 구성이 아니다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: W21 lifecycle이 소유하는 PostgreSQL db service 하나와 healthcheck·전용 volume을 정의한다.
2단계 · 코드 조각 재조립
- db service image and port
- database environment
- readiness healthcheck
- named database volume
3단계 · 파일 전체 다시 쓰기
19개 물리 줄을 원본 순서로 복원하고 SHA-256 3ef6ca229dfa8d54120fb66f2215b906ba8155784d32791d7f93f3be879ade8d와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
services:
db:
image: postgres:17.10-alpine
ports:
- "${FCL_DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: financial_core
POSTGRES_USER: app
POSTGRES_PASSWORD: ${FCL_DB_PASSWORD:?set FCL_DB_PASSWORD in this terminal}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d financial_core"]
interval: 2s
timeout: 2s
retries: 30
volumes:
- financial-core-db:/var/lib/postgresql/data
volumes:
financial-core-db:
06run-w21-host-app.ps1 — D3 DB+host JVM lifecycle owner
scripts/run-w21-host-app.ps1
원문 정본 · 완전한 packaged PowerShell source · 정본 · W21-F066줄 연결6줄 번역4 chunks
run-w21-host-app.ps1 — D3 DB+host JVM lifecycle owner
scripts/run-w21-host-app.ps1
원문 정본 · 완전한 packaged PowerShell source · 정본 · W21-F06STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
- `compose_services=[db]`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `app container proof가 아니다`과 어떻게 연결되는가?
compose_services=[db]runtime=host JVMhealth=UPteardown=1container/volume/process absentSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
run-w21-host-app.ps1 — D3 DB+host JVM lifecycle owner를 무대 운영표로 바꾸기
Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
핵심값 compose_services=[db], runtime=host JVM, health=UP, teardown=1, container/volume/process absent을 원본 줄로 따라가되, `app container proof가 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
parameters and packaged dependency
1~2줄을 한 덩어리로 읽어 1번째 움직임을 본다. Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
- 코드 연결
1~2줄- 비유
- 공연 감독이 창고(DB)만 Compose로 열고 배우(app)는 host에서 출연시킨 뒤, 공연 종료 후 배우·창고·짐을 모두 철수하고 manifest를 남기는 한 번의 lifecycle
- 비유의 끝
- app container proof가 아니다
host JVM environment and project scope
3~4줄을 한 덩어리로 읽어 2번째 움직임을 본다. Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
- 코드 연결
3~4줄- 비유
- 공연 감독이 창고(DB)만 Compose로 열고 배우(app)는 host에서 출연시킨 뒤, 공연 종료 후 배우·창고·짐을 모두 철수하고 manifest를 남기는 한 번의 lifecycle
- 비유의 끝
- API 200/401은 authorization proof가 아니다
D3 lifecycle try finally
5~5줄을 한 덩어리로 읽어 3번째 움직임을 본다. Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
- 코드 연결
5~5줄- 비유
- 공연 감독이 창고(DB)만 Compose로 열고 배우(app)는 host에서 출연시킨 뒤, 공연 종료 후 배우·창고·짐을 모두 철수하고 manifest를 남기는 한 번의 lifecycle
- 비유의 끝
- Stop-Process -Force는 graceful SIGTERM proof가 아니다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `compose_services=[db]` 맞아?
-
니지카
Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
-
료
첫 source 값은 `compose_services=[db]`로 잡자.
-
키타
단, `app container proof가 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`runtime=host JVM`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. db service exactness와 readiness를 확인한다. → host JVM을 bootRun하고 aggregate health·listen PID·API 200/401을 관찰한다.
-
료
그 다음은 finally에서 process와 Compose project/volume을 정리한다. → 세 absent invariant 뒤에만 host-app-manifest.json과 W21_HOST_APP_GREEN을 만든다.
-
키타
관찰값과 `API 200/401은 authorization proof가 아니다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 6줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F06-L01 | param( |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | runner의 mandatory/optional PowerShell parameter 계약을 연다.
|
| 2줄F06-L02 | $ErrorActionPreference= |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | PowerShell cmdlet error를 즉시 terminating error로 바꾼다.
|
| 3줄F06-L03 | $env: |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | disposable DB secret/port와 Spring datasource, host server port를 현재 process 환경에 주입한다.
|
| 4줄F06-L04 | Push-Location $root |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | native docker/Gradle 상대 path 기준을 learner root로 바꾼다.
|
| 5줄F06-L05 | try{ |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | D3의 핵심 try/catch/finally 한 줄이다. exact db 확인, DB ready, host bootRun, health/listen/API 관찰, process·container·volume cleanup을 한 owner lifecycle로 묶는다.
|
| 6줄F06-L06 | if( |
DB 창고와 host JVM 배우를 한 감독이 열고 닫는 lifecycle 장부 | cleanup exit와 세 absent 값을 fail-closed로 확인한 뒤 manifest를 atomic replace하고 W21_HOST_APP_GREEN marker를 출력한다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `container/volume/process absent`이야.
-
키타
`Stop-Process -Force는 graceful SIGTERM proof가 아니다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 hash로 고정한 native 정본 source에서 그대로 잘랐습니다.
param([Parameter(Mandatory=$true)][string]$ProjectRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[string]$ComposeProject='w21-learner-host',[int]$Port=28021,[int]$DbPort=62121,[switch]$ForceFailureAfterCompose)
$ErrorActionPreference='Stop';$root=(Resolve-Path -LiteralPath $ProjectRoot).Path;$referenceRoot=Split-Path -Parent $PSScriptRoot;$e=[IO.Path]::GetFullPath($EvidenceDir);$compose=Join-Path $referenceRoot 'compose.yaml';if(!(Test-Path -LiteralPath $compose)){throw 'W21 packaged DB-only compose.yaml missing'};New-Item -ItemType Directory -Force $e|Out-Null;$proc=$null;$appPid=$null;$body=$null;$caught=$null
$env:FCL_DB_PASSWORD='w21-disposable-password';$env:FCL_DB_PORT="$DbPort";$env:SPRING_DATASOURCE_URL="jdbc:postgresql://localhost:$DbPort/financial_core";$env:SPRING_DATASOURCE_USERNAME='app';$env:SPRING_DATASOURCE_PASSWORD=$env:FCL_DB_PASSWORD;$env:SERVER_PORT="$Port"
Push-Location $root
try{$services=@(& docker compose -f $compose -p $ComposeProject config --services);if($LASTEXITCODE-ne 0-or$services.Count-ne 1-or$services[0]-cne'db'){throw "W21 compose must be exact db: $services"};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw 'W21 db readiness failed'};if($ForceFailureAfterCompose){throw 'W21 forced partial-startup failure'};$stdout=Join-Path $e 'host-app.stdout.log';$stderr=Join-Path $e 'host-app.stderr.log';$proc=Start-Process -FilePath (Join-Path $root 'gradlew.bat') -ArgumentList @('bootRun','--no-daemon') -WorkingDirectory $root -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr;$health=$null;for($i=0;$i-lt 90;$i++){if($proc.HasExited){throw "host app exited=$($proc.ExitCode)"};try{$health=Invoke-RestMethod "http://localhost:$Port/actuator/health" -TimeoutSec 2;if($health.status-eq'UP'){break}}catch{};Start-Sleep 1};if(!$health-or$health.status-ne'UP'){throw 'host health never UP'};$listen=Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue;if(!$listen){throw "host port $Port not listening"};$appPid=[int]$listen[0].OwningProcess;$apiStatus=0;try{Invoke-WebRequest "http://localhost:$Port/api/accounts/1" -TimeoutSec 5|Out-Null;$apiStatus=200}catch{$apiStatus=[int]$_.Exception.Response.StatusCode};if($apiStatus-notin@(200,401)){throw "unexpected API status=$apiStatus"};$body=[ordered]@{compose_services=@($services);compose_project=$ComposeProject;runtime='host JVM';pid=$appPid;port=$Port;health='UP';api_status=$apiStatus;native_exit=0}}catch{$caught=$_}finally{if($appPid){Stop-Process -Id $appPid -Force -ErrorAction SilentlyContinue};if($proc-and!$proc.HasExited){Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue};& docker compose -f $compose -p $ComposeProject down -v|Out-Null;$down=$LASTEXITCODE;$containers=@(& docker compose -f $compose -p $ComposeProject ps -q);$containerAbsent=($LASTEXITCODE-eq 0-and$containers.Count-eq 0);$volumes=@(& docker volume ls -q --filter "name=^${ComposeProject}_financial-core-db$");$volumeAbsent=($LASTEXITCODE-eq 0-and$volumes.Count-eq 0);$processAbsent=if($appPid){-not[bool](Get-Process -Id $appPid -ErrorAction SilentlyContinue)}else{$true};Pop-Location;Remove-Item Env:FCL_DB_PASSWORD,Env:FCL_DB_PORT,Env:SPRING_DATASOURCE_URL,Env:SPRING_DATASOURCE_USERNAME,Env:SPRING_DATASOURCE_PASSWORD,Env:SERVER_PORT -ErrorAction SilentlyContinue}
if($down-ne 0-or!$containerAbsent-or!$volumeAbsent-or!$processAbsent){throw "W21 cleanup failed down=$down container_absent=$containerAbsent volume_absent=$volumeAbsent process_absent=$processAbsent"};if($caught){throw $caught};$body.teardown=1;$body.down_native_exit=$down;$body.container_absent=$containerAbsent;$body.volume_absent=$volumeAbsent;$body.process_absent=$processAbsent;$tmp=Join-Path $e 'host-app-manifest.json.tmp';$body|ConvertTo-Json|Set-Content -Encoding utf8 $tmp;Move-Item -LiteralPath $tmp -Destination (Join-Path $e 'host-app-manifest.json') -Force;"W21_HOST_APP_GREEN compose=db runtime=host-JVM health=UP teardown=1 container_absent=$containerAbsent volume_absent=$volumeAbsent process_absent=$processAbsent"
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 6줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | param([Parameter(Mandatory=$true)][string]$ProjectRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[string]$ComposeProject='w21-learner-host',[int]$Port=28021,[int]$DbPort=62121,[switch]$ForceFailureAfterCompose) | runner의 mandatory/optional PowerShell parameter 계약을 연다. |
| 2 | $ErrorActionPreference='Stop';$root=(Resolve-Path -LiteralPath $ProjectRoot).Path;$referenceRoot=Split-Path -Parent $PSScriptRoot;$e=[IO.Path]::GetFullPath($EvidenceDir);$compose=Join-Path $referenceRoot 'compose.yaml';if(!(Test-Path -LiteralPath $compose)){throw 'W21 packaged DB-only compose.yaml missing'};New-Item -ItemType Directory -Force $e|Out-Null;$proc=$null;$appPid=$null;$body=$null;$caught=$null | PowerShell cmdlet error를 즉시 terminating error로 바꾼다. |
| 3 | $env:FCL_DB_PASSWORD='w21-disposable-password';$env:FCL_DB_PORT="$DbPort";$env:SPRING_DATASOURCE_URL="jdbc:postgresql://localhost:$DbPort/financial_core";$env:SPRING_DATASOURCE_USERNAME='app';$env:SPRING_DATASOURCE_PASSWORD=$env:FCL_DB_PASSWORD;$env:SERVER_PORT="$Port" | disposable DB secret/port와 Spring datasource, host server port를 현재 process 환경에 주입한다. |
| 4 | Push-Location $root | native docker/Gradle 상대 path 기준을 learner root로 바꾼다. |
| 5 | try{$services=@(& docker compose -f $compose -p $ComposeProject config --services);if($LASTEXITCODE-ne 0-or$services.Count-ne 1-or$services[0]-cne'db'){throw "W21 compose must be exact db: $services"};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw 'W21 db readiness failed'};if($ForceFailureAfterCompose){throw 'W21 forced partial-startup failure'};$stdout=Join-Path $e 'host-app.stdout.log';$stderr=Join-Path $e 'host-app.stderr.log';$proc=Start-Process -FilePath (Join-Path $root 'gradlew.bat') -ArgumentList @('bootRun','--no-daemon') -WorkingDirectory $root -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr;$health=$null;for($i=0;$i-lt 90;$i++){if($proc.HasExited){throw "host app exited=$($proc.ExitCode)"};try{$health=Invoke-RestMethod "http://localhost:$Port/actuator/health" -TimeoutSec 2;if($health.status-eq'UP'){break}}catch{};Start-Sleep 1};if(!$health-or$health.status-ne'UP'){throw 'host health never UP'};$listen=Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue;if(!$listen){throw "host port $Port not listening"};$appPid=[int]$listen[0].OwningProcess;$apiStatus=0;try{Invoke-WebRequest "http://localhost:$Port/api/accounts/1" -TimeoutSec 5|Out-Null;$apiStatus=200}catch{$apiStatus=[int]$_.Exception.Response.StatusCode};if($apiStatus-notin@(200,401)){throw "unexpected API status=$apiStatus"};$body=[ordered]@{compose_services=@($services);compose_project=$ComposeProject;runtime='host JVM';pid=$appPid;port=$Port;health='UP';api_status=$apiStatus;native_exit=0}}catch{$caught=$_}finally{if($appPid){Stop-Process -Id $appPid -Force -ErrorAction SilentlyContinue};if($proc-and!$proc.HasExited){Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue};& docker compose -f $compose -p $ComposeProject down -v|Out-Null;$down=$LASTEXITCODE;$containers=@(& docker compose -f $compose -p $ComposeProject ps -q);$containerAbsent=($LASTEXITCODE-eq 0-and$containers.Count-eq 0);$volumes=@(& docker volume ls -q --filter "name=^${ComposeProject}_financial-core-db$");$volumeAbsent=($LASTEXITCODE-eq 0-and$volumes.Count-eq 0);$processAbsent=if($appPid){-not[bool](Get-Process -Id $appPid -ErrorAction SilentlyContinue)}else{$true};Pop-Location;Remove-Item Env:FCL_DB_PASSWORD,Env:FCL_DB_PORT,Env:SPRING_DATASOURCE_URL,Env:SPRING_DATASOURCE_USERNAME,Env:SPRING_DATASOURCE_PASSWORD,Env:SERVER_PORT -ErrorAction SilentlyContinue} | D3의 핵심 try/catch/finally 한 줄이다. exact db 확인, DB ready, host bootRun, health/listen/API 관찰, process·container·volume cleanup을 한 owner lifecycle로 묶는다. |
| 6 | if($down-ne 0-or!$containerAbsent-or!$volumeAbsent-or!$processAbsent){throw "W21 cleanup failed down=$down container_absent=$containerAbsent volume_absent=$volumeAbsent process_absent=$processAbsent"};if($caught){throw $caught};$body.teardown=1;$body.down_native_exit=$down;$body.container_absent=$containerAbsent;$body.volume_absent=$volumeAbsent;$body.process_absent=$processAbsent;$tmp=Join-Path $e 'host-app-manifest.json.tmp';$body|ConvertTo-Json|Set-Content -Encoding utf8 $tmp;Move-Item -LiteralPath $tmp -Destination (Join-Path $e 'host-app-manifest.json') -Force;"W21_HOST_APP_GREEN compose=db runtime=host-JVM health=UP teardown=1 container_absent=$containerAbsent volume_absent=$volumeAbsent process_absent=$processAbsent" | cleanup exit와 세 absent 값을 fail-closed로 확인한 뒤 manifest를 atomic replace하고 W21_HOST_APP_GREEN marker를 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다. 다만 app container proof가 아니다
문법 해부
- 한 줄로 압축된 PowerShell에서도 `try/catch/finally`의 owner 순서는 그대로다.
- `Start-Process -PassThru -WindowStyle Hidden`이 host Gradle JVM process handle을 돌려준다.
실행 순서
- db service exactness와 readiness를 확인한다.
- host JVM을 bootRun하고 aggregate health·listen PID·API 200/401을 관찰한다.
- finally에서 process와 Compose project/volume을 정리한다.
- 세 absent invariant 뒤에만 host-app-manifest.json과 W21_HOST_APP_GREEN을 만든다.
W21 조각별 정밀 해설
F06-C01 · parameters and packaged dependency
- 문법 해부
- 1~2줄의 `parameters and packaged dependency`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `compose_services=[db]`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `compose_services=[db]`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: app container proof가 아니다.
- 착각 방지
- `parameters and packaged dependency`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: app container proof가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F06-C02 · host JVM environment and project scope
- 문법 해부
- 3~4줄의 `host JVM environment and project scope`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `runtime=host JVM`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `runtime=host JVM`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: API 200/401은 authorization proof가 아니다.
- 착각 방지
- `host JVM environment and project scope`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: API 200/401은 authorization proof가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F06-C03 · D3 lifecycle try finally
- 문법 해부
- 5~5줄의 `D3 lifecycle try finally`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `health=UP`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `health=UP`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: Stop-Process -Force는 graceful SIGTERM proof가 아니다.
- 착각 방지
- `D3 lifecycle try finally`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: Stop-Process -Force는 graceful SIGTERM proof가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F06-C04 · teardown invariant and atomic manifest
- 문법 해부
- 6~6줄의 `teardown invariant and atomic manifest`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `teardown=1`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `teardown=1`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: D6/D7은 새 lifecycle 없이 이 manifest만 읽는다.
- 착각 방지
- `teardown invariant and atomic manifest`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: D6/D7은 새 lifecycle 없이 이 manifest만 읽는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
docker ps에 app container가 없어도 정상이다.
-
료
왜 틀렸는지: source가 gradlew.bat bootRun을 host에서 시작한다.
-
키타
수정: db-only Compose + host JVM으로 설명한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F06-T01 startup | db-only compose + learner root | up --wait + bootRun | host health UP | default port 28021과 PDF 8080을 섞지 않는다 |
| F06-T02 probe | listen PID + API | Get-NetTCPConnection + HTTP | pid/port/api_status | 200/401만으로 BOLA를 증명하지 않는다 |
| F06-T03 cleanup | process/project/volume | finally stop + down -v | teardown1 + absent flags | forced cleanup은 graceful proof가 아니다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `정본 D3 lifecycle producer`이야.
-
료
대표 경계는 `D6/D7은 새 lifecycle 없이 이 manifest만 읽는다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
Compose는 db, Start-Process는 host JVM을 소유한다.
app container로 표현하면 topology가 바뀐다.최대 90회 aggregate health를 읽고 process 조기 종료도 감지한다.
liveness/readiness outage transition은 직접 보지 않는다.성공 body에 teardown fields를 더한 뒤 tmp manifest를 atomic replace한다.
D6/D7은 같은 manifest의 read-only consumer다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ D3는 app container lifecycle이다
왜 틀리나 source가 gradlew.bat bootRun을 host에서 시작한다.
바르게 읽기 db-only Compose + host JVM으로 설명한다.
반례 docker ps에 app container가 없어도 정상이다.
❌ API 200/401이면 authorization이 맞다
왜 틀리나 허용 status 범위만 본다.
바르게 읽기 object owner·write no-effect는 별도 test로 검증한다.
반례 잘못된 body도 200일 수 있다.
❌ health UP이면 liveness/readiness 전이도 증명된다
왜 틀리나 aggregate endpoint healthy 한 시점만 본다.
바르게 읽기 D5 matrix에서 DB 중단·복구를 별도 관찰한다.
반례 dependency DOWN 때 liveness 유지 여부는 미검증이다.
❌ D6/D7이 새 lifecycle을 실행한다
왜 틀리나 둘은 D3 manifest를 읽는 review다.
바르게 읽기 no new lifecycle claim을 유지한다.
반례 같은 hash 재검토를 재실행으로 부르면 과장이다.
❌ 8080과 default 28021은 자동으로 같다
왜 틀리나 source default와 PDF overview가 충돌한다.
바르게 읽기 실제 invocation과 manifest port에 바인딩한다.
반례 다른 port를 probe하고 8080 Green이라 쓸 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
app container proof가 아니다
이 책임을 맡는 곳: 별도 selector/실행API 200/401은 authorization proof가 아니다
이 책임을 맡는 곳: secret·운영 정책Stop-Process -Force는 graceful SIGTERM proof가 아니다
이 책임을 맡는 곳: concurrency·infrastructureD6/D7은 새 lifecycle 없이 이 manifest만 읽는다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: Compose db와 host JVM을 한 번 실행·관찰·정리해 D3 manifest를 만드는 W21 lifecycle owner다.
2단계 · 코드 조각 재조립
- parameters and packaged dependency
- host JVM environment and project scope
- D3 lifecycle try finally
- teardown invariant and atomic manifest
3단계 · 파일 전체 다시 쓰기
6개 물리 줄을 원본 순서로 복원하고 SHA-256 875a1a471ffbbd74b6c2471eb66b8d3d0a538238f49d89af3b4ccf1faed74dc0와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
원문 정본 전체 source 확인하기
param([Parameter(Mandatory=$true)][string]$ProjectRoot,[Parameter(Mandatory=$true)][string]$EvidenceDir,[string]$ComposeProject='w21-learner-host',[int]$Port=28021,[int]$DbPort=62121,[switch]$ForceFailureAfterCompose)
$ErrorActionPreference='Stop';$root=(Resolve-Path -LiteralPath $ProjectRoot).Path;$referenceRoot=Split-Path -Parent $PSScriptRoot;$e=[IO.Path]::GetFullPath($EvidenceDir);$compose=Join-Path $referenceRoot 'compose.yaml';if(!(Test-Path -LiteralPath $compose)){throw 'W21 packaged DB-only compose.yaml missing'};New-Item -ItemType Directory -Force $e|Out-Null;$proc=$null;$appPid=$null;$body=$null;$caught=$null
$env:FCL_DB_PASSWORD='w21-disposable-password';$env:FCL_DB_PORT="$DbPort";$env:SPRING_DATASOURCE_URL="jdbc:postgresql://localhost:$DbPort/financial_core";$env:SPRING_DATASOURCE_USERNAME='app';$env:SPRING_DATASOURCE_PASSWORD=$env:FCL_DB_PASSWORD;$env:SERVER_PORT="$Port"
Push-Location $root
try{$services=@(& docker compose -f $compose -p $ComposeProject config --services);if($LASTEXITCODE-ne 0-or$services.Count-ne 1-or$services[0]-cne'db'){throw "W21 compose must be exact db: $services"};& docker compose -f $compose -p $ComposeProject up -d --wait db;if($LASTEXITCODE-ne 0){throw 'W21 db readiness failed'};if($ForceFailureAfterCompose){throw 'W21 forced partial-startup failure'};$stdout=Join-Path $e 'host-app.stdout.log';$stderr=Join-Path $e 'host-app.stderr.log';$proc=Start-Process -FilePath (Join-Path $root 'gradlew.bat') -ArgumentList @('bootRun','--no-daemon') -WorkingDirectory $root -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdout -RedirectStandardError $stderr;$health=$null;for($i=0;$i-lt 90;$i++){if($proc.HasExited){throw "host app exited=$($proc.ExitCode)"};try{$health=Invoke-RestMethod "http://localhost:$Port/actuator/health" -TimeoutSec 2;if($health.status-eq'UP'){break}}catch{};Start-Sleep 1};if(!$health-or$health.status-ne'UP'){throw 'host health never UP'};$listen=Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue;if(!$listen){throw "host port $Port not listening"};$appPid=[int]$listen[0].OwningProcess;$apiStatus=0;try{Invoke-WebRequest "http://localhost:$Port/api/accounts/1" -TimeoutSec 5|Out-Null;$apiStatus=200}catch{$apiStatus=[int]$_.Exception.Response.StatusCode};if($apiStatus-notin@(200,401)){throw "unexpected API status=$apiStatus"};$body=[ordered]@{compose_services=@($services);compose_project=$ComposeProject;runtime='host JVM';pid=$appPid;port=$Port;health='UP';api_status=$apiStatus;native_exit=0}}catch{$caught=$_}finally{if($appPid){Stop-Process -Id $appPid -Force -ErrorAction SilentlyContinue};if($proc-and!$proc.HasExited){Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue};& docker compose -f $compose -p $ComposeProject down -v|Out-Null;$down=$LASTEXITCODE;$containers=@(& docker compose -f $compose -p $ComposeProject ps -q);$containerAbsent=($LASTEXITCODE-eq 0-and$containers.Count-eq 0);$volumes=@(& docker volume ls -q --filter "name=^${ComposeProject}_financial-core-db$");$volumeAbsent=($LASTEXITCODE-eq 0-and$volumes.Count-eq 0);$processAbsent=if($appPid){-not[bool](Get-Process -Id $appPid -ErrorAction SilentlyContinue)}else{$true};Pop-Location;Remove-Item Env:FCL_DB_PASSWORD,Env:FCL_DB_PORT,Env:SPRING_DATASOURCE_URL,Env:SPRING_DATASOURCE_USERNAME,Env:SPRING_DATASOURCE_PASSWORD,Env:SERVER_PORT -ErrorAction SilentlyContinue}
if($down-ne 0-or!$containerAbsent-or!$volumeAbsent-or!$processAbsent){throw "W21 cleanup failed down=$down container_absent=$containerAbsent volume_absent=$volumeAbsent process_absent=$processAbsent"};if($caught){throw $caught};$body.teardown=1;$body.down_native_exit=$down;$body.container_absent=$containerAbsent;$body.volume_absent=$volumeAbsent;$body.process_absent=$processAbsent;$tmp=Join-Path $e 'host-app-manifest.json.tmp';$body|ConvertTo-Json|Set-Content -Encoding utf8 $tmp;Move-Item -LiteralPath $tmp -Destination (Join-Path $e 'host-app-manifest.json') -Force;"W21_HOST_APP_GREEN compose=db runtime=host-JVM health=UP teardown=1 container_absent=$containerAbsent volume_absent=$volumeAbsent process_absent=$processAbsent"
07PowerShell 환경 변수 template — secret을 파일에 쓰지 않기
illustrative/powershell/W21-host-env-template.txt
학습용 예시 · PDF template/worksheet · 정본 답안 아님 · lifecycle owner 아님 · 학습용 예시 · 정본 답안 아님 · W21-F077줄 연결7줄 번역3 chunks
PowerShell 환경 변수 template — secret을 파일에 쓰지 않기
illustrative/powershell/W21-host-env-template.txt
학습용 예시 · PDF template/worksheet · 정본 답안 아님 · lifecycle owner 아님 · 학습용 예시 · 정본 답안 아님 · W21-F07STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
- `status=TEMPLATE`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `실제 Green owner가 아니다`과 어떻게 연결되는가?
status=TEMPLATEURL uses db-port placeholderusername=apppassword not writtenSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
PowerShell 환경 변수 template — secret을 파일에 쓰지 않기를 무대 운영표로 바꾸기
PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
핵심값 status=TEMPLATE, URL uses db-port placeholder, username=app, password not written을 원본 줄로 따라가되, `실제 Green owner가 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
template provenance
1~4줄을 한 덩어리로 읽어 1번째 움직임을 본다. PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
- 코드 연결
1~4줄- 비유
- 주소·사용자명은 배포 표에 적되 비밀번호는 잠긴 봉투처럼 현재 trusted terminal에서만 전달하는 수동 점검표
- 비유의 끝
- 실제 Green owner가 아니다
non-secret environment shape
5~6줄을 한 덩어리로 읽어 2번째 움직임을 본다. PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
- 코드 연결
5~6줄- 비유
- 주소·사용자명은 배포 표에 적되 비밀번호는 잠긴 봉투처럼 현재 trusted terminal에서만 전달하는 수동 점검표
- 비유의 끝
- 완성 PowerShell script가 아니다
trusted-terminal password rule
7~7줄을 한 덩어리로 읽어 3번째 움직임을 본다. PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
- 코드 연결
7~7줄- 비유
- 주소·사용자명은 배포 표에 적되 비밀번호는 잠긴 봉투처럼 현재 trusted terminal에서만 전달하는 수동 점검표
- 비유의 끝
- secret non-exposure를 자동 증명하지 않는다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `status=TEMPLATE` 맞아?
-
니지카
PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
-
료
첫 source 값은 `status=TEMPLATE`로 잡자.
-
키타
단, `실제 Green owner가 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`URL uses db-port placeholder`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. template provenance를 읽는다. → URL placeholder와 username 모양을 검토한다.
-
료
그 다음은 password가 source에 없는지 확인한다. → actual Green은 D3 owner runner에서 수행한다.
-
키타
관찰값과 `완성 PowerShell script가 아니다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 7줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F07-L01 | status: |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 이 block이 실행 정답이 아니라 TEMPLATE임을 선언한다.
|
| 2줄F07-L02 | target: |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 검토 대상이 PowerShell 환경 변수 모양임을 제한한다.
|
| 3줄F07-L03 | evidence_owner: |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 실제 evidence owner가 Sunday host-app runner임을 가리킨다.
|
| 4줄F07-L04 | actual_green_owner: |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 이 template가 아니라 run-w21-host-app.ps1만 Green owner임을 못박는다.
|
| 5줄F07-L05 | $env: |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | host의 disposable DB port를 쓰는 Spring datasource JDBC URL 모양을 보여준다.
|
| 6줄F07-L06 | $env: |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | Spring datasource username을 app으로 지정하는 PowerShell 환경 변수 모양이다.
|
| 7줄F07-L07 | # password is supplied from the current trusted terminal, |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 실제 password는 source가 아니라 trusted terminal에서 공급한다고 경고한다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `password not written`이야.
-
키타
`secret non-exposure를 자동 증명하지 않는다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
status: TEMPLATE
target: learner reviews PowerShell environment-variable shape only
evidence_owner: scripts/run-w21-host-app.ps1 (Sunday)
actual_green_owner: run-w21-host-app.ps1 only
$env:SPRING_DATASOURCE_URL='jdbc:postgresql://localhost:<db-port>/financial_core'
$env:SPRING_DATASOURCE_USERNAME='app'
# password is supplied from the current trusted terminal, never written here
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 7줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | status: TEMPLATE | 이 block이 실행 정답이 아니라 TEMPLATE임을 선언한다. |
| 2 | target: learner reviews PowerShell environment-variable shape only | 검토 대상이 PowerShell 환경 변수 모양임을 제한한다. |
| 3 | evidence_owner: scripts/run-w21-host-app.ps1 (Sunday) | 실제 evidence owner가 Sunday host-app runner임을 가리킨다. |
| 4 | actual_green_owner: run-w21-host-app.ps1 only | 이 template가 아니라 run-w21-host-app.ps1만 Green owner임을 못박는다. |
| 5 | $env:SPRING_DATASOURCE_URL='jdbc:postgresql://localhost:<db-port>/financial_core' | host의 disposable DB port를 쓰는 Spring datasource JDBC URL 모양을 보여준다. |
| 6 | $env:SPRING_DATASOURCE_USERNAME='app' | Spring datasource username을 app으로 지정하는 PowerShell 환경 변수 모양이다. |
| 7 | # password is supplied from the current trusted terminal, never written here | 실제 password는 source가 아니라 trusted terminal에서 공급한다고 경고한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다. 다만 실제 Green owner가 아니다
문법 해부
- PowerShell 환경 변수는 `$env:NAME='value'` 형식이며 Linux의 `NAME=value command`와 다르다.
- 앞의 `status:` metadata는 설명용 text이므로 파일 전체가 실행 가능한 ps1은 아니다.
실행 순서
- template provenance를 읽는다.
- URL placeholder와 username 모양을 검토한다.
- password가 source에 없는지 확인한다.
- actual Green은 D3 owner runner에서 수행한다.
W21 조각별 정밀 해설
F07-C01 · template provenance
- 문법 해부
- 1~4줄의 `template provenance`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `status=TEMPLATE`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `status=TEMPLATE`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 실제 Green owner가 아니다.
- 착각 방지
- `template provenance`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 실제 Green owner가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F07-C02 · non-secret environment shape
- 문법 해부
- 5~6줄의 `non-secret environment shape`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `URL uses db-port placeholder`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `URL uses db-port placeholder`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 완성 PowerShell script가 아니다.
- 착각 방지
- `non-secret environment shape`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 완성 PowerShell script가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F07-C03 · trusted-terminal password rule
- 문법 해부
- 7~7줄의 `trusted-terminal password rule`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `username=app`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `username=app`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: secret non-exposure를 자동 증명하지 않는다.
- 착각 방지
- `trusted-terminal password rule`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: secret non-exposure를 자동 증명하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
그대로 실행하면 status: 줄에서 실패한다.
-
료
왜 틀렸는지: metadata와 placeholder가 남아 있다.
-
키타
수정: D3 packaged runner를 actual owner로 둔다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F07-T01 URL | db-port | $env:SPRING_DATASOURCE_URL | host DB target | placeholder를 실제 값으로 바꿔야 한다 |
| F07-T02 user | app | $env:...USERNAME | Spring datasource username | 권한 최소화는 별도다 |
| F07-T03 password | trusted terminal | source에 쓰지 않음 | file secret absent | runtime secret store 검증은 별도다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `수동 환경 변수 모양 template`이야.
-
료
대표 경계는 `Linux NAME=value 문법과 다르다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
$env:` assignment가 child process 환경에 상속된다.
현재 terminal scope 밖 보존 정책은 별도다.SPRING_DATASOURCE_*가 externalized configuration으로 binding된다.
profile precedence 전체는 matrix로 검토한다.password value는 source block에 존재하지 않는다.
log/actuator 노출이 자동으로 막히지는 않는다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ 이 template 자체가 Green 실행기다
왜 틀리나 metadata와 placeholder가 남아 있다.
바르게 읽기 D3 packaged runner를 actual owner로 둔다.
반례 그대로 실행하면 status: 줄에서 실패한다.
❌ Linux 문법을 PowerShell에 붙이면 된다
왜 틀리나 shell assignment syntax가 다르다.
바르게 읽기 각 `$env:` 값을 먼저 설정한다.
반례 `NAME=value java`는 PowerShell에서 다른 해석이다.
❌ password 주석이면 값도 안전하다
왜 틀리나 실제 terminal·log·history 취급이 중요하다.
바르게 읽기 trusted source에서 주입하고 출력하지 않는다.
반례 command history에 평문이 남을 수 있다.
❌ 환경 변수면 Git 노출은 불가능하다
왜 틀리나 스크립트에 값을 하드코딩하면 Git에 남는다.
바르게 읽기 이 파일에는 이름과 placeholder만 둔다.
반례 prod password literal commit은 실패다.
❌ URL과 username이 맞으면 profile도 맞다
왜 틀리나 active profile과 property precedence는 별도다.
바르게 읽기 config matrix와 startup source를 검토한다.
반례 local 값이 prod를 덮을 수 있다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
실제 Green owner가 아니다
이 책임을 맡는 곳: 별도 selector/실행완성 PowerShell script가 아니다
이 책임을 맡는 곳: secret·운영 정책secret non-exposure를 자동 증명하지 않는다
이 책임을 맡는 곳: concurrency·infrastructureLinux NAME=value 문법과 다르다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: PowerShell `$env:` 형식과 trusted terminal secret 공급 원칙을 보여주는 PDF 수동 검토 template다.
2단계 · 코드 조각 재조립
- template provenance
- non-secret environment shape
- trusted-terminal password rule
3단계 · 파일 전체 다시 쓰기
7개 물리 줄을 원본 순서로 복원하고 SHA-256 8eb0b7325514b73c96b5034920866dc5154790a9fd4ef55890868be5a9c5dcca와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
status: TEMPLATE
target: learner reviews PowerShell environment-variable shape only
evidence_owner: scripts/run-w21-host-app.ps1 (Sunday)
actual_green_owner: run-w21-host-app.ps1 only
$env:SPRING_DATASOURCE_URL='jdbc:postgresql://localhost:<db-port>/financial_core'
$env:SPRING_DATASOURCE_USERNAME='app'
# password is supplied from the current trusted terminal, never written here
08health worksheet — liveness·readiness·aggregate 분리
illustrative/powershell/W21-health-probe.ps1
학습용 예시 · PDF template/worksheet · 정본 답안 아님 · lifecycle owner 아님 · 학습용 예시 · 정본 답안 아님 · W21-F0813줄 연결13줄 번역3 chunks
health worksheet — liveness·readiness·aggregate 분리
illustrative/powershell/W21-health-probe.ps1
학습용 예시 · PDF template/worksheet · 정본 답안 아님 · lifecycle owner 아님 · 학습용 예시 · 정본 답안 아님 · W21-F08STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
- `liveness=UP`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `D3 lifecycle owner가 아니다`과 어떻게 연결되는가?
liveness=UPreadiness=UPaggregate=UPchecked_at UTCSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
health worksheet — liveness·readiness·aggregate 분리를 무대 운영표로 바꾸기
liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
핵심값 liveness=UP, readiness=UP, aggregate=UP, checked_at UTC을 원본 줄로 따라가되, `D3 lifecycle owner가 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
three health requests
1~4줄을 한 덩어리로 읽어 1번째 움직임을 본다. liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
- 코드 연결
1~4줄- 비유
- 직원이 살아 있는지(liveness), 재료와 결제기가 준비됐는지(readiness), 매장 전체 상태가 어떤지(aggregate)를 서로 다른 칸에 적는 건강 점검표
- 비유의 끝
- D3 lifecycle owner가 아니다
meaning-specific UP guards
5~7줄을 한 덩어리로 읽어 2번째 움직임을 본다. liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
- 코드 연결
5~7줄- 비유
- 직원이 살아 있는지(liveness), 재료와 결제기가 준비됐는지(readiness), 매장 전체 상태가 어떤지(aggregate)를 서로 다른 칸에 적는 건강 점검표
- 비유의 끝
- DB 중단/복구 전이를 자동 실행하지 않는다
JSON worksheet output
8~13줄을 한 덩어리로 읽어 3번째 움직임을 본다. liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
- 코드 연결
8~13줄- 비유
- 직원이 살아 있는지(liveness), 재료와 결제기가 준비됐는지(readiness), 매장 전체 상태가 어떤지(aggregate)를 서로 다른 칸에 적는 건강 점검표
- 비유의 끝
- health-matrix.csv를 대신 만들지 않는다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `liveness=UP` 맞아?
-
니지카
liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
-
료
첫 source 값은 `liveness=UP`로 잡자.
-
키타
단, `D3 lifecycle owner가 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`readiness=UP`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. 세 health endpoint를 각각 호출한다. → 각 의미에 맞는 status가 UP인지 guard한다.
-
료
그 다음은 liveness/readiness/aggregate 값을 모은다. → UTC checked_at과 함께 JSON을 출력한다.
-
키타
관찰값과 `DB 중단/복구 전이를 자동 실행하지 않는다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 13줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F08-L01 | $BaseUrl = |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 세 health endpoint가 공유할 localhost base URL을 정한다.
|
| 2줄F08-L02 | $Live = |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 세 health endpoint가 공유할 localhost base URL을 정한다.
|
| 3줄F08-L03 | $Ready = |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 세 health endpoint가 공유할 localhost base URL을 정한다.
|
| 4줄F08-L04 | $Health = |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 세 health endpoint가 공유할 localhost base URL을 정한다.
|
| 5줄F08-L05 | if( |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | liveness가 UP이 아니면 process 의미가 깨졌다고 중단한다.
|
| 6줄F08-L06 | if( |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | readiness가 UP이 아니면 dependency 준비 의미가 깨졌다고 중단한다.
|
| 7줄F08-L07 | if( |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | aggregate health가 UP이 아니면 worksheet를 중단한다.
|
| 8줄F08-L08 | [ |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 안정된 순서의 health result map을 연다.
|
| 9줄F08-L09 | liveness= |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | liveness status를 result에 기록한다.
|
| 10줄F08-L10 | readiness= |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | readiness status를 result에 기록한다.
|
| 11줄F08-L11 | aggregate= |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | aggregate health status를 result에 기록한다.
|
| 12줄F08-L12 | checked_at= |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | 관찰 시각을 UTC ISO-8601 문자열로 기록한다.
|
| 13줄F08-L13 | }|ConvertTo-Json |
설정 봉투와 세 종류의 건강등을 구분하는 수동 점검표 | ordered health result를 JSON text로 출력한다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `checked_at UTC`이야.
-
키타
`health-matrix.csv를 대신 만들지 않는다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 3개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
$BaseUrl = 'http://localhost:8080'
$Live = Invoke-RestMethod "$BaseUrl/actuator/health/liveness"
$Ready = Invoke-RestMethod "$BaseUrl/actuator/health/readiness"
$Health = Invoke-RestMethod "$BaseUrl/actuator/health"
if($Live.status -ne 'UP'){throw 'liveness must describe this JVM process'}
if($Ready.status -ne 'UP'){throw 'readiness must include required dependencies'}
if($Health.status -ne 'UP'){throw 'aggregate health is not UP'}
[ordered]@{
liveness=$Live.status
readiness=$Ready.status
aggregate=$Health.status
checked_at=[DateTimeOffset]::UtcNow.ToString('o')
}|ConvertTo-Json
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 13줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | $BaseUrl = 'http://localhost:8080' | 세 health endpoint가 공유할 localhost base URL을 정한다. |
| 2 | $Live = Invoke-RestMethod "$BaseUrl/actuator/health/liveness" | 세 health endpoint가 공유할 localhost base URL을 정한다. |
| 3 | $Ready = Invoke-RestMethod "$BaseUrl/actuator/health/readiness" | 세 health endpoint가 공유할 localhost base URL을 정한다. |
| 4 | $Health = Invoke-RestMethod "$BaseUrl/actuator/health" | 세 health endpoint가 공유할 localhost base URL을 정한다. |
| 5 | if($Live.status -ne 'UP'){throw 'liveness must describe this JVM process'} | liveness가 UP이 아니면 process 의미가 깨졌다고 중단한다. |
| 6 | if($Ready.status -ne 'UP'){throw 'readiness must include required dependencies'} | readiness가 UP이 아니면 dependency 준비 의미가 깨졌다고 중단한다. |
| 7 | if($Health.status -ne 'UP'){throw 'aggregate health is not UP'} | aggregate health가 UP이 아니면 worksheet를 중단한다. |
| 8 | [ordered]@{ | 안정된 순서의 health result map을 연다. |
| 9 | liveness=$Live.status | liveness status를 result에 기록한다. |
| 10 | readiness=$Ready.status | readiness status를 result에 기록한다. |
| 11 | aggregate=$Health.status | aggregate health status를 result에 기록한다. |
| 12 | checked_at=[DateTimeOffset]::UtcNow.ToString('o') | 관찰 시각을 UTC ISO-8601 문자열로 기록한다. |
| 13 | }|ConvertTo-Json | ordered health result를 JSON text로 출력한다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다. 다만 D3 lifecycle owner가 아니다
문법 해부
- `Invoke-RestMethod`는 JSON body를 PowerShell object로 변환해 `.status`로 읽게 한다.
- `[ordered]@{...}|ConvertTo-Json`은 관찰 순서가 안정된 JSON worksheet를 만든다.
실행 순서
- 세 health endpoint를 각각 호출한다.
- 각 의미에 맞는 status가 UP인지 guard한다.
- liveness/readiness/aggregate 값을 모은다.
- UTC checked_at과 함께 JSON을 출력한다.
W21 조각별 정밀 해설
F08-C01 · three health requests
- 문법 해부
- 1~4줄의 `three health requests`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `liveness=UP`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `liveness=UP`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: D3 lifecycle owner가 아니다.
- 착각 방지
- `three health requests`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: D3 lifecycle owner가 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F08-C02 · meaning-specific UP guards
- 문법 해부
- 5~7줄의 `meaning-specific UP guards`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `readiness=UP`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `readiness=UP`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: DB 중단/복구 전이를 자동 실행하지 않는다.
- 착각 방지
- `meaning-specific UP guards`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: DB 중단/복구 전이를 자동 실행하지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F08-C03 · JSON worksheet output
- 문법 해부
- 8~13줄의 `JSON worksheet output`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `aggregate=UP`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `aggregate=UP`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: health-matrix.csv를 대신 만들지 않는다.
- 착각 방지
- `JSON worksheet output`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: health-matrix.csv를 대신 만들지 않는다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
둘 다 DOWN이면 불필요한 restart loop가 생긴다.
-
료
왜 틀렸는지: process 생존과 dependency 준비는 다르다.
-
키타
수정: DB outage에도 liveness는 유지하도록 분리한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F08-T01 healthy | three endpoints | GET + status guards | UP/UP/UP | healthy 한 시점뿐이다 |
| F08-T02 DB outage | dependency DOWN | 별도 장애 주입 | readiness DOWN 예상 | worksheet는 이 전이를 실행하지 않는다 |
| F08-T03 recovery | DB restored | 별도 재조회 | readiness UP 예상 | CSV evidence 작성은 learner 책임이다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `healthy 시점 health 의미 worksheet`이야.
-
료
대표 경계는 `detail secret 비노출은 별도 검토다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
liveness와 readiness가 서로 다른 indicator 집합을 노출할 수 있다.
group 설정이 실제로 올바른지는 별도 검증이다.readiness DOWN은 traffic 제외 판단에 쓰인다.
process restart 판단과 섞으면 loop가 날 수 있다.status와 timestamp를 stdout JSON으로 만든다.
canonical health-matrix.csv를 자동 생성하지 않는다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ liveness와 readiness는 같은 값이어야 한다
왜 틀리나 process 생존과 dependency 준비는 다르다.
바르게 읽기 DB outage에도 liveness는 유지하도록 분리한다.
반례 둘 다 DOWN이면 불필요한 restart loop가 생긴다.
❌ aggregate health만 보면 충분하다
왜 틀리나 어떤 layer가 DOWN인지 의미가 섞인다.
바르게 읽기 세 endpoint를 따로 읽는다.
반례 traffic 판단과 process 판단을 나눌 수 없다.
❌ 이 worksheet가 DB를 중단시킨다
왜 틀리나 source에는 장애 주입 명령이 없다.
바르게 읽기 D3 owner와 별도 matrix 절차에서 수행한다.
반례 healthy JSON만으로 recovery를 주장할 수 없다.
❌ JSON 출력이 곧 CSV evidence다
왜 틀리나 형식과 owner가 다르다.
바르게 읽기 health-matrix.csv를 별도로 작성·검토한다.
반례 stdout만 저장하면 required path가 없다.
❌ health detail은 무조건 안전하다
왜 틀리나 URL·username 등이 노출될 수 있다.
바르게 읽기 운영 detail과 log를 점검한다.
반례 UP이어도 secret leak이면 실패다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
D3 lifecycle owner가 아니다
이 책임을 맡는 곳: 별도 selector/실행DB 중단/복구 전이를 자동 실행하지 않는다
이 책임을 맡는 곳: secret·운영 정책health-matrix.csv를 대신 만들지 않는다
이 책임을 맡는 곳: concurrency·infrastructuredetail secret 비노출은 별도 검토다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: liveness·readiness·aggregate health를 구분해 healthy 시점의 세 상태를 JSON으로 읽는 worksheet다.
2단계 · 코드 조각 재조립
- three health requests
- meaning-specific UP guards
- JSON worksheet output
3단계 · 파일 전체 다시 쓰기
13개 물리 줄을 원본 순서로 복원하고 SHA-256 927a28375b497cf2ddd15120120293c928fd39b8bb43d7d1bbbd85642ed96dd2와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
$BaseUrl = 'http://localhost:8080'
$Live = Invoke-RestMethod "$BaseUrl/actuator/health/liveness"
$Ready = Invoke-RestMethod "$BaseUrl/actuator/health/readiness"
$Health = Invoke-RestMethod "$BaseUrl/actuator/health"
if($Live.status -ne 'UP'){throw 'liveness must describe this JVM process'}
if($Ready.status -ne 'UP'){throw 'readiness must include required dependencies'}
if($Health.status -ne 'UP'){throw 'aggregate health is not UP'}
[ordered]@{
liveness=$Live.status
readiness=$Ready.status
aggregate=$Health.status
checked_at=[DateTimeOffset]::UtcNow.ToString('o')
}|ConvertTo-Json
09W21-SQL-Q33 — LAG로 직전 거래와 금액 차이 읽기
illustrative/sql/W21-SQL-Q33.sql
학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F0928줄 연결28줄 번역4 chunks
W21-SQL-Q33 — LAG로 직전 거래와 금액 차이 읽기
illustrative/sql/W21-SQL-Q33.sql
학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F09STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
- `fixture rows=21`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `workbook 제공 정답이 아니다`과 어떻게 연결되는가?
fixture rows=21first previous=NULLaccount101 last difference=0stable occurred_at+tx_idSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
W21-SQL-Q33 — LAG로 직전 거래와 금액 차이 읽기를 무대 운영표로 바꾸기
account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
핵심값 fixture rows=21, first previous=NULL, account101 last difference=0, stable occurred_at+tx_id을 원본 줄로 따라가되, `workbook 제공 정답이 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
provenance assumptions and schema
1~5줄을 한 덩어리로 읽어 1번째 움직임을 본다. account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
- 코드 연결
1~5줄- 비유
- 계좌별 카드 줄에서 각 카드가 바로 앞 카드의 금액을 빌려 적고 현재 금액과 차이를 계산하는 표
- 비유의 끝
- workbook 제공 정답이 아니다
account window and LAG
6~17줄을 한 덩어리로 읽어 2번째 움직임을 본다. account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
- 코드 연결
6~17줄- 비유
- 계좌별 카드 줄에서 각 카드가 바로 앞 카드의 금액을 빌려 적고 현재 금액과 차이를 계산하는 표
- 비유의 끝
- 모든 status 포함은 명시한 가정이다
difference projection and stable output
18~27줄을 한 덩어리로 읽어 3번째 움직임을 본다. account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
- 코드 연결
18~27줄- 비유
- 계좌별 카드 줄에서 각 카드가 바로 앞 카드의 금액을 빌려 적고 현재 금액과 차이를 계산하는 표
- 비유의 끝
- 첫 difference는 NULL이다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `fixture rows=21` 맞아?
-
니지카
account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
-
료
첫 source 값은 `fixture rows=21`로 잡자.
-
키타
단, `workbook 제공 정답이 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`first previous=NULL`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. business_tx 21행을 account별로 나눈다. → 시간·tx_id 순서로 세운다.
-
료
그 다음은 직전 amount를 previous_amount로 붙인다. → 현재-직전 difference를 계산하고 같은 순서로 출력한다.
-
키타
관찰값과 `모든 status 포함은 명시한 가정이다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 28줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F09-L01 | -- W21-SQL-Q33: |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다.
|
| 2줄F09-L02 | -- 학습용 비정본 예시: |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | workbook이 배포한 정답이 아닌 학습용 예시임을 선언한다.
|
| 3줄F09-L03 | -- 가정: |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 문제에서 비운 inclusion/tie 정책을 명시적 가정으로 고정한다.
|
| 4줄F09-L04 | -- grain/ |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 입력·출력 행 단위와 fixture 예상 행 수를 주석으로 고정한다.
|
| 5줄F09-L05 | SET search_path TO : |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | workbook_schema를 우선 조회하도록 PostgreSQL search_path를 고정한다.
|
| 7줄F09-L07 | WITH ordered_transactions AS ( |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | LAG 계산을 담을 ordered_transactions CTE를 연다.
|
| 8줄F09-L08 | SELECT account_id, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다.
|
| 9줄F09-L09 | tx_id, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 10줄F09-L10 | status, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 11줄F09-L11 | amount, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 12줄F09-L12 | occurred_at, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 13줄F09-L13 | LAG( |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 같은 account partition의 직전 amount를 previous_amount로 붙인다.
|
| 14줄F09-L14 | PARTITION BY account_id |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 직전 거래 state를 account마다 독립시킨다.
|
| 15줄F09-L15 | ORDER BY occurred_at, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 시간과 tx_id 오름차순으로 LAG predecessor를 안정화한다.
|
| 16줄F09-L16 | ) AS previous_amount |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 비정본 LAG SQL 예시에서 16번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 17줄F09-L17 | FROM business_tx |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 모든 status를 포함한 business_tx row를 입력으로 읽는다.
|
| 18줄F09-L18 | ) |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
| 19줄F09-L19 | SELECT account_id, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다.
|
| 20줄F09-L20 | tx_id, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 21줄F09-L21 | status, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 22줄F09-L22 | amount, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 23줄F09-L23 | previous_amount, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 24줄F09-L24 | amount - previous_amount AS amount_difference, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 amount에서 직전 amount를 빼 difference를 계산한다.
|
| 25줄F09-L25 | occurred_at |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 26줄F09-L26 | FROM ordered_transactions |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | LAG가 붙은 거래 relation을 최종 projection 입력으로 읽는다.
|
| 27줄F09-L27 | ORDER BY account_id, |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | account·time·tx_id 순서로 최종 LAG 결과를 안정화한다.
|
| 29줄F09-L29 | -- 첫 거래는 previous_amount와 amount_difference가 NULL이다. |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다.
|
| 30줄F09-L30 | -- account 101의 마지막 두 reversal은 600 -> 600이므로 마지막 차이는 0이다. |
현재 카드 옆에 바로 앞 카드의 금액을 빌려 적는 거래표 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `stable occurred_at+tx_id`이야.
-
키타
`첫 difference는 NULL이다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
-- W21-SQL-Q33: 직전 거래와 금액 차이
-- 학습용 비정본 예시: workbook에는 제공 정답이 없다.
-- 가정: SUCCESS/FAILED/UNKNOWN/PROCESSING를 포함한 business_tx 전체를 거래 이력으로 읽는다.
-- grain/cardinality: 거래 1건당 1행, fixture 기준 21행.
SET search_path TO :"workbook_schema", public;
WITH ordered_transactions AS (
SELECT account_id,
tx_id,
status,
amount,
occurred_at,
LAG(amount) OVER (
PARTITION BY account_id
ORDER BY occurred_at, tx_id
) AS previous_amount
FROM business_tx
)
SELECT account_id,
tx_id,
status,
amount,
previous_amount,
amount - previous_amount AS amount_difference,
occurred_at
FROM ordered_transactions
ORDER BY account_id, occurred_at, tx_id;
-- 첫 거래는 previous_amount와 amount_difference가 NULL이다.
-- account 101의 마지막 두 reversal은 600 -> 600이므로 마지막 차이는 0이다.
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 28줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | -- W21-SQL-Q33: 직전 거래와 금액 차이 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다. |
| 2 | -- 학습용 비정본 예시: workbook에는 제공 정답이 없다. | workbook이 배포한 정답이 아닌 학습용 예시임을 선언한다. |
| 3 | -- 가정: SUCCESS/FAILED/UNKNOWN/PROCESSING를 포함한 business_tx 전체를 거래 이력으로 읽는다. | 문제에서 비운 inclusion/tie 정책을 명시적 가정으로 고정한다. |
| 4 | -- grain/cardinality: 거래 1건당 1행, fixture 기준 21행. | 입력·출력 행 단위와 fixture 예상 행 수를 주석으로 고정한다. |
| 5 | SET search_path TO :"workbook_schema", public; | workbook_schema를 우선 조회하도록 PostgreSQL search_path를 고정한다. |
| 7 | WITH ordered_transactions AS ( | LAG 계산을 담을 ordered_transactions CTE를 연다. |
| 8 | SELECT account_id, | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다. |
| 9 | tx_id, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 10 | status, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 11 | amount, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 12 | occurred_at, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 13 | LAG(amount) OVER ( | 같은 account partition의 직전 amount를 previous_amount로 붙인다. |
| 14 | PARTITION BY account_id | 직전 거래 state를 account마다 독립시킨다. |
| 15 | ORDER BY occurred_at, tx_id | 시간과 tx_id 오름차순으로 LAG predecessor를 안정화한다. |
| 16 | ) AS previous_amount | 비정본 LAG SQL 예시에서 16번째 원문 문장을 앞뒤 단계와 연결한다. |
| 17 | FROM business_tx | 모든 status를 포함한 business_tx row를 입력으로 읽는다. |
| 18 | ) | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
| 19 | SELECT account_id, | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다. |
| 20 | tx_id, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 21 | status, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 22 | amount, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 23 | previous_amount, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 24 | amount - previous_amount AS amount_difference, | 현재 amount에서 직전 amount를 빼 difference를 계산한다. |
| 25 | occurred_at | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 26 | FROM ordered_transactions | LAG가 붙은 거래 relation을 최종 projection 입력으로 읽는다. |
| 27 | ORDER BY account_id, occurred_at, tx_id; | account·time·tx_id 순서로 최종 LAG 결과를 안정화한다. |
| 29 | -- 첫 거래는 previous_amount와 amount_difference가 NULL이다. | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다. |
| 30 | -- account 101의 마지막 두 reversal은 600 -> 600이므로 마지막 차이는 0이다. | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다. 다만 workbook 제공 정답이 아니다
문법 해부
- `LAG(amount) OVER`는 row를 줄이지 않고 같은 partition의 직전 amount를 새 column으로 붙인다.
- `occurred_at, tx_id`가 같은 시각의 순서를 결정론적으로 푼다.
실행 순서
- business_tx 21행을 account별로 나눈다.
- 시간·tx_id 순서로 세운다.
- 직전 amount를 previous_amount로 붙인다.
- 현재-직전 difference를 계산하고 같은 순서로 출력한다.
W21 조각별 정밀 해설
F09-C01 · provenance assumptions and schema
- 문법 해부
- 1~5줄의 `provenance assumptions and schema`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `fixture rows=21`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `fixture rows=21`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: workbook 제공 정답이 아니다.
- 착각 방지
- `provenance assumptions and schema`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: workbook 제공 정답이 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F09-C02 · account window and LAG
- 문법 해부
- 6~17줄의 `account window and LAG`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `first previous=NULL`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `first previous=NULL`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 모든 status 포함은 명시한 가정이다.
- 착각 방지
- `account window and LAG`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 모든 status 포함은 명시한 가정이다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F09-C03 · difference projection and stable output
- 문법 해부
- 18~27줄의 `difference projection and stable output`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `account101 last difference=0`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `account101 last difference=0`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 첫 difference는 NULL이다.
- 착각 방지
- `difference projection and stable output`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 첫 difference는 NULL이다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F09-C04 · fixture oracles
- 문법 해부
- 28~30줄의 `fixture oracles`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `stable occurred_at+tx_id`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `stable occurred_at+tx_id`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: tx_id tie-breaker는 학습 선택이다.
- 착각 방지
- `fixture oracles`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: tx_id tie-breaker는 학습 선택이다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
COALESCE 0은 다른 정책이다.
-
료
왜 틀렸는지: 직전 row가 없으므로 SQL NULL이다.
-
키타
수정: NULL을 그대로 표시하고 의미를 설명한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F09-T01 첫 거래 | account opening | LAG | previous/difference NULL | 0으로 바꾸지 않는다 |
| F09-T02 account101 | 10000 -> 1000 | current-previous | -9000 | status를 필터하지 않는 가정 |
| F09-T03 same amount | last reversals 600 -> 600 | difference | 0 | 같은 시각은 tx_id로 푼다 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `비정본 LAG SQL 예시`이야.
-
료
대표 경계는 `tx_id tie-breaker는 학습 선택이다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
account마다 별도 previous-row state를 유지한다.
partition key를 빼면 다른 account가 섞인다.직전 row 값을 읽되 현재 row cardinality를 보존한다.
첫 row에는 predecessor가 없어 NULL이다.window와 같은 stable key로 결과를 표시한다.
표시 순서가 window 계산 key와 다르면 해석이 어려워진다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ 첫 거래 차이는 0이다
왜 틀리나 직전 row가 없으므로 SQL NULL이다.
바르게 읽기 NULL을 그대로 표시하고 의미를 설명한다.
반례 COALESCE 0은 다른 정책이다.
❌ LAG에는 ORDER BY가 없어도 된다
왜 틀리나 직전의 의미가 정해지지 않는다.
바르게 읽기 time과 tie-breaker를 명시한다.
반례 동일 실행에서도 순서가 달라질 수 있다.
❌ FAILED 거래는 자동 제외된다
왜 틀리나 query에 status filter가 없다.
바르게 읽기 전체 status 포함 가정을 주석으로 고정한다.
반례 FAILED amount도 차이에 들어간다.
❌ GROUP BY와 LAG는 같은 결과다
왜 틀리나 GROUP BY는 거래 row를 축약한다.
바르게 읽기 window로 21행을 유지한다.
반례 account별 한 행만 남으면 직전 거래를 잃는다.
❌ 이 query는 workbook 공식 답이다
왜 틀리나 PDF는 LAG 문제 계약만 준다.
바르게 읽기 비정본 label과 가정을 유지한다.
반례 다른 table/status/tie 정책도 가능한 해석이다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
workbook 제공 정답이 아니다
이 책임을 맡는 곳: 별도 selector/실행모든 status 포함은 명시한 가정이다
이 책임을 맡는 곳: secret·운영 정책첫 difference는 NULL이다
이 책임을 맡는 곳: concurrency·infrastructuretx_id tie-breaker는 학습 선택이다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: account별 직전 거래 금액을 LAG로 붙여 현재 금액과의 차이를 계산하는 비정본 학습 예시다.
2단계 · 코드 조각 재조립
- provenance assumptions and schema
- account window and LAG
- difference projection and stable output
- fixture oracles
3단계 · 파일 전체 다시 쓰기
30개 물리 줄을 원본 순서로 복원하고 SHA-256 cd249711b847141c8bd6c3884a4cab07ef4230d3dd2128066f5adb19bb4292ed와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
-- W21-SQL-Q33: 직전 거래와 금액 차이
-- 학습용 비정본 예시: workbook에는 제공 정답이 없다.
-- 가정: SUCCESS/FAILED/UNKNOWN/PROCESSING를 포함한 business_tx 전체를 거래 이력으로 읽는다.
-- grain/cardinality: 거래 1건당 1행, fixture 기준 21행.
SET search_path TO :"workbook_schema", public;
WITH ordered_transactions AS (
SELECT account_id,
tx_id,
status,
amount,
occurred_at,
LAG(amount) OVER (
PARTITION BY account_id
ORDER BY occurred_at, tx_id
) AS previous_amount
FROM business_tx
)
SELECT account_id,
tx_id,
status,
amount,
previous_amount,
amount - previous_amount AS amount_difference,
occurred_at
FROM ordered_transactions
ORDER BY account_id, occurred_at, tx_id;
-- 첫 거래는 previous_amount와 amount_difference가 NULL이다.
-- account 101의 마지막 두 reversal은 600 -> 600이므로 마지막 차이는 0이다.
10W21-SQL-Q34 — ROW_NUMBER로 고객별 최근 3건 찾기
illustrative/sql/W21-SQL-Q34.sql
학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F1036줄 연결36줄 번역4 chunks
W21-SQL-Q34 — ROW_NUMBER로 고객별 최근 3건 찾기
illustrative/sql/W21-SQL-Q34.sql
학습용 예시 · 정본 답안 아님 · 학습용 예시 · 정본 답안 아님 · W21-F10STEP 01 / 13
오늘 이 코드에서 해결할 문제
무엇을 이해해야 하는지 질문부터 잡습니다.
customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
- `fixture rows=8`은 어느 줄에서 생길까?
- 입력→처리→관찰→cleanup 순서는 무엇인가?
- producer와 read-only consumer는 누구인가?
- 직접 관찰하지 않은 Green은 무엇인가?
- counterexample은 `workbook 제공 정답이 아니다`과 어떻게 연결되는가?
fixture rows=8customer1=3 rowscustomer2=3 rowscustomer3/6=1 rowSTEP 02 / 13
아주 짧게: 이 코드는 왜 필요할까?
웹소설 대신 이 코드가 필요한 이유만 두 문단으로 쉽게 봅니다.
STARRY가 image 세트·DB 창고·host JVM 배우·evidence 영수증을 서로 다른 칸에 놓는다.
W21-SQL-Q34 — ROW_NUMBER로 고객별 최근 3건 찾기를 무대 운영표로 바꾸기
customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
핵심값 fixture rows=8, customer1=3 rows, customer2=3 rows, customer3/6=1 row을 원본 줄로 따라가되, `workbook 제공 정답이 아니다`까지 함께 표시해 Green 범위를 부풀리지 않는다.
딱 여기까지만 무대 비유는 ownership과 순서를 기억하게 할 뿐 Docker isolation, PowerShell native exit, Spring health group, SQL ordering을 자동 증명하지 않는다.
STEP 03 / 13
초등학생도 이해하는 설명
생활 비유와 실제 코드의 경계를 함께 확인합니다.
provenance assumptions and schema
1~5줄을 한 덩어리로 읽어 1번째 움직임을 본다. customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
- 코드 연결
1~5줄- 비유
- 고객마다 별도 줄을 세우고 가장 최근 거래부터 1·2·3 번호표를 붙여 세 장만 꺼내는 과정
- 비유의 끝
- workbook 제공 정답이 아니다
customer join and recent row number
6~24줄을 한 덩어리로 읽어 2번째 움직임을 본다. customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
- 코드 연결
6~24줄- 비유
- 고객마다 별도 줄을 세우고 가장 최근 거래부터 1·2·3 번호표를 붙여 세 장만 꺼내는 과정
- 비유의 끝
- 거래 없는 고객 제외는 명시한 가정이다
top-three projection
25~35줄을 한 덩어리로 읽어 3번째 움직임을 본다. customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
- 코드 연결
25~35줄- 비유
- 고객마다 별도 줄을 세우고 가장 최근 거래부터 1·2·3 번호표를 붙여 세 장만 꺼내는 과정
- 비유의 끝
- 모든 account/status 포함은 학습 선택이다
문제의 첫 장면히토리 → 니지카 → 료 → 키타
-
히토리
이 항목의 첫 관찰값은 `fixture rows=8` 맞아?
-
니지카
customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
-
료
첫 source 값은 `fixture rows=8`로 잡자.
-
키타
단, `workbook 제공 정답이 아니다`도 같이 적어.
입력에서 결과까지히토리 → 니지카 → 료 → 키타
-
히토리
`customer1=3 rows`은 언제 생겨?
-
니지카
순서를 두 칸씩 놓자. customer-account-transaction을 연결한다. → 고객별 partition을 만든다.
-
료
그 다음은 시간·tx_id 내림차순으로 recent_no를 붙인다. → 1~3만 남기고 customer/recent_no 순서로 출력한다.
-
키타
관찰값과 `거래 없는 고객 제외는 명시한 가정이다`을 분리하면 돼.
STEP 04 / 13
비유 ↔ 코드 전체 연결표
감사 규칙상 연결 대상인 원본 36줄을 빠짐없이 연결합니다.
| 줄 | 정확한 원본 줄 | STARRY 비유 | 실제 뜻·입력·결과·한계 |
|---|---|---|---|
| 1줄F10-L01 | -- W21-SQL-Q34: |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다.
|
| 2줄F10-L02 | -- 학습용 비정본 예시: |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | workbook이 배포한 정답이 아닌 학습용 예시임을 선언한다.
|
| 3줄F10-L03 | -- 가정: |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 문제에서 비운 inclusion/tie 정책을 명시적 가정으로 고정한다.
|
| 4줄F10-L04 | -- grain/ |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 입력·출력 행 단위와 fixture 예상 행 수를 주석으로 고정한다.
|
| 5줄F10-L05 | SET search_path TO : |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | workbook_schema를 우선 조회하도록 PostgreSQL search_path를 고정한다.
|
| 7줄F10-L07 | WITH ranked_transactions AS ( |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 고객별 recent_no를 계산할 ranked_transactions CTE를 연다.
|
| 8줄F10-L08 | SELECT c. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다.
|
| 9줄F10-L09 | c. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 9번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 10줄F10-L10 | a. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 10번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 11줄F10-L11 | t. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 11번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 12줄F10-L12 | t. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 12번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 13줄F10-L13 | t. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 13번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 14줄F10-L14 | t. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 14번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 15줄F10-L15 | ROW_NUMBER( |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 고객 partition 안에서 최신순 고유 번호 recent_no를 붙인다.
|
| 16줄F10-L16 | PARTITION BY c. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 최근 거래 순번을 customer마다 1부터 다시 시작한다.
|
| 17줄F10-L17 | ORDER BY t. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 가장 최근 거래가 1번이 되도록 time·tx_id 내림차순을 고정한다.
|
| 18줄F10-L18 | ) AS recent_no |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 비정본 최근 3건 SQL 예시에서 18번째 원문 문장을 앞뒤 단계와 연결한다.
|
| 19줄F10-L19 | FROM customer AS c |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | customer를 거래 identity join의 시작 relation으로 읽는다.
|
| 20줄F10-L20 | JOIN account AS a |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 각 customer에 속한 account를 inner join한다.
|
| 21줄F10-L21 | ON a. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 바로 앞 join·조건을 key equality로 제한한다.
|
| 22줄F10-L22 | JOIN business_tx AS t |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 각 account의 거래를 inner join해 거래 grain을 만든다.
|
| 23줄F10-L23 | ON t. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 바로 앞 join·조건을 key equality로 제한한다.
|
| 24줄F10-L24 | ) |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다.
|
| 25줄F10-L25 | SELECT customer_id, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다.
|
| 26줄F10-L26 | customer_name, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 27줄F10-L27 | recent_no, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 28줄F10-L28 | account_id, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 29줄F10-L29 | tx_id, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 30줄F10-L30 | status, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 31줄F10-L31 | amount, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 32줄F10-L32 | occurred_at |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다.
|
| 33줄F10-L33 | FROM ranked_transactions |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | recent_no가 붙은 거래 relation을 최종 projection 입력으로 읽는다.
|
| 34줄F10-L34 | WHERE recent_no <= |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | 각 customer partition의 1·2·3번 거래만 남긴다.
|
| 35줄F10-L35 | ORDER BY customer_id, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | customer와 recent_no 순서로 최종 top-3 결과를 안정화한다.
|
| 37줄F10-L37 | -- customer 1과 2는 각 3행, |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다.
|
| 38줄F10-L38 | -- 동일 시각의 tx 212와 211은 tx_id DESC로 순서를 고정한다. |
고객별 줄에서 최근 순서표 1·2·3을 붙이는 거래표 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다.
|
Green 범위만 말하기히토리 → 니지카 → 료 → 키타
-
히토리
marker가 보이면 전부 성공한 거야?
-
니지카
아니. 직접 읽은 process·image·health·SQL row까지만 말해야 해.
-
료
여기서 직접 묶이는 값은 `customer3/6=1 row`이야.
-
키타
`모든 account/status 포함은 학습 선택이다`는 별도 증거가 필요해.
STEP 05 / 13
원본 코드 조각
원본을 4개 의미 조각으로 나누어 그대로 확인합니다.
파일을 한꺼번에 외우지 않고 실행 의미가 이어지는 작은 조각으로 봅니다. 아래 코드는 prompt·fixture·가정을 밝힌 학습용 예시 source이며, 제공 정본 답안이 아닙니다.
-- W21-SQL-Q34: 고객별 최근 3건
-- 학습용 비정본 예시: workbook에는 제공 정답이 없다.
-- 가정: 모든 account 상태와 모든 business_tx 상태를 포함하고, 거래가 없는 고객은 결과에서 제외한다.
-- grain/cardinality: 고객별 최대 3개 거래, fixture 기준 8행.
SET search_path TO :"workbook_schema", public;
WITH ranked_transactions AS (
SELECT c.customer_id,
c.customer_name,
a.account_id,
t.tx_id,
t.status,
t.amount,
t.occurred_at,
ROW_NUMBER() OVER (
PARTITION BY c.customer_id
ORDER BY t.occurred_at DESC, t.tx_id DESC
) AS recent_no
FROM customer AS c
JOIN account AS a
ON a.customer_id = c.customer_id
JOIN business_tx AS t
ON t.account_id = a.account_id
)
SELECT customer_id,
customer_name,
recent_no,
account_id,
tx_id,
status,
amount,
occurred_at
FROM ranked_transactions
WHERE recent_no <= 3
ORDER BY customer_id, recent_no;
-- customer 1과 2는 각 3행, customer 3과 6은 각 1행이다.
-- 동일 시각의 tx 212와 211은 tx_id DESC로 순서를 고정한다.
STEP 06 / 13
코드 한 줄씩 한국어로 번역
비어 있지 않은 36줄을 모두 한국어로 옮깁니다.
비어 있지 않은 원본 줄은 하나도 생략하지 않습니다.
| 줄 | 원본 | 한국어 번역 |
|---|---|---|
| 1 | -- W21-SQL-Q34: 고객별 최근 3건 | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다. |
| 2 | -- 학습용 비정본 예시: workbook에는 제공 정답이 없다. | workbook이 배포한 정답이 아닌 학습용 예시임을 선언한다. |
| 3 | -- 가정: 모든 account 상태와 모든 business_tx 상태를 포함하고, 거래가 없는 고객은 결과에서 제외한다. | 문제에서 비운 inclusion/tie 정책을 명시적 가정으로 고정한다. |
| 4 | -- grain/cardinality: 고객별 최대 3개 거래, fixture 기준 8행. | 입력·출력 행 단위와 fixture 예상 행 수를 주석으로 고정한다. |
| 5 | SET search_path TO :"workbook_schema", public; | workbook_schema를 우선 조회하도록 PostgreSQL search_path를 고정한다. |
| 7 | WITH ranked_transactions AS ( | 고객별 recent_no를 계산할 ranked_transactions CTE를 연다. |
| 8 | SELECT c.customer_id, | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다. |
| 9 | c.customer_name, | 비정본 최근 3건 SQL 예시에서 9번째 원문 문장을 앞뒤 단계와 연결한다. |
| 10 | a.account_id, | 비정본 최근 3건 SQL 예시에서 10번째 원문 문장을 앞뒤 단계와 연결한다. |
| 11 | t.tx_id, | 비정본 최근 3건 SQL 예시에서 11번째 원문 문장을 앞뒤 단계와 연결한다. |
| 12 | t.status, | 비정본 최근 3건 SQL 예시에서 12번째 원문 문장을 앞뒤 단계와 연결한다. |
| 13 | t.amount, | 비정본 최근 3건 SQL 예시에서 13번째 원문 문장을 앞뒤 단계와 연결한다. |
| 14 | t.occurred_at, | 비정본 최근 3건 SQL 예시에서 14번째 원문 문장을 앞뒤 단계와 연결한다. |
| 15 | ROW_NUMBER() OVER ( | 고객 partition 안에서 최신순 고유 번호 recent_no를 붙인다. |
| 16 | PARTITION BY c.customer_id | 최근 거래 순번을 customer마다 1부터 다시 시작한다. |
| 17 | ORDER BY t.occurred_at DESC, t.tx_id DESC | 가장 최근 거래가 1번이 되도록 time·tx_id 내림차순을 고정한다. |
| 18 | ) AS recent_no | 비정본 최근 3건 SQL 예시에서 18번째 원문 문장을 앞뒤 단계와 연결한다. |
| 19 | FROM customer AS c | customer를 거래 identity join의 시작 relation으로 읽는다. |
| 20 | JOIN account AS a | 각 customer에 속한 account를 inner join한다. |
| 21 | ON a.customer_id = c.customer_id | 바로 앞 join·조건을 key equality로 제한한다. |
| 22 | JOIN business_tx AS t | 각 account의 거래를 inner join해 거래 grain을 만든다. |
| 23 | ON t.account_id = a.account_id | 바로 앞 join·조건을 key equality로 제한한다. |
| 24 | ) | 현재 block·CTE·map 범위를 닫고 다음 단계로 제어를 넘긴다. |
| 25 | SELECT customer_id, | 다음 줄들과 함께 최종 또는 window 입력 column projection을 연다. |
| 26 | customer_name, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 27 | recent_no, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 28 | account_id, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 29 | tx_id, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 30 | status, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 31 | amount, | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 32 | occurred_at | 현재 relation에서 이 column을 결과 또는 window 입력으로 전달한다. |
| 33 | FROM ranked_transactions | recent_no가 붙은 거래 relation을 최종 projection 입력으로 읽는다. |
| 34 | WHERE recent_no <= 3 | 각 customer partition의 1·2·3번 거래만 남긴다. |
| 35 | ORDER BY customer_id, recent_no; | customer와 recent_no 순서로 최종 top-3 결과를 안정화한다. |
| 37 | -- customer 1과 2는 각 3행, customer 3과 6은 각 1행이다. | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다. |
| 38 | -- 동일 시각의 tx 212와 211은 tx_id DESC로 순서를 고정한다. | fixture oracle 또는 SQL 책임 경계를 사람이 대조할 주석이다. |
STEP 07 / 13
기존 수준의 한 줄 읽기·문법 해부
쉬운 설명 다음에 문법과 실행 순서를 정밀하게 읽습니다.
한 줄로 읽기customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다. 다만 workbook 제공 정답이 아니다
문법 해부
- `ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY ... DESC)`가 고객마다 1부터 다시 번호를 매긴다.
- window 결과는 같은 SELECT의 WHERE에서 바로 쓸 수 없어 CTE 밖에서 `recent_no <= 3`으로 거른다.
실행 순서
- customer-account-transaction을 연결한다.
- 고객별 partition을 만든다.
- 시간·tx_id 내림차순으로 recent_no를 붙인다.
- 1~3만 남기고 customer/recent_no 순서로 출력한다.
W21 조각별 정밀 해설
F10-C01 · provenance assumptions and schema
- 문법 해부
- 1~5줄의 `provenance assumptions and schema`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `fixture rows=8`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `fixture rows=8`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: workbook 제공 정답이 아니다.
- 착각 방지
- `provenance assumptions and schema`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: workbook 제공 정답이 아니다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F10-C02 · customer join and recent row number
- 문법 해부
- 6~24줄의 `customer join and recent row number`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `customer1=3 rows`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `customer1=3 rows`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 거래 없는 고객 제외는 명시한 가정이다.
- 착각 방지
- `customer join and recent row number`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 거래 없는 고객 제외는 명시한 가정이다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F10-C03 · top-three projection
- 문법 해부
- 25~35줄의 `top-three projection`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `customer2=3 rows`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `customer2=3 rows`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 모든 account/status 포함은 학습 선택이다.
- 착각 방지
- `top-three projection`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 모든 account/status 포함은 학습 선택이다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
F10-C04 · fixture oracles
- 문법 해부
- 36~38줄의 `fixture oracles`을 입력·guard·관찰·cleanup 단계로 나눈다.
- 실제 값 추적
- 항목 전체 흐름에 연결해 `customer3/6=1 row`을 대조한다. 이 값이 chunk 단독 산출이라는 뜻은 아니다.
- 정상 예
- 정상 예에서는 source 순서와 owner 계약이 일치해 `customer3/6=1 row`을 관찰한다.
- 틀린 예·반례
- 반례에서는 선행 guard·stable order·cleanup을 생략한다. 동결 경계: 같은 시각은 tx_id DESC로 푼다.
- 착각 방지
- `fixture oracles`이 항목 전체 Green을 단독 증명한다고 읽지 않는다.
- 하지 않는 일
- 이 chunk가 책임지지 않는 범위: 같은 시각은 tx_id DESC로 푼다.
- 다음 연결
- 다음 source chunk의 입력 또는 최종 marker/SQL oracle로 결과를 넘긴다.
틀린 예 찾기히토리 → 니지카 → 료 → 키타
-
히토리
겉보기에는 맞지만 깨지는 예를 하나 골라 줘.
-
니지카
한 고객 거래만 남을 수 있다.
-
료
왜 틀렸는지: 전체 결과에서 3건만 자른다.
-
키타
수정: customer partition의 row_number를 필터한다.
STEP 08 / 13
실제 값 따라가기
같은 입력값이 어느 줄을 지나 어떤 결과가 되는지 추적합니다.
| 순서 | 들어온 값 | 코드가 하는 일 | 나온 값·상태 | 경계 |
|---|---|---|---|---|
| F10-T01 customer1 | accounts101+105 | latest DESC row_number | 3 rows | account 상태 전체 포함 가정 |
| F10-T02 customer2 | accounts102+106 | latest DESC row_number | 3 rows | PROCESSING도 포함한다 |
| F10-T03 sparse | customer3/6 | available rows | 1 row each | customer4/5는 transaction 없어 제외 |
책임 경계히토리 → 니지카 → 료 → 키타
-
히토리
이 파일이 책임지지 않는 마지막 칸은?
-
니지카
직접 역할은 `비정본 최근 3건 SQL 예시`이야.
-
료
대표 경계는 `같은 시각은 tx_id DESC로 푼다`이야.
-
키타
source SHA·owner·정본/비정본 label까지 대조하면 끝이야.
STEP 09 / 13
Docker·PowerShell·Spring·DB 내부에서 벌어지는 일
Docker·PowerShell·Spring·DB에서 실제로 일어나는 일과 증명 범위를 구분합니다.
각 business_tx가 customer identity와 연결된 한 행이 된다.
잘못된 join은 중복 거래를 만든다.tie여도 모든 row에 유일한 순번을 준다.
tx_id DESC 선택은 학습용 stable policy다.CTE 계산 뒤 recent_no 1~3을 남긴다.
전체 거래 cardinality와 고객별 결과 cardinality를 구분한다.STEP 10 / 13
흔한 착각과 틀린 예
그럴듯하지만 틀린 해석을 반례로 고칩니다.
❌ LIMIT 3이면 고객별 3건이다
왜 틀리나 전체 결과에서 3건만 자른다.
바르게 읽기 customer partition의 row_number를 필터한다.
반례 한 고객 거래만 남을 수 있다.
❌ ORDER BY time만 충분하다
왜 틀리나 같은 timestamp tie가 있다.
바르게 읽기 tx_id DESC를 tie-breaker로 둔다.
반례 tx211/212 순서가 불안정해진다.
❌ 거래 없는 고객도 자동 한 행이다
왜 틀리나 INNER JOIN이므로 사라진다.
바르게 읽기 제외 가정을 명시하거나 LEFT JOIN 정책을 별도 설계한다.
반례 customer4/5는 결과에 없다.
❌ ACTIVE account만 자동 포함된다
왜 틀리나 status filter가 없다.
바르게 읽기 모든 account 상태 포함을 주석으로 밝힌다.
반례 CLOSED account107 거래도 나온다.
❌ 이 SQL은 정본 답안이다
왜 틀리나 PDF는 ROW_NUMBER prompt만 제공한다.
바르게 읽기 비정본 label·grain·tie 가정을 유지한다.
반례 다른 inclusion policy도 가능하다.
STEP 11 / 13
이 코드가 보장하지 않는 것
이 코드가 책임지지 않는 일을 분리합니다.
workbook 제공 정답이 아니다
이 책임을 맡는 곳: 별도 selector/실행거래 없는 고객 제외는 명시한 가정이다
이 책임을 맡는 곳: secret·운영 정책모든 account/status 포함은 학습 선택이다
이 책임을 맡는 곳: concurrency·infrastructure같은 시각은 tx_id DESC로 푼다
이 책임을 맡는 곳: owner lifecycleSTEP 12 / 13
직접 다시 써보기
뜻 → 조각 → 전체 코드 순서로 다시 씁니다.
1단계 · 뜻부터 복원
다음 역할을 고정값과 미보장 경계까지 말한다: customer별 거래를 최신순 ROW_NUMBER로 매겨 상위 3건만 고르는 비정본 학습 예시다.
2단계 · 코드 조각 재조립
- provenance assumptions and schema
- customer join and recent row number
- top-three projection
- fixture oracles
3단계 · 파일 전체 다시 쓰기
38개 물리 줄을 원본 순서로 복원하고 SHA-256 9572e013c586c1f78e5e3e1236971560e8f8d3cf6b72a16af39e8f1766e24faa와 대조한다.
자가 점검
- 원본 source/template bytes를 바꾸지 않았는가?
- 입력·처리·관찰·cleanup을 구분했는가?
- D3 producer와 D6/D7 consumer를 바꾸지 않았는가?
- 8080과 runner default 28021을 같은 값으로 섞지 않았는가?
- Q33/Q34를 workbook 정본 답안이라고 부르지 않았는가?
STEP 13 / 13
전체 원본 source
감사로 고정한 전체 source를 가감 없이 확인합니다.
학습용 예시 전체 확인하기 · 정본 답안 아님
-- W21-SQL-Q34: 고객별 최근 3건
-- 학습용 비정본 예시: workbook에는 제공 정답이 없다.
-- 가정: 모든 account 상태와 모든 business_tx 상태를 포함하고, 거래가 없는 고객은 결과에서 제외한다.
-- grain/cardinality: 고객별 최대 3개 거래, fixture 기준 8행.
SET search_path TO :"workbook_schema", public;
WITH ranked_transactions AS (
SELECT c.customer_id,
c.customer_name,
a.account_id,
t.tx_id,
t.status,
t.amount,
t.occurred_at,
ROW_NUMBER() OVER (
PARTITION BY c.customer_id
ORDER BY t.occurred_at DESC, t.tx_id DESC
) AS recent_no
FROM customer AS c
JOIN account AS a
ON a.customer_id = c.customer_id
JOIN business_tx AS t
ON t.account_id = a.account_id
)
SELECT customer_id,
customer_name,
recent_no,
account_id,
tx_id,
status,
amount,
occurred_at
FROM ranked_transactions
WHERE recent_no <= 3
ORDER BY customer_id, recent_no;
-- customer 1과 2는 각 3행, customer 3과 6은 각 1행이다.
-- 동일 시각의 tx 212와 211은 tx_id DESC로 순서를 고정한다.