Releases: Ahngbeom/dba-guide
Release list
v1.5.0 — 좁은 창에서도 읽히고, 어디서든 나갈 수 있고, 세 벤더가 같이 실습한다
v1.4.0이 챕터 본문을 터미널용으로 조판하기 시작했다. 다만 그 조판은 처음 그린 폭을 유지할 때만 온전했다 — 창을 좁히면 코드가 토큰 중간에서 잘리고 표가 뭉개졌다. 이번 릴리스는 그 조판이 창 크기를 바꿔도 무너지지 않게 만들고, 화면 어디서든 한 타로 빠져나올 길을 내고, 다 읽은 챕터에서 시험으로 넘어가는 길을 막지 않는 형태로 바꾼다. 그리고 mysql·oracle 단일 벤더 뷰에서 통째로 사라져 있던 실습 예제를 돌려준다.
챕터 읽기 — 창을 좁혀도 무너지지 않는다 (#96)
화면에서 복사한 명령이 이제 그대로 실행된다. 코드 펜스를 감싸던 상자를 걷어냈다 — 상자가 렌더 폭에 맞춰 하드랩되면서 명령을 토큰 중간에서 잘랐기 때문이다. 실측하면 80칸에서 코드 줄의 6%, 60칸에서 25%, 40칸에서 **49%**가 잘렸다. 대신 짧은 언어 라벨과 4칸 들여쓰기만 붙이고 코드는 손대지 않고 그대로 내보낸다. 긴 줄을 접는 것은 less의 몫이다.
표는 더 이상 억지로 욱여넣지 않는다. 자연 폭이 화면에 들어갈 때만 정렬해 그리고, 넘치면 행 하나를 카드 한 장으로 편다(▸ 제목 아래에 헤더: 값). 이 학습서의 표는 자연 폭 중앙값이 130칸이라, 44개 표 중 36개가 80칸에서 이미 뭉개지고 있었다. 40칸에서는 PostgreSQL이 PostgreSQ/L로 갈라졌다. 열을 압축하는 코드는 아예 남기지 않았다.
제목은 잘리지 않는다. 폭을 넘으면 줄바꿈하면서 제목 스타일을 다음 줄까지 이어 간다 — 스타일이 접힌 뒷줄을 본문과 구별해 주는 유일한 단서인데, 꼬리를 통째로 잃는 쪽이 더 나빴다. 가로선은 폭을 채우는 대신 24칸에서 멈춘다.
바탕에 깔린 규칙은 하나다. 렌더된 줄은 줄머리에 의미를 담지 않는다. less는 소프트랩된 다음 줄에 앞머리 글리프나 여백을 다시 그려 주지 않으므로, 거기 실어 둔 정보는 독자가 창을 좁히는 순간 사라진다. 위 세 가지가 모두 이 규칙에서 나왔고, 각각 그 이유를 이름에 적은 테스트가 지킨다.
어디서든 Q (#95)
어느 화면에 있든 대문자 Q 한 타로 앱이 끝난다. Esc와 q는 한 단계 뒤로 나간다 — 챕터 목록에서 티어로, 티어에서 DBMS로. 메뉴 깊숙이 들어간 뒤 빠져나오려고 들어간 단계 수만큼 누를 일이 없어졌다.
챕터를 닫을 때마다 뜨던 "계속하려면 Enter"도 없앴다. 페이저에서 나오면 곧바로 챕터 목록이다.
예외는 둘이고 둘 다 의도한 것이다. 장애 대응 게임이 진행 중일 때 뜨는 서버 선택 화면은 Q를 막아 뒀다 — 그 화면에서 앱을 끊으면 dbshoot-* 컨테이너가 뜬 채 남는다. 취소는 Esc로 한다. 그리고 학습 점검(exam) 화면은 아직 자체 키 계약을 쓴다(아래 「알려진 한계」).
읽고 나서 바로 풀기
챕터 목록에서 x를 누르면 그 챕터의 문제은행이 곧바로 열린다. 다 읽고 나와서 이어 풀 수도 있고, 목록만 훑다가 풀 챕터를 골라도 된다.
목록이 그 선택에 필요한 것을 함께 보여 준다. 문제은행이 없는 챕터에는 [시험 없음]이 붙고, 이미 푼 챕터에는 [지난 최고 A·92%]가 붙는다. 어느 챕터에 시험이 있고 어디를 다시 봐야 하는지 일일이 눌러 보고 알아낼 필요가 없다.
읽고 나올 때마다 묻던 [Y/n] 프롬프트는 없앴다. 기본값이 예였던 탓에, 예전의 "계속하려면 Enter"를 치우던 습관적인 Enter가 그대로 시험을 시작시켰다. 시험은 이제 명시적으로 x를 누를 때만 시작한다.
학습서 — mysql·oracle 독자에게 실습을 돌려준다 (#90)
02-intermediate/04-backup-recovery-strategies.md의 「실습 예제」가 통째로 dbms:postgresql 마커 안에 들어 있었다. main에서는 멀쩡해 보였지만 mysql·oracle 브랜치에서는 그 절이 통째로 사라져, 두 벤더 독자에게는 손으로 해 볼 것이 하나도 없었다.
이제 시나리오는 공유한다 — 오후 2시 30분에 실수로 전체 DELETE가 났고 2시 29분 상태로 되돌린다는 하나의 상황을 두고, 세 벤더가 각자의 완전한 절차를 갖는다. PostgreSQL은 base backup + WAL 재생, MySQL은 전체 백업 복원 + binlog 재적용, Oracle은 RMAN 불완전 복구다. 원리가 같고 다른 것은 로그의 이름과 재적용을 멈추는 방법, 복구를 끝내고 서비스를 여는 절차라는 점이 본문에 드러난다. 체크리스트 항목도 벤더별로 짝을 맞췄다.
같은 부류를 다시 들이지 않도록 네 절 구조 검사가 세 단일 벤더 뷰에도 돌아간다. 손으로는 python3 scripts/check_content.py --dbms mysql, CI에서는 ShippedContentTest가 세 벤더 모두를 main 위에서 검사한다. 검사에 쓰는 필터는 벤더 브랜치를 만드는 filter_dbms.filter_lines 바로 그 함수라, 검사와 실제 브랜치가 갈라질 수 없다.
postgresql·mysql·oracle 세 벤더 브랜치는 이 태그 시점의 main에서 재생성해 push한다. 이번에 고친 결함은 그 브랜치에서만 보이던 것이라, 재생성이 끝나야 세 벤더 독자의 손에 실제로 실습 예제가 들어간다.
알려진 한계
발견되지 않은 버그가 아니라 알고 감수하는 경계다.
- East Asian Ambiguous 글리프(
─ │ ━ • · ◆)를 2칸으로 그리도록 설정한 터미널에서는 렌더된 줄 256개가 80칸을 넘는다(이번 작업 전에는 853줄).tui.cwidth가 이 글리프를 1칸으로 세는 것은 저장소 전역의 가정이고, 그 터미널 설정을 이식 가능하게 탐지할 방법이 없다. - 학습 점검(
exam) 화면은 아직 자체 키 계약을 쓴다 —q는 시험만 끝내고Esc는 반응하지 않는다. 키 계약이 다른 유일한 화면이다(아래 서버 선택 화면도Q로 앱이 끝나지는 않지만, 그쪽은 같은 계약 위에서 종료만 막아 둔 것이다). - 장애 대응 게임이 진행 중일 때 서버 선택 화면은
Q를 막아 뒀다. 게임을 도중에 끊으면dbshoot-*컨테이너가 뜬 채 남기 때문이다 — 그 화면은Esc로 취소한다. - PostgreSQL 판정의 구멍 둘은 그대로다. 파싱 오류가 감시 소스에 남지 않고(실측 — 없는 테이블·권한 오류는 잡히고
KILL 999999는 놓친다), pid를 적지 않는pg_terminate_backend쓸기는kill_precision이 보지 못한다(forbidden_command로 막는다). - 장애 대응 스테이지의 벤더 분포는 MySQL 13 / PostgreSQL 1 / Oracle 0이다 (#79).
- Oracle RMAN 절차는 실행 검증이 아니라 문서 근거로 작성됐다. 실습 랩(
shooting/lab/)에는 MySQL과 PostgreSQL만 있어 이 저장소 안에서 돌려 볼 수단이 없다. - 치트시트 행, 용어집·비교 매트릭스 반영, 문제은행 내용의 최신성은 여전히 사람 몫이다. 검사기는 파일의 존재와 절 구조만 판정한다.
커밋 46개, 파일 27개, +7,938 / −373. 이 가운데 +5,714는 docs/superpowers/의 설계·계획 문서이고, 실행 코드와 테스트는 +2,119 / −352이다. 테스트 스위트는 v1.4.0의 792개에서 897개로 늘었다. 챕터 31개, 문제은행 23개 / 225문항, 스테이지 14개는 v1.4.0과 같다 — 이번 릴리스는 전부 도구와 조판 쪽이다.
전체 변경 이력: https://github.com/Ahngbeom/dba-guide/commits/v1.5.0
호환성: 더해지고 고쳐졌다. 제거되거나 의미가 바뀐 ./exam·./shoot CLI 인자, 스테이지·문제은행 JSON 스키마 변경, .shooting-progress·.exam-results 기록 형식 변경, 챕터 경로 변경 모두 없다. 눈에 보이는 동작 둘이 사라지지만(챕터를 닫을 때의 "계속하려면 Enter" 대기와 그 뒤의 [Y/n] 시험 프롬프트) 어느 쪽도 인자나 파일이 아니라 지나가는 화면이고, 그 자리에서 하던 일은 챕터 목록의 x로 명시적으로 할 수 있다.
v1.4.0 — 챕터가 읽을 만해지고, 점검이 자동으로 돈다
v1.1.0에서 챕터 읽기가 생겼을 때, 본문은 마크다운 원문 그대로 $PAGER로 넘어갔다. #과 **와 |가 눈에 그대로 밟혔다. 이번 릴리스는 그걸 렌더링하고, 그동안 사람 손에만 맡겨 뒀던 점검 일부를 기계에 넘긴다.
새로 생긴 것 — 터미널 마크다운 렌더러
./guide → 챕터 읽기로 연 본문이 이제 터미널용으로 조판되어 나온다. 표준 라이브러리만 쓰는 scripts/markdown_render.py이며, 외부 렌더러를 부르지 않는다.
- 제목·문단·구분선과 HTML 주석(벤더 마커) 처리
- 펜스 코드 블록을 상자로 감싸고, 그 안에서는 마크업을 해석하지 않는다 — 코드 안의
*가 강조로 먹히던 문제가 사라진다 - 목록·체크박스·인용문에 행잉 인덴트 — 두 번째 줄부터 첫 줄의 글머리 위치에 맞춰 들어간다
- 파이프 표를 열 너비에 맞춰 정렬하고, 넘치는 칸은 줄바꿈한다. 이 학습서는 DBMS 3종을 표로 비교하는 문서라 표가 많다
- 깊게 중첩된 목록·인용문, 언어 태그가 긴 펜스 헤더에서 폭이 넘치던 것을 고쳤다
색은 페이저가 받아 줄 때만 쓴다. less는 -R 없이는 ANSI 이스케이프를 그대로 찍어 버리므로, 페이저를 판별해 필요하면 -R을 붙인다. bat·delta는 목록에서 뺐다 — 그건 페이저가 아니라 포매터라, 우리가 조판한 결과를 한 번 더 조판한다.
CI가 생겼다
.github/workflows/tests.yml 하나. 테스트 스위트만 돌린다.
docs/release-policy.md의 「릴리스 전 점검」 10개 항목 중 자동으로 막아 줄 수 있는 것만 여기서 처리하고, 나머지(벤더 브랜치 재생성, 인스톨러 실검증 등)는 여전히 사람 몫이다. ./shoot doctor는 일부러 넣지 않았다 — docker와 DB 클라이언트 유무에 따라 결과가 갈리는데, 점검 목록이 doctor에 기대했던 「스테이지 정의 파싱」은 이미 ShippedStagesTest가 검증한다.
첫 실행이 곧바로 하나를 잡았다. macOS 러너에는 docker가 없는데, ClientCheckSymmetryTest가 머신의 docker를 읽고 있었다. 로컬에서 몇 달간 초록이던 테스트가 CI가 도는 첫날 빨개졌다 — 환경 차이를 가정하지 않은 테스트는 그 환경을 만나기 전까지 아무것도 증명하지 않는다.
scripts/check_content.py가 점검 목록의 기계로 판정 가능한 절반을 맡는다 — 상대 링크가 실제로 풀리는지, 티어·부록 문서가 README.md에서 빠지지 않았는지, 모든 챕터가 네 절 구조를 지키는지, 문제은행이 스키마에 맞는지. 링크 검사는 코드 펜스와 인라인 코드를 건너뛴다(문서가 자기 문법을 설명하는 자리를 오탐하지 않기 위해).
scripts/generate-branch.sh도 이제 실제로 돌려서 검증한다(tests/test_generate_branch.py) — 임시 저장소를 만들어 진짜 스크립트를 실행한다. v1.1.0에서 "브랜치를 실제로 잘라 돌려 봐야만 드러난다"고 적었던 종류의 결함을 이제 스위트가 잡는다.
install.sh — 커스텀 설치 경로를 기억한다
v1.2.0에서 인스톨러가 생긴 뒤 남아 있던 함정을 닫는다.
XDG_DATA_HOME으로 다른 경로에 설치한 사람이 다음 실행에서 그 값을 빠뜨리면, 스크립트는 지난 설치본을 찾지 못해 기본 경로에 두 번째 설치본을 만들고 링크를 그쪽으로 옮겼다. 원래 설치본과 학습 기록은 고아가 됐고, --purge는 더 나빴다 — 엉뚱한 트리를 지우고 진짜는 남겼다.
계약은 한 문장이다. 주면 그 값을 따르고, 주지 않으면 지난 위치를 기억한다.
- 클론에 성공한 순간 그 경로가
${XDG_STATE_HOME:-~/.local/state}/dba-guide/install-path에 기록된다. - 기록하는 것은 이번 실행이 직접 내려받은 트리뿐이다. 이미 있던 디렉터리를 지정한 경우에는 기록하지 않고, 그 사실을 화면에 알린다. 우리가 만들지 않은 트리에 소유권을 주장하면 한 번의 지정이 영구 조준점이 되어, 나중에 맨손으로 친
--purge가 엉뚱한 곳을 지운다. - 명시한
XDG_DATA_HOME이 기록을 이긴다. 그러지 않으면 설치를 옮길 방법이--purge(= 백업 없는 학습 기록 삭제)밖에 남지 않는다. --purge는 설치 경로가 심볼릭 링크면 거부한다.rm -rf는 링크만 지우고 트리를 남긴 채 "삭제했습니다"를 찍기 때문이다.
v1.3.0 이전에 커스텀 경로로 설치했다면 기록이 없다. 그 설치본은 이번 실행이 만든 것이 아니므로 기록하지 않으니, XDG_DATA_HOME을 업데이트·제거에 계속 함께 준다(v1.3.0과 같다). README.md에 적어 뒀다.
학습 점검
03-advanced/06-automation-and-iac 은행에 문항을 더해 --dbms 필터가 실제로 걸리게 했다(216 → 225문항). 단답 채점이 사람들이 실제로 치는 표기를 받아들이도록 다섯 문항의 허용 답안을 넓혔다.
벤더 브랜치
postgresql/mysql/oracle 세 브랜치를 이 릴리스 시점의 main에서 재생성해 push했다. 세 브랜치 모두 Ran 792 tests … OK (skipped=3), 챕터 마커 잔여 0, install.sh는 main과 바이트 동일.
검증에 대해
이 릴리스의 인스톨러 변경은 적대적 코드 리뷰 6라운드를 거쳤다. 그 과정에서 배운 것을 남긴다 — 처음 세 라운드는 매번 리뷰에 대한 대응 자체가 그 시점 최악의 결함을 만들었다.
원인은 하나였다. 소유권의 대리 지표를 다른 대리 지표로 바꿨을 뿐, 그 지표가 현실에서 어떤 상태를 갖는지 열거하지 않았다. 런처 링크는 in-place 설치에서 기여자의 작업 클론을 가리키고, "HEAD가 detached면 우리 것"이라는 판정은 서브모듈·워크트리·태그 확인 중인 클론이 전부 통과하며, 정작 릴리스 직후의 우리 설치본은 브랜치 위에 남는다. 답은 추론을 더하는 것이 아니라 아는 범위로 되돌리는 것이었다.
테스트에 대해서도 같은 것을 배웠다. 통과하는 테스트 수는 커버리지가 아니다 — 링크의 모양만 검사하고 실행하지 않았고, cwd를 숨은 입력으로 쓰는 함수를 언제나 저장소 루트에서만 돌렸고, 픽스처가 쉬운 분기만 타게 만들어 실제 설치가 지나는 경로를 통째로 비껴갔다. 지금은 각 불변식을 지우면 지정된 테스트가 실패하는 것까지 확인했다.
알려진 한계
발견되지 않은 버그가 아니라 알고 감수하는 경계다.
인스톨러
XDG_DATA_HOME을 자기dba-guide클론의 부모로 직접 지정하면 그 클론이 fetch·detach 된다.--purge의 삭제 가드도is_our_install(저장소 루트 +scripts/guide.py)뿐이라 기여자의 클론을 통과시킨다. 소유권은 git 상태에서 추론할 수 없다는 것이 이번 릴리스의 결론이고, 그래서 막는 대신 적어 둔다.- 설치 경로에
origin원격이 없는 저장소가 있으면 영어 git 오류로 죽는다. --purge는 파이프로 실행할 수 없다(확인에 tty가 필요하다). PATH 등록은 자동으로 하지 않는다.- Windows 네이티브 미지원. Homebrew·PyPI·컨테이너 이미지도 없다.
렌더러
- 표준 라이브러리만 쓰는 조판기다. 중첩 표, 각주, 정의 목록 같은 확장 문법은 다루지 않는다.
- 색은 페이저가 ANSI를 받아 줄 때만 켜진다. 판별에 실패하면 색 없이 나온다 — 읽기에는 지장이 없다.
판정 (./shoot)
- MySQL: 플레이어에게
SYSTEM_VARIABLES_ADMIN이 있어 엔진과 같은 방법으로 자기 명령을 로그에서 뺄 수 있다. 로그를 읽고 비우는 사이 수 ms의 유실 창이 있고, 로그 테이블에DELETE가 막혀 있어 고칠 수 없다. - PostgreSQL: 파싱 오류가 감시에 남지 않는다(실측 — 없는 테이블·권한 오류는 잡히고
KILL 999999는 놓친다). pid를 적지 않는pg_terminate_backend쓸기는kill_precision이 못 본다.
커버리지 공백
- 스테이지 14개 중 MySQL 13 / PostgreSQL 1. Oracle 스테이지는 없다.
- 문항 벤더 표기가 전부
neutral인 은행이 하나 남아 있다(01-beginner/01).
운영
- CI는 테스트만 돌린다. 벤더 브랜치 재생성, 릴리스 발행, 인스톨러 실검증은 여전히 사람이 손으로 한다.
- 인스톨러는 최신 태그를 설치하므로, 이 릴리스를 발행해야 비로소 그 내용이 배포된다.
요약
| v1.3.0 | v1.4.0 | |
|---|---|---|
| 챕터 | 31 | 31 |
| 문제은행 / 문항 | 23 / 216 | 23 / 225 |
| 스테이지 | 14 | 14 |
| 챕터 읽기 | 마크다운 원문 | 터미널 렌더링 |
| CI | 없음 | 테스트 스위트 (macOS·Linux, Python 3.9·3.13) |
| 릴리스 점검 자동화 | 없음 | check_content.py (링크·구조·은행) |
| 커스텀 설치 경로 | 매번 지정 | 기억한다 (직접 내려받은 것만) |
| 테스트 | 592 | 792 |
커밋 46개. 전체 변경 이력: v1.3.0...v1.4.0
호환성: 더해지고 고쳐졌다. 제거되거나 의미가 바뀐 CLI 인자, 스키마 변경, 진행·결과 기록 형식 변경, 챕터 경로 변경 모두 없다. install.sh에 상태 파일이 하나 생기지만(~/.local/state/dba-guide/install-path) 없어도 v1.3.0과 똑같이 동작한다.
v1.3.0 — 읽기가 첫 줄로, 그리고 설치본에서도 문서대로
./guide를 열면 이제 챕터 읽기가 첫 줄이다.
무엇을 할까요
1) 챕터 읽기 31챕터
2) 학습 점검 (퀴즈/시험) 216문항
3) 장애 대응 (실전 훈련) 14스테이지
메뉴 순서가 곧 학습 순서다 — 읽기 → 확인 → 겪기. ./guide를 처음 여는 사람은 아직 아무것도 읽지 않았으므로, 읽지 않은 내용을 묻는 퀴즈로 먼저 보내지 않는다.
문서가 이미 맞고 코드가 틀렸던 경우다. README.md는 v1.1.0부터 "챕터 읽기 · 학습 점검 · 장애 대응"이라고 적어 왔는데 메뉴만 반대 순서였다.
설치본으로 쓰는 사람의 경로를 훑었다
v1.2.0에서 설치 기능이 생기면서 "저장소 안에서 실행한다"는 오래된 가정이 처음으로 문제가 됐다. v1.2.1이 런처를 고쳤고, 이번에는 같은 축으로 남은 자리를 마저 닫는다.
./exam의 대상 인자가 어디서 실행하든 같게 풀린다
README가 안내하는 세 형식 중 둘이 동작하지 않았다.
| 형식 | v1.2.1까지 | v1.3.0 |
|---|---|---|
exam exams/01-beginner/….json |
저장소 루트에서만 | 어디서든 |
exam 01-beginner/….json |
어디서도 안 됨 | 어디서든 |
exam 01-beginner |
동작 | 동작 |
원인은 하나였다 — 대상 해석의 .json 분기가 준 경로만 보고, exams/ 폴백이 디렉터리 분기에만 있었다. 그래서 티어 지정만 멀쩡했고, 그것만 쓰이는 동안 드러나지 않았다.
두 번째 줄은 v1.0.0 이래 한 번도 동작한 적이 없다. 설치 기능이 이 결함의 성격을 바꿨다 — 전에는 저장소 안에서만 쓰였으니 문서의 오타 정도였지만, 이제는 설치한 사람이 문서대로 따라 하면 반드시 막히는 자리다.
랩이 멈췄을 때 안내하는 명령이 절대경로가 됐다
docker compose -f shooting/lab/compose.yaml logs는 붙여넣어 쓰라는 명령인데 상대경로였다. 저장소 루트에 서 있지 않으면 실패한다. 엔진이 실제로 쓰는 경로는 늘 절대경로였으니 동작에는 문제가 없었고 안내 문장만 틀렸다.
첫 설치가 "이미 최신입니다"로 시작하지 않는다
릴리스 직후에는 최신 태그가 main의 tip에 있어, 갓 클론한 HEAD가 곧 그 태그다. 그래서 조기 반환 분기를 그대로 타 모든 첫 설치가 "이미"라는 말로 시작했다. 재실행에서는 그대로 나온다.
검증에 대해 — 테스트가 없어서가 아니라, 있을 수 없는 모양이어서
v1.2.1 노트가 "링크의 모양만 검사하고 동작을 검사하지 않았다"를 남겼다. 이번 것은 결이 조금 다르다.
대상 해석 함수에는 테스트가 하나도 없었다. 그런데 있었더라도 잡히지 않았을 가능성이 크다 — 이 함수는 현재 디렉터리를 암묵적 입력으로 쓰는데, 테스트는 늘 저장소 루트에서 돌기 때문에 그 입력이 언제나 "맞는 값"이었다. 결함은 입력이 달라질 때만 보이고, 그 입력을 바꾸는 것은 설치 기능이 생기기 전까지 아무도 하지 않았다.
지금은 테스트 7개가 세 형식을 저장소 안과 밖 양쪽에서 돌린다. cwd를 명시적으로 바꾸는 것이 핵심이다.
네 건 모두 수정 전에 실패하는 것을 먼저 확인했고, 마지막에는 로컬 저장소를 file:// 원본으로 삼아 실제 설치한 뒤 설치본에서 직접 돌려 확인했다.
벤더 브랜치
postgresql/mysql/oracle 세 브랜치를 이 릴리스 시점의 main에서 재생성해 push했다. 세 브랜치 모두 Ran 600 tests … OK (skipped=1), 챕터 마커 잔여 0.
알려진 한계
v1.2.0의 「알려진 한계」가 그대로 유효하다 — curl | bash 신뢰 모델, --purge의 tty 요구, PATH 자동 등록 안 함, MySQL 판정 회피, PostgreSQL 판정의 두 구멍, Oracle 스테이지 부재, CI 부재. 이번 릴리스에서 새로 늘거나 준 것은 없다.
shoot doctor의 [ok] compose 파일 shooting/lab/compose.yaml은 여전히 상대경로다. 붙여넣는 명령이 아니라 표시용 라벨이라 짧은 쪽을 택했다.
요약
| v1.2.1 | v1.3.0 | |
|---|---|---|
| 챕터 | 31 | 31 |
| 문제은행 / 문항 | 23 / 216 | 23 / 216 |
| 스테이지 | 14 | 14 |
./guide 첫 항목 |
학습 점검 | 챕터 읽기 |
README의 ./exam 형식 3개 중 동작하는 수 |
1 | 3 |
| 테스트 | 592 | 600 |
커밋 4개. 전체 변경 이력: v1.2.1...v1.3.0
호환성: 제거되거나 의미가 바뀐 CLI 인자, 스키마 변경, 진행·결과 기록 형식 변경, 챕터 경로 변경이 없다. ./exam의 대상 인자는 되던 것이 그대로 되고 안 되던 것이 되기 시작하므로 하위 호환이다. 메뉴 순서는 바뀌었지만 고르는 방식은 같다.
v1.2.1 — 설치된 명령이 실제로 실행된다
v1.2.0으로 설치했다면 이 버전으로 올려야 한다. 설치된 guide·exam·shoot 세 명령이 전부 동작하지 않았다.
$ exam
can't open file '~/.local/bin/scripts/exam.py': [Errno 2] No such file or directory
이미 v1.2.0을 설치한 경우, 설치 명령을 그대로 다시 실행하면 고쳐진다.
curl -fsSL https://raw.githubusercontent.com/Ahngbeom/dba-guide/main/install.sh | bash저장소를 클론해 ./guide로 쓰던 사람은 영향이 없다. 깨진 것은 심볼릭 링크 경유 호출뿐이었다.
무엇이 깨졌나
세 런처가 자기 위치를 dirname "${BASH_SOURCE[0]}"로 찾았다. 그 값은 심볼릭 링크를 따라가지 않고 링크 자신의 경로다. install.sh가 런처를 ~/.local/bin에 링크하므로, 링크로 부르면 있지도 않은 ~/.local/bin/scripts/exam.py를 찾았다.
결함은 인스톨러가 아니라 런처에 있었다. 런처는 v1.0.0부터 ./exam 형태로만 불렸기에 이 가정이 드러날 일이 없었고, v1.2.0이 처음으로 링크 경유 호출을 만들면서 잠재 결함을 실제 결함으로 바꿨다. 새 기능이 손대지 않은 옛 코드의 미검증 가정을 활성화한 경우다 — 바뀐 파일은 install.sh인데 깨진 파일은 exam이었다.
readlink -f는 이식성이 없어 링크 사슬을 직접 따라간다. 상대 대상은 링크가 있던 디렉터리 기준으로 푼다.
왜 592개 테스트가 놓쳤나
이번 릴리스가 남길 가치가 있는 건 수정보다 이쪽이다. 구멍이 두 겹이었다.
self.assertEqual(os.readlink(link), str(self.install_dir / name))링크가 가리키는 문자열만 보고 그 명령을 한 번도 실행하지 않았다. 링크의 모양을 검사했지 링크가 동작한다는 사실을 검사하지 않았다.
그리고 테스트 픽스처가 런처를 echo guide 스텁으로 대신하고 있었다. 진짜 런처의 경로 해석이 애초에 테스트의 사정거리 밖이었다는 뜻이다 — 첫 번째 구멍만 메우고 스텁을 그대로 뒀다면 여전히 통과했을 것이다.
v1.2.0 노트에 "통과하는 테스트 수는 커버리지가 아니다"라고 적어 두고 그 아래에서 같은 실수를 하고 있었다.
이제 픽스처는 저장소의 진짜 런처를 복사하고, LinkedCommandTest가 설치된 명령을 심볼릭 링크로 실제 실행한다. cwd는 일부러 설치 경로 바깥에 둔다. 인자가 그대로 넘어가는지도 함께 본다. 수정 전에 이 테스트가 정확히 그 이유로 실패하는 것을 확인했다.
AssertionError: 2 != 0 : exam: can't open file '.../.local/bin/scripts/exam.py'
그 밖에
CLAUDE.md에 이 제약을 기록했다 — 런처를 고칠 다음 사람이 같은 자리를 다시 밟지 않도록.postgresql/mysql/oracle세 브랜치를 이 릴리스 시점의main에서 재생성해 push했다. 세 브랜치 모두Ran 592 tests … OK (skipped=1), 챕터 마커 잔여 0.
알려진 한계
v1.2.0의 「알려진 한계」가 그대로 유효하다 — 인스톨러의 curl | bash 신뢰 모델과 --purge의 tty 요구, MySQL 판정 회피, PostgreSQL 판정의 두 구멍, Oracle 스테이지 부재, CI 부재. 이번 릴리스에서 새로 늘거나 준 것은 없다.
한 가지 덧붙인다. 이 결함은 자동 검증이 아니라 발행 직후 손 점검에서 잡혔다. docs/release-policy.md가 요구하는 그 점검이다. CI가 없는 저장소에서 그 목록이 실제로 무엇을 막고 있는지를 보여준 사례로 남긴다.
요약
| v1.2.0 | v1.2.1 | |
|---|---|---|
| 챕터 | 31 | 31 |
| 문제은행 / 문항 | 23 / 216 | 23 / 216 |
| 스테이지 | 14 | 14 |
| 설치 후 명령 동작 | 깨짐 | 동작 |
| 테스트 | 590 | 592 |
커밋 1개. 전체 변경 이력: v1.2.0...v1.2.1
호환성: 고치기만 했다. CLI 인자·스키마·기록 형식·챕터 경로 변경이 없다.
v1.2.0 — 한 줄로 설치하고, 한 줄로 최신으로
v1.1.0은 입구를 ./guide 하나로 모았다. 그런데 그 입구에 닿으려면 저장소를 먼저 손에 넣어야 했고, 그 방법이 문서 어디에도 없었다.
$ rg "git clone|다운로드|releases/" README.md docs/*.md
(0건)
README.md는 ## 한 번에 시작하기에서 곧장 ./guide로 넘어간다 — 이미 클론했다는 전제가 암묵적으로 깔려 있었다. 이번 릴리스는 그 앞의 한 칸을 채운다.
curl -fsSL https://raw.githubusercontent.com/Ahngbeom/dba-guide/main/install.sh | bash새로 생긴 것 — install.sh
한 줄이면 저장소가 ~/.local/share/dba-guide에 놓이고 guide·exam·shoot 세 명령이 ~/.local/bin에 걸린다. 같은 명령을 다시 실행하면 업데이트다(멱등). 제거는 … | bash -s -- --uninstall.
- 설치 지점은 언제나 최신 정식 릴리스 태그다 —
mainHEAD가 아니다. 이 저장소에는 CI가 없어 테스트 실행도,./shoot doctor도, 벤더 브랜치 재생성도 전부 릴리스 전 사람 손 점검이고, 그 점검을 통과한 지점에만 태그가 붙는다.mainHEAD는 아무도 검증하지 않은 지점이라 학습자의 기본값이 될 수 없다. 프리릴리스(-rc)는 건너뛴다. - 설치를 강제하지 않는다.
git clone후./guide도 그대로 동작하며, README는 두 길을 나란히 적는다. 런처가 자기 경로를 스스로 풀기 때문에 클론만으로 전부 돌아간다. - 필요한 것은
git과python3(3.9 이상)뿐이다. Docker가 없으면 경고만 하고 진행한다 — 챕터 읽기와 학습 점검은 Docker 없이 완전히 동작하고, 장애 대응만 랩이 필요하다.
자기가 만들지 않은 것은 건드리지 않는다
남의 머신에서 curl | bash로 도는 스크립트다. 다음은 전부 한국어 설명과 함께 중단한다 — 덮거나 지우지 않는다.
| 상황 | 왜 위험한가 |
|---|---|
| 런처 이름에 남의 파일·심볼릭 링크가 있다 | 세 이름을 모두 확인한 뒤 한 번에 보고한다. 하나씩 죽으면 같은 실패를 세 번 겪는다 |
| 설치 경로에 git 저장소가 아닌 것이 있다 | 지우지도, 그 위에 클론하지도 않는다 |
| 설치 경로가 남의 저장소 안에 들어앉아 있다 | 홈을 git으로 관리하는 사람의 경우다. 그대로 두면 fetch·checkout이 바깥 저장소를 대상으로 돈다 |
| 설치 경로에 다른 저장소가 있다 | scripts/guide.py 유무로 신원을 확인한다. 벤더 브랜치를 그 자리에 클론해 둔 사람이 실제로 겪을 수 있다 |
| 설치본의 추적 파일이 수정돼 있다 | git status --short를 보여주고 멈춘다 |
저장소 안에서 실행하면 그 자리를 링크할 뿐 HEAD를 옮기지 않는다. 기여자가 자기 작업 클론에서 돌렸는데 브랜치가 릴리스 태그로 detached 되면 작업이 끊긴다.
제거는 학습 기록을 지우지 않는다
--uninstall은 링크만 지운다. 저장소 디렉터리는 남는다.
.exam-results/와 .shooting-progress/notes/는 .gitignore 대상이라 어디에도 백업이 없는 유일본이다. 특히 정리 노트는 엔진이 관찰한 사실만 채우고 근본 원인·5 Whys·재발 방지를 빈칸으로 남기는 — 그걸 채우는 게 학습인 — 산출물이다. 지우려면 --purge를 명시해야 하고, 그때도 시험 결과 몇 건·정리 노트 몇 건이 사라지는지 먼저 보여준 뒤 확인을 받는다. tty가 없으면 확인할 수 없으므로 지우지 않고 멈춘다.
학습서
본문은 이번 릴리스에서 바뀌지 않았다(31챕터 그대로). README.md에 ## 설치 절이 들어갔고, docs/release-policy.md에 인스톨러와 태그의 순서 의존성이 기록됐다.
./guide · ./exam · ./shoot
바뀌지 않았다. 제거되거나 의미가 바뀐 인자는 없고, 문제은행(23개 216문항)과 스테이지(14개)도 그대로다. 설치본에서는 ./ 접두사만 떼고 guide·exam·shoot으로 부르면 된다 — 그래서 기존 문서의 ./exam·./shoot 표기 67곳이 그대로 유효하다.
벤더 브랜치
postgresql/mysql/oracle 세 브랜치를 이 릴리스 시점의 main에서 재생성해 push했다. install.sh는 이 브랜치들을 설치 대상으로 삼지 않는다 — 편의가 아니라 구조 때문이다. v1.1.0의 읽기 모드가 filter_dbms.filter_lines(단일 벤더 브랜치를 자르는 것과 같은 함수)로 벤더별 본문을 보여주므로 브랜치를 클론할 이유가 사라졌고, 이 브랜치들은 매번 --force-with-lease로 재생성되므로 설치 대상으로 삼으면 업데이트가 fast-forward가 아니라 reset --hard가 되어 사용자 수정을 말없이 지우는 경로가 생긴다.
검증에 대해 — 통과하는 테스트 수는 커버리지가 아니다
이번 개발에서 실측된 것 하나를 남긴다. 리뷰 도중 install.sh의 링크 소유권 판별 검사 한 줄을 지웠는데 578개 테스트가 전부 그대로 통과했다. 그 불변식은 테스트가 아니라 코드 리뷰로만 지켜지고 있었다는 뜻이다.
지금은 그 두 검사가 각각 전용 테스트로 고정돼 있고, 해당 줄을 지우면 지정된 테스트가 실패하는 것까지 확인했다. --purge의 확인 프롬프트는 pty.openpty()로 진짜 tty를 만들어 y(삭제)·n(취소)·Ctrl-D(취소) 세 경로를 모두 실행한다 — 이 저장소에서 가장 파괴적인 한 줄이 그전까지 한 번도 실행되지 않고 있었다.
알려진 한계
발견되지 않은 버그가 아니라 알고 감수하는 경계다.
인스톨러 (신규)
curl | bash의 신뢰 모델. 스크립트를 읽지 않고 실행하게 한다. README에 내려받아 확인한 뒤 실행하는 2단계 방법을 함께 적었고, 스크립트는 짧고 읽을 수 있게 유지했다.--purge는 파이프로 실행할 수 없다. 확인에 tty가 필요하다. 내려받은 사본에서bash install.sh --uninstall --purge로 실행해야 한다.- PATH 등록은 자동으로 하지 않는다.
~/.local/bin이$PATH에 없으면 더할 한 줄을 출력만 한다. 남의 셸 설정을 말없이 고치지 않는다. - PATH 점검은
~/.local/bin이 끝에 슬래시가 붙거나~로 미전개된 형태면 못 알아본다. - 태그가 붙은 트리에 세 런처가 다 있는지는 검사하지 않는다(
v1.1.0이후로는 항상 참). - 이름 충돌:
guide/exam/shoot은 일반적인 이름이다. 사용자 스코프 설치와 설치 전 충돌 감지로 막지만, 나중에 같은 이름이 설치되면 우리 링크가 가려질 수 있다. - Windows 네이티브 미지원. WSL 안에서 쓴다. Homebrew·PyPI·컨테이너 이미지도 지원하지 않는다.
판정 회피 (MySQL)
- 플레이어(
dba)에게는 튜닝 스테이지를 위해SYSTEM_VARIABLES_ADMIN이 있고, 그 권한이면 엔진과 같은 방법(SET SESSION sql_log_off = 1)으로 자기 명령을 로그에서 뺄 수 있다. - 명령 로그를 읽고 비우는 사이에 수 ms의 유실 창이 있다. 로그 테이블에는
DELETE가 막혀 있어(ERROR 1556) "읽은 지점까지만 지우기"가 불가능하므로 고칠 수 없다. 판정 대부분이 누적 목록이 아니라 최종 상태에 기대므로 실해는 거의 없지만, 타임라인에서 한 줄이 빠질 수 있다.
PostgreSQL 판정의 두 구멍 (MySQL에는 없다)
- 파싱 오류는 감시에 남지 않는다. 실측 결과 정확히 파싱 오류만 빠진다 — 없는 테이블 조회나 권한 오류는 잡히고,
KILL 999999(PG 문법이 아님)는 놓친다. - pid를 적지 않는 쓸기가 보이지 않는다.
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE ...형태는 문장에 숫자가 없어kill_precision이 잡지 못한다. 감점 대상으로 삼으려면 스테이지에서forbidden_command로 직접 막아야 한다.
커버리지 공백
- 스테이지 14개 중 MySQL 13 / PostgreSQL 1. Oracle 스테이지는 없다.
- PostgreSQL 스테이지를 하려면
psql클라이언트가 필요하다(./shoot doctor가 알려 준다). - 단답 채점의 허용 답안이 하나뿐인 문항이 남아 있어, 뜻이 같은 다른 표기를 오답 처리할 수 있다.
- 문항 벤더 표기가 전부
neutral인 은행이 2개 있다(01-beginner/01,03-advanced/06) —--dbms로 걸러도 이 은행은 항상 전부 나온다.
운영
- CI가 없다. 테스트 실행도, 벤더 브랜치 재생성도, 릴리스 발행도 전부 사람이 손으로 한다. 실행 코드가 외부 의존성 없는 Python 표준 라이브러리와 Bash뿐이라 유지할 파이프라인 자체가 얇다는 판단이다. 인스톨러가 늘어나면서 릴리스 전 점검 항목도 하나 늘었다.
- 인스톨러는 태그를 기다린다.
install.sh자체는raw.githubusercontent.com/.../main/install.sh에서 받으므로 스크립트 버그 수정은 즉시 반영되지만, 설치되는 내용은 최신 태그다. 그래서 이 릴리스를 발행한 직후 실제로 한 번 돌려 확인해야 한다.
요약
| v1.1.0 | v1.2.0 | |
|---|---|---|
| 챕터 | 31 | 31 |
| 문제은행 / 문항 | 23 / 216 | 23 / 216 |
| 스테이지 | 14 | 14 |
| 실행 진입점 | ./guide ./exam ./shoot |
동일 + install.sh |
| 설치 수단 | 없음(수동 clone) | 한 줄 설치·업데이트·제거 |
| 테스트 | 557 | 590 |
커밋 18개. 전체 변경 이력: https://github.com/Ahngbeom/dba-guide/commits/v1.2.0
호환성: 더해지기만 했다. 제거되거나 의미가 바뀐 CLI 인자, 스키마 변경, 진행·결과 기록 형식 변경, 챕터 경로 변경 모두 없다.
v1.1.0 — ./guide 하나로 읽고, 풀고, 겪는다
v1.0.0은 학습서·./exam·./shoot 셋을 한 저장소에 모았지만, 셋을 쓰려면 각각의 실행 파일을 알아야 했다. 이번 릴리스는 입구를 하나로 만들고, 읽는 축을 추가한다.
$ ./guide
무엇을 할까요
1) 학습 점검 (퀴즈/시험) 216문항
2) 장애 대응 (실전 훈련) 14스테이지
3) 챕터 읽기 31챕터
읽기 → 확인 → 겪기가 한 자리에서 닫힌다. 챕터를 다 읽으면 그 챕터의 시험을 권하고, 스테이지를 클리어하면 관련 챕터의 시험을 권한다.
새로 생긴 것 — ./guide
- 최상위 메뉴 하나. 모드를 고르면 curses에서 빠져나와 해당 러너를 부르고, 끝나면 메뉴로 돌아온다. 한 모드가 어떻게 끝나든(
SystemExit·Ctrl-C) 런처는 살아남는다. - 챕터 읽기 모드 (신규). DBMS → 티어 → 챕터를 고르면 그 벤더의 본문만 남겨
$PAGER로 연다. 각 화면에서Esc로 한 단계씩 물러난다.- 벤더 필터는 단일 벤더 브랜치를 자르는 것과 같은 함수(
filter_dbms.filter_lines)를 쓴다. 브랜치로 보는 것과 읽기 모드로 보는 것이 갈라질 수 없다. - 본문 뷰어를 새로 만들지 않았다.
less가 스크롤·검색을 이미 다 한다.
- 벤더 필터는 단일 벤더 브랜치를 자르는 것과 같은 함수(
./exam·./shoot은 그대로 남는다 — 인자를 줄 때는 여전히 이쪽이다. 제거되거나 의미가 바뀐 인자는 없다.
학습서
챕터 본문은 이번 릴리스에서 바뀌지 않았다(31챕터 그대로). README.md에 ./guide 시작 안내가 들어갔다.
./exam
- 길이로 정답이 드러나던 64문항의 오답을 다시 썼다. 정답 선택지가 가장 긴 문항은 본문을 몰라도 길이만 보고 맞힐 수 있었다. 은행 23개 216문항을 전수 점검해 걸린 것을 고쳤고, 오답의 정보 밀도를 정답에 맞췄다.
- 길이 누설을 기계적으로 막는다. 새 문항이 같은 함정을 다시 만들면 테스트가 실패한다. 측정 기준은 문자 수가 아니라 표시 폭(
tui.cwidth)이다 — 한글은 2칸,max_connections는 1칸이라 문자 수로 재면 통과하는 문항이 화면에서는 여전히 누설된다. - 라인 모드(파이프·비-tty) 선택 화면이 세 러너에서 하나로 통일됐다.
./shoot
- 진단 문항의 선택지 순서가 이제 실제로 섞인다. 게임 시드로 섞이며, 순서 자체가 의미를 갖는 문항은
shuffle: false로 고정할 수 있다. - 스테이지를 클리어하면 관련 챕터의 시험을 권한다. 경로를 손으로 찾게 하면 거기서 학습이 끊긴다.
- PostgreSQL 스테이지 판정 정정. 여러 피해자 세션을 한 행에 몰면 InnoDB와 달리 PostgreSQL은 튜플 락으로 대기를 직렬화해 범인이 아니라 중간 대기자를 blocker로 보고한다(실측: 피해자 4명일 때 범인은 4개 중 1개에서만 잡혔다). 피해자를 서로 다른 행으로 갈랐다.
- 랩 기동 직후의 준비 대기, 벤더별 클라이언트 표기, 접속 힌트 자동 생성, 오탐을 내던 정규식 하나를 고쳤다.
- 사후 분석(postmortem) 노트에서 해당 챕터로 연결된다.
벤더 브랜치
postgresql/mysql/oracle 세 브랜치를 이 릴리스 시점의 main에서 재생성해 push했다. 각 브랜치에서 마커 잔여 0, 비교표·부록·문제은행 무변형, 테스트 스위트 초록을 확인했다.
이번에 발견해 고친 것: 저장소 본문을 읽는 테스트가 자기도 모르게 "main 위에서 돈다"고 가정하고 있었다. 벤더 브랜치의 챕터는 이미 필터된 뷰라 마커가 0개인데, 스크립트는 *.md만 필터하므로 테스트는 그대로 실려 간다 — 그래서 세 브랜치의 스위트가 조용히 빨간 상태였다. main에서는 어떤 리뷰로도 재현되지 않고, 브랜치를 실제로 잘라 돌려 봐야만 드러난다. 이 규칙을 docs/dbms-branch-strategy.md에 적어 뒀다.
알려진 한계
발견되지 않은 버그가 아니라 알고 감수하는 경계다. 스테이지를 만들거나 판정에 기댈 때 여기를 넘지 마라.
판정 회피 (MySQL)
- 플레이어(
dba)에게는 튜닝 스테이지를 위해SYSTEM_VARIABLES_ADMIN이 있고, 그 권한이면 엔진과 같은 방법(SET SESSION sql_log_off = 1)으로 자기 명령을 로그에서 뺄 수 있다. - 명령 로그를 읽고 비우는 사이에 수 ms의 유실 창이 있다. 로그 테이블에는
DELETE가 막혀 있어(ERROR 1556) "읽은 지점까지만 지우기"가 불가능하므로 고칠 수 없다. 판정 대부분이 누적 목록이 아니라 최종 상태에 기대므로 실해는 거의 없지만, 타임라인에서 한 줄이 빠질 수 있다.
PostgreSQL 판정의 두 구멍 (MySQL에는 없다)
- 파싱 오류는 감시에 남지 않는다. 실측 결과 정확히 파싱 오류만 빠진다 — 없는 테이블 조회나 권한 오류는 잡히고,
KILL 999999(PG 문법이 아님)는 놓친다. - pid를 적지 않는 쓸기가 보이지 않는다.
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE ...형태는 문장에 숫자가 없어kill_precision이 잡지 못한다. 감점 대상으로 삼으려면 스테이지에서forbidden_command로 직접 막아야 한다.
커버리지 공백
- 스테이지 14개 중 MySQL 13 / PostgreSQL 1. Oracle 스테이지는 없다.
- PostgreSQL 스테이지를 하려면
psql클라이언트가 필요하다(./shoot doctor가 알려 준다). - 단답 채점의 허용 답안이 하나뿐인 문항이 남아 있어, 뜻이 같은 다른 표기를 오답 처리할 수 있다.
- 문항 벤더 표기가 전부
neutral인 은행이 2개 있다(01-beginner/01,03-advanced/06) —--dbms로 걸러도 이 은행은 항상 전부 나온다.
운영
- CI가 없다. 테스트 실행도, 벤더 브랜치 재생성도, 릴리스 발행도 전부 사람이 손으로 한다. 실행 코드가 외부 의존성 없는 Python 표준 라이브러리와 Bash뿐이라 유지할 파이프라인 자체가 얇다는 판단이다. 대신 릴리스 전 점검을 자동으로 막아 주는 장치도 없다.
요약
| v1.0.0 | v1.1.0 | |
|---|---|---|
| 챕터 | 31 | 31 |
| 문제은행 / 문항 | 23 / 216 | 23 / 216 (64문항 개정) |
| 스테이지 | 14 | 14 |
| 실행 진입점 | ./exam ./shoot |
./guide ./exam ./shoot |
| 테스트 | 426 | 557 |
커밋 51개. 전체 변경 이력: https://github.com/Ahngbeom/dba-guide/commits/v1.1.0
호환성: 더해지기만 했다. 제거되거나 의미가 바뀐 CLI 인자, 스키마 변경, 진행·결과 기록 형식 변경, 챕터 경로 변경 모두 없다.
v1.0.0 — 읽고, 풀고, 겪는 DBA 학습서
2026-07-15 첫 커밋 이후 커밋 54개 · PR 38건이 쌓여 이 저장소는 세 축을 모두 갖췄다.
읽는 가이드(3단계 커리큘럼), 푸는 학습 점검(./exam), 겪는 장애 대응
게임(./shoot). v1.0.0은 그 셋이 처음으로 한자리에 모인 시점을 표시한다.
| 축 | 무엇 | 규모 |
|---|---|---|
| 학습서 | 예비 DBA → 시니어 DBA/아키텍트 3단계 커리큘럼 | 챕터 23개 + 치트시트 3장 + 부록 2종 |
./exam |
챕터별 학습 점검 퀴즈/시험 TUI | 문제은행 23개 (전 챕터) |
./shoot |
로컬 Docker 랩에 실제 장애를 주입하는 대응 게임 | 스테이지 14개 (월드 4개, MySQL·PostgreSQL) |
외부 의존성은 없다. 실행 코드는 Python 3 표준 라이브러리와 Bash뿐이고, pip도
npm도 빌드 단계도 쓰지 않는다.
읽는다 — 3단계 커리큘럼
경력 단계가 디렉터리 이름에 그대로 박혀 있다.
01-beginner(예비/신입 DBA) — RDBMS 기초 · SQL 기초 · 설치/접속 · 계정·권한 · 기본 백업 · 기본 모니터링02-intermediate(실무 독립 수행 DBA) — 트랜잭션/락 · 인덱싱/쿼리튜닝 · 성능 모니터링 · 백업·복구 전략 · 복제 · 스키마 변경 관리 · 클라우드 DB 인프라 · 매니지드 DB 기초03-advanced(시니어 DBA / DB 아키텍트) — 고급 튜닝 · 고가용성/페일오버 · 재해복구 · 확장/샤딩 · 보안/컴플라이언스 · 자동화(IaC) · 매니지드 DB 심화 · Kubernetes DB Operator · 장애 대응/포스트모템
모든 챕터는 핵심 개념 → 명령어/문법 → 실습 예제 → 체크리스트 네 절을 같은 순서로
지킨다. 명령어는 PostgreSQL → MySQL → Oracle 순으로 병기하되, 문법만 다른 곳이 아니라
동작이 실제로 갈리는 곳에서만 차이를 짚는다. 각 티어의 00-overview.md는 선수 지식과
다음 단계로 넘어갈 졸업 기준을 체크리스트로 제시한다.
곁들여 있는 것:
- 티어마다
*-commands-cheatsheet.md— 그 단계의 명령어를 3개 DBMS 열로 비교한 한 장짜리 표 appendix/dbms-comparison-matrix.md— 클라우드 매니지드 서비스(AWS/GCP/Azure)까지 포함한 교차 비교표appendix/glossary.md— 전 티어 용어집03-advanced/labs/oracle-mysql-operator-kind/— kind 위에서 Oracle MySQL Operator를 직접 세워 보는 실습 랩
보고 싶은 DBMS만
main은 벤더 중립을 유지하고, 벤더 전용 문단·코드블록·체크리스트 항목은
<!-- dbms:postgresql --> 류의 마커로 감싼다. scripts/generate-branch.sh <dbms>가
이를 걸러 postgresql /
mysql /
oracle 브랜치를 재생성한다.
v1.0.0 기준 마킹 대상 챕터는 전부 마킹 완료다(치트시트와 부록 비교표는 설계상
필터링하지 않는다). 세 브랜치 모두 main@c98a6e0에서 재생성돼 동기 상태다.
파생 브랜치는 직접 수정하지 않는다. 규약은
docs/dbms-branch-strategy.md.
푼다 — ./exam
챕터를 읽고 나서 실제로 남았는지 확인하는 TUI다.
./exam # DBMS → 티어 → 챕터를 화면에서 고른다
./exam 02-intermediate # 티어 전체를 시험 모드로
./exam --dbms postgresql # 그 벤더 문항만- 전 챕터 23개의 문제은행이
exams/<tier>/<chapter>.json에 들어 있다 - 객관식·단답(
short) 혼합. 벤더 종속 명령은dbms태그가 붙어--dbms필터에 걸린다 - curses TUI가 기본, 터미널이 아니면 라인 모드로 자동 폴백해 파이프에서도 돌아간다
- 결과는 로컬에 저장돼 약한 챕터를 다시 짚을 수 있다
문항 작성 규약(정답 노출 금지 규칙 포함)은 docs/exam-authoring.md.
초안 생성기 scripts/seed_exam.py가 챕터에서 문항 뼈대를 뽑아 준다.
겪는다 — ./shoot
읽어서 아는 것과 새벽 3시에 고치는 것은 다르다. ./shoot은 로컬 Docker 랩에 진짜
장애를 주입하고, 플레이어가 그것을 진단·복구하는 과정을 판정한다.
./shoot doctor # 사전 점검 (docker / 포트 / 클라이언트 / 스테이지 정의)
./shoot up # 랩 기동 (--with-postgresql 로 PG 랩까지)
./shoot # DBMS → 월드 → 스테이지를 골라 플레이
./shoot 1-2-deadlock # 바로 그 스테이지로
./shoot replay # 마지막 판을 같은 시드로 다시
./shoot notes # 지난 포스트모템 노트 읽기
./shoot down # 랩 정리스테이지 14개
| 월드 | 스테이지 |
|---|---|
| 1 · 잠금과 대기 | 1-1 끝나지 않는 리포트(폭주 쿼리) · 1-2 서로를 기다리다(데드락) · 1-3 락 지옥 · 1-4 멈춘 배포(메타데이터 락) |
| 2 · 복제 | 2-1 리포팅 서버 붙이기(구축형) · 2-2 멈춘 리포트(복제 지연) |
| 3 · 자원 고갈 | 3-1 문이 닫혔다(커넥션 고갈) · 3-2 지워지지 않는 과거(롱 트랜잭션 undo) · 3-3 쌓아둔 것의 값(디스크 풀) · 3-4 있지도 않은 중복(AUTO_INCREMENT 상한) |
| 4 · 성능과 실행 계획 | 4-1 느린 화면(인덱스 부재) · 4-2 보이지 않는 느림(슬로우 로그 구성) · 4-3 있는데 쓰지 않는다(암묵적 형변환) |
| PostgreSQL | pg-1-1 아무 일도 하지 않는 범인(idle in transaction) |
랩은 MySQL primary/replica 2노드가 기본이고, PostgreSQL 랩은 compose 프로파일
뒤에 있어 ./shoot up --with-postgresql로만 뜬다. 포트는 루프백에만 바인딩된다.
이 게임을 게임으로 만드는 것들
- 판정은 사용자 분리 위에 선다 —
dba는 플레이어,app은 엔진이 만든 부하 세션,repl은 복제 계정,root@localhost는 엔진 자신이다. 플레이어 명령만 골라 읽으므로 엔진의 소음이 채점에 섞이지 않는다 - 판마다 숫자가 흔들린다 —
vars가 시드에서 파라미터를 변주해 답을 외워도 다시 봐야 한다../shoot replay는 같은 시드로 그 판을 그대로 되살린다 - 금지 명령을 감점한다 — 원인을 짚지 않고 재시작으로 뭉개는 지름길은
constraints가 잡는다 - 화면은 진짜 클라이언트다 — TUI 안에 SQL 콘솔을 만들지 않고
c키로 실제mysql/psql을 띄운다. 판정은 입력 경로가 아니라 로그에 남은 사용자로 하므로 영향받지 않는다 - 끝나면 포스트모템을 쓴다 — 초안에는 관측된 사실만(타임라인, 틀린 진단 문항, 위반 사항) 채워지고 근본 원인·5 Whys·액션 아이템은 비어 있다. 채우는 것이 훈련이다. 공식 해설은 편집기를 닫은 뒤에 붙어, 눈으로 보고 베끼는 일이 생기지 않는다
- 스테이지마다 읽어야 할 챕터가 연결돼 있어 게임에서 막히면 학습서로 돌아간다
스테이지는 전부 선언적 JSON(shooting/stages/*.json)이라 직접 만들 수 있다. 스키마와
작성 함정은 docs/shooting-game.md에 정리돼 있다.
시작하기
git clone https://github.com/Ahngbeom/dba-guide.git
cd dba-guide
# 읽기: README.md 의 로드맵부터
# 풀기:
./exam
# 겪기 (Docker 필요):
./shoot doctor && ./shoot up && ./shoot요구 사항 — Python 3(표준 라이브러리만), ./shoot에는 Docker와 mysql 클라이언트
(PostgreSQL 스테이지는 psql). ./shoot doctor가 빠진 것을 먼저 알려 준다.
알려진 한계
이 릴리스가 알고 감수하는 지점들이다. 감춰진 버그가 아니라 문서화된 경계다.
판정 (MySQL) — ① dba는 이후 튜닝 스테이지를 위해 SYSTEM_VARIABLES_ADMIN을
가지므로, 마음먹으면 엔진과 같은 방법(SET SESSION sql_log_off = 1)으로 자기 명령을
로그에서 숨길 수 있다. ② 명령 로그를 읽고 비우는 사이에 수 ms의 유실 창이 있다. 이
게임은 학습 도구이지 부정 방지 시스템이 아니며, 등급을 속여 얻는 것도 없다.
판정 (PostgreSQL 전용) — ① 실측 결과 파싱 오류는 감시 소스에 도달하지 않는다
(의미·권한 오류는 도달한다). ② SELECT pg_terminate_backend(pid) FROM pg_stat_activity
같은 일괄 종료는 pid를 명시하지 않아 kill_precision이 볼 수 없다 — 이런 스테이지는
forbidden_command로 잡는다.
커버리지 — PostgreSQL 스테이지는 아직 pg-1-1 하나뿐이다(월드 1만). Oracle은
브랜치 필터링까지만 지원하고 ./shoot 스테이지는 없다. 학습서 본문은 세 벤더 모두
동등하게 다룬다.
CI 없음 — 테스트 426개는 python3 -m unittest discover -s tests로 로컬에서 돈다.
v1.0.0 태그 시점 기준 전부 통과. 릴리스 전 점검이 사람 손에 달려 있다는 뜻이라,
그 목록과 절차는 docs/release-policy.md에 적어 두었다.
이 릴리스의 숫자
커밋 54개 · 머지된 PR 38건 · 2026-07-15 → 2026-08-05
챕터 23개 + 치트시트 3장 + 부록 2종
문제은행 23개 (전 챕터)
스테이지 14개 / 월드 4개 / DBMS 2종
테스트 426개 통과
외부 의존성 0개
전체 변경 이력: https://github.com/Ahngbeom/dba-guide/commits/v1.0.0