단원 홈
4단원 · 8차시

세 명이 만든 세 개의 버그
함께 고치는 법

7차시에 우리 팀은 k=7에서 85.4%를 봤습니다. 그런데 셋이 나눠 고친 파일을 오늘 합쳤더니 첫 줄에서 죽습니다. 겨우 살려 놓으면 이번에는 정확도가 100%로 나오고요. 각자 자기 몫은 다 맞다는데 말입니다. 버그 셋은 성격이 셋 다 다릅니다.

성취기준 12인기04-03
짝 프로그래밍검사표 죽는 버그 · 조용한 버그합치기 협업 기록생성형 AI 사용 규칙
🎯 학습 목표
  • 버그의 성격 셋(죽는 것 · 조용히 틀리는 것 · 가끔만 틀리는 것)을 구별하고, 오류가 터진 자리와 고쳐야 할 범인의 자리가 다를 수 있음을 우리 코드로 보일 수 있다.
  • “이 함수에 이 값을 넣으면 이 값이 나와야 한다”는 검사를 여섯 줄 적어 두고, 그 검사표로 팀 코드의 버그가 어디 있는지 짚어낼 수 있다.
  • 짝 프로그래밍과 파일 단위 역할 분담으로 팀 코드를 합치고, 무엇을 · 왜 고쳤는지 협업 기록표에 남길 수 있다.
🤔

여는 장면 — 셋이 각자 맞다는데 합치니 100%

지난 시간 우리 팀은 처음으로 성능을 쟀습니다. 견본 데이터 158행을 훈련 110 · 테스트 48로 나누고, k를 일곱 개 돌려 k=7에서 85.4%를 얻었지요. 늘 ‘가능’이라고만 답하는 기준선이 60.4%였으니 +25.0%p였습니다.

그다음 시간에 팀은 일을 나눴습니다. 갑은 데이터 설정을, 을은 파이프라인을, 병은 학습 부분을 맡았습니다. 각자 자기 컴퓨터에서 파일을 하나씩 내려받아 고쳤고, 셋 다 “내 쪽은 잘 돌아간다”고 했습니다. 그리고 오늘, 합친 파일을 처음으로 함께 실행합니다.

Traceback (most recent call last): ... (가운데 여러 줄) ... return [(v[i] - lo[i]) / (hi[i] - lo[i]) for i in range(len(v))] ZeroDivisionError: division by zero

첫 줄에서 죽었습니다. 그 아래로는 한 줄도 돌지 않았습니다. 어렵게 그것을 고치고 다시 돌리면 이번에는 표가 이렇게 나옵니다.

k135791525
테스트 정확도 100.0%100.0%100.0% 100.0%100.0%100.0%100.0%
맞힌 수 110/110110/110110/110 110/110110/110110/110110/110
버그 하나를 고친 뒤의 화면. 기준선은 50.9%로 나오고 차이는 +49.1%p입니다. 그런데 같은 화면 위쪽에는 ‘테스트 48’이라고 적혀 있습니다.
💭 오늘의 물음

각자 맡은 부분은 다 맞다는데 합치니 정확도가 100%로 나왔다. 이건 좋은 소식인가?
그리고 ‘고쳤다’는 것을 무엇을 보고 말할 수 있을까?

답을 미리 말해 두겠습니다. 좋은 소식이 아닙니다. 이 파일에는 버그가 셋 있고, 100%는 그중 둘째 버그가 내는 소리입니다. 그리고 셋째 버그는 이 표를 아무리 들여다봐도 안 보입니다. 오늘은 셋을 차례로 잡습니다. 잡는 도구는 눈이 아니라 검사표입니다.

행사장 탁자에서 노트북 자판을 두드리는 참가자와, 뒤쪽 탁자에서 각자 노트북 앞에 앉은 사람들
해커톤 행사장의 한 장면입니다. 저마다 자기 노트북 앞에서 따로 일하지요. 갑·을·병도 이렇게 각자의 컴퓨터에서 같은 파일을 고쳤고, 셋 다 “내 쪽은 돌아간다”고 했습니다. 각자 확인한 것은 자기 몫뿐이고, 합친 파일은 아직 아무도 돌려 보지 않았습니다. 출처: Gaelle Berton, Wikimedia Commons (CC BY-SA 3.0)
1

버그가 셋인 것이 아니라, 성격이 셋 다 다르다

“버그가 셋 있다”는 말은 오늘 배울 것의 절반도 담지 못합니다. 중요한 것은 개수가 아니라 성격입니다. 셋은 서로 다른 방식으로 우리를 속입니다.

ㄱ

죽는다

실행하면 그 자리에서 멈추고 빨간 글씨가 뜹니다. ZeroDivisionError가 그것입니다.

가장 착한 버그입니다. 자기가 있다고 소리쳐 주니까요. 다만 그 아래는 한 줄도 안 돕니다 — 검사표조차 못 돕니다. 그래서 먼저 고칩니다.

ㄴ

조용히 틀린다

오류가 없습니다. 표도 예쁘게 나옵니다. 그런데 값이 틀렸습니다. 여기서는 정확도가 100.0%로 나옵니다.

가장 위험한 버그입니다. 결과가 좋아 보여서 아무도 의심하지 않습니다. 눈으로는 못 잡고 검사로만 잡힙니다.

ㄷ

가끔만 틀린다

어떤 설정에서는 정답과 완전히 같은 값을 냅니다. 여기서는 k=1일 때 정직한 코드와 글자까지 같습니다.

가장 찾기 어려운 버그입니다. “내 컴퓨터에서는 되는데”라는 말이 대개 이 성격입니다. 여러 설정으로 돌려야 비로소 드러납니다.

세 버그가 이 파일 어디에 있는지도 성격만큼 중요합니다. 셋 중 둘은 함수 안이 아니라 함수를 부르는 자리에 있습니다.

버그어디에 있나무엇이 나오나
ㄱ 【0】의 FEATURES에 값이 모두 같은 열 기기번호(늘 3)가 들어 있다 ZeroDivisionError: division by zero
ㄴ 정규화 두 줄을 복사해 놓고 아랫줄만 안 고쳤다 (test_rows여야 하는데 train_rows) 오류 없음. 모든 k에서 100.0%, 화면은 ‘테스트 48’인데 채점은 110개
ㄷ ‘고치지 않는다’ 상자 안의 nearest가 ranked[:k] 대신 ranked[:1] 오류 없음. k를 바꿔도 전부 70.8% — 그 값은 정직한 k=1과 같다
ㄱ은 설정 한 줄, ㄴ은 파이프라인의 한 낱말, ㄷ만 함수 안에 있습니다. 버그는 대개 함수 안이 아니라 함수를 부르는 자리에 있습니다.

오류가 터진 자리 ≠ 고쳐야 할 자리

ㄱ의 오류 메시지는 minmax_apply를 가리킵니다. 그런데 minmax_apply는 2단원 5차시에서 가져온 함수입니다. 우리가 고칠 곳이 아니에요. 그 함수는 시킨 대로 (v − lo) / (hi − lo)를 계산했을 뿐인데, 마침 hi − lo가 0이었던 것입니다. ZeroDivisionError는 ‘여기가 범인’이 아니라 ‘여기까지 잘못된 값이 흘러 들어왔다’는 신호입니다.

【0】 FEATURES '기기번호' 가 섞여 있다 ← 진짜 범인 featurize() 열을 그대로 담는다 minmax_fit() lo=3, hi=3 을 낸다 minmax_apply() (v − 3) / (3 − 3) 💥 여기서 터진다 오류를 읽는 방향 — 터진 곳에서 거슬러 올라가 ‘누가 무엇을 넘겼는지’를 본다 트레이스백의 마지막 줄은 ‘무엇이 났나’, 그 위 줄들은 ‘어디를 거쳐 왔나’를 말한다
터진 자리는 넷째 상자인데 고칠 자리는 첫째 상자입니다. 오류의 줄 번호를 ‘범인의 주소’로 읽으면 엉뚱한 함수를 뜯어고치게 됩니다.

그래서 오류 메시지를 읽는 순서는 이렇습니다. ① 마지막 줄이 무엇이 났는지를 말합니다 (ZeroDivisionError: division by zero). ② 그 위 줄들이 어느 함수를 거쳐 왔는지를 아래에서 위로 말합니다. ③ 그중 ‘우리가 쓴 줄’을 찾습니다. 2단원에서 가져온 함수 안에서 터졌다면, 우리가 그 함수에 무엇을 넘겼는지를 보는 것이 순서입니다.

⚠ 단서 하나를 더

오류가 났을 때 어디까지 찍혔는지도 단서입니다. 이 파일은 ‘정제 160행 → 158행’ 두 줄만 찍고 멈춥니다. 그 아래의 분할 표시도, k 표도, 검사표도 하나도 안 나왔습니다. 죽는 버그는 뒤에 있는 모든 안전장치를 무력화합니다. 그래서 셋 중 ㄱ을 가장 먼저 고칩니다.

2

검사란 — “이 값을 넣으면 이 값이 나와야 한다”를 미리 적어 두는 것

버그 ㄴ과 ㄷ은 오류를 내지 않습니다. 화면만 보고는 알 수 없습니다. 그러면 무엇으로 잡을까요? 검사로 잡습니다. 검사는 거창한 것이 아닙니다. 한 줄이면 됩니다.

ℹ️ 검사 한 줄의 꼴

minmax_apply([5], [0], [10])은 [0.5]여야 한다.
0과 10의 한가운데가 5이니 0~1로 옮기면 0.5입니다. — 손으로 답을 낼 수 있을 만큼 작게 잡는 것이 요령입니다.

오늘 쓸 파일 맨 아래에는 이런 검사가 여섯 줄 들어 있습니다. 다만 기대값 칸이 비어 있습니다. 오늘 여러분이 첫 5분에 할 일은 그 여섯 칸을 채우는 것입니다. “무엇을 고칠까”가 아니라 “무엇을 기대해야 하는가”를 먼저 정하는 것이지요.

검사무엇을 재나기대값이것이 걸리면
1dist([0,0,0,0,0], [0,0,3,4,0]) 5.0거리가 뒤쪽 특성도 보고 있는가
2minmax_apply([5], [0], [10]) [0.5]정규화가 한가운데를 0.5로 옮기는가
3len(nearest(TINY, [0,0], 3)) 3이웃을 k개 돌려주는가 → 버그 ㄷ
4knn(TINY, [0,0], 3) "나"k=3 다수결을 하는가 → 버그 ㄷ
5테스트 점 가운데 훈련에도 있는 것의 수 0시험 문제를 미리 보여 줬는가 → 버그 ㄴ
6FEATURES 중 (최댓값−최솟값)이 0인 열의 수 0상수 열이 섞였는가 → 버그 ㄱ
여섯 검사와 그것이 가리키는 버그. 검사 1·2는 오늘의 세 버그를 못 잡습니다 — 그래도 넣어 둡니다. 검사는 지금 아는 버그가 아니라 앞으로 날 사고를 막는 것이니까요.

검사값을 잘못 고르면 ‘죽은 검사’가 된다

검사 1을 보세요. 왜 하필 [0,0,3,4,0]처럼 뒤쪽 자리에 3과 4를 두었을까요? 앞의 두 자리에 두면 안 될까요? 실제로 재 보면 이렇습니다.

넣는 값바른 dist2차원만 보는 dist검사가
[0,0,0,0,0] vs [3,4,0,0,0] 5.05.0통과시킨다 ✘
[0,0,0,0,0] vs [0,0,3,4,0] 5.00.0잡는다 ✔
같은 5.0을 기대하는 검사인데 하나는 죽은 검사이고 하나는 살아 있는 검사입니다. 검사는 있다고 되는 것이 아니라 ‘틀렸을 때 값이 달라지는가’로 살아납니다.

검사 4의 TINY도 마찬가지로 골라 두었습니다. 점 셋 ([0,0],"가") ([0,1],"나") ([0,2],"나")는 [0,0]에서 가까운 순서가 ‘가’·‘나’·‘나’입니다. 그래서 k=1이면 ‘가’, k=3이면 ‘나’로 답이 갈립니다. 만약 셋 다 같은 이름표였다면 k를 무시하는 버그가 이 검사를 그냥 통과했겠지요.

검사는 뒤가 아니라 앞에 둔다

지금 이 파일의 검사표는 파이프라인 뒤에 있습니다. 그런데 버그 ㄱ이 앞에서 죽여 버리니 검사표는 한 줄도 못 돕니다. 검사 6을 앞으로 옮기면 같은 사고가 이렇게 바뀝니다.

특성 ['기기번호'] 은 값이 모두 같다 — FEATURES 에서 빼라

같은 사고인데 읽을 수 있는 문장이 되었습니다. 무엇을 어디서 고쳐야 하는지가 메시지 안에 들어 있습니다. ZeroDivisionError는 그 정보를 하나도 주지 않았지요. 이것이 검사를 앞에 두는 까닭입니다. 뒤에 둔 검사는 앞에서 죽으면 아무것도 지켜 주지 못합니다.

3

짝 프로그래밍과 막혔을 때의 네 단계

버그 ㄴ은 복사한 두 줄 중 아랫줄에 있고, 버그 ㄷ은 ‘고치지 않는다’라고 적힌 상자 안에 있습니다. 둘 다 혼자 읽으면 눈이 미끄러지는 자리입니다. 사람은 자기가 방금 쓴 줄을 읽을 때 쓴 대로가 아니라 쓰려던 대로 읽거든요. 그래서 오늘은 둘이 한 화면을 봅니다.

운전대

손을 잡는 사람

키보드를 잡고 실제로 고칩니다. 자기가 무엇을 하는지 소리 내어 말하면서 칩니다 — “지금 nearest의 마지막 줄을 볼게.”

길잡이

읽고 의심하는 사람

키보드를 잡지 않습니다. 화면을 읽고 묻습니다 — “그 줄, 위아래 두 줄이 똑같은데 아래만 train인 건 일부러야?”

규칙

번갈아 잡는다

버그 하나를 잡을 때마다 자리를 바꿉니다. 오늘은 세 번 바꾸게 됩니다. 길잡이가 계속 길잡이면 그 사람은 손이 안 자랍니다.

모니터 한 대 앞에 나란히 앉아, 한 사람은 마우스를 잡고 옆 사람은 팔을 뻗어 화면 쪽을 가리키는 두 프로그래머
짝 프로그래밍의 두 자리가 그대로 보입니다. 오른쪽 사람이 마우스를 잡은 운전대이고, 안경 쓴 왼쪽 사람은 팔을 뻗어 화면 쪽을 가리키는 길잡이입니다. 길잡이의 손가락이 가는 자리 — 복사한 두 줄 중 아랫줄, ‘고치지 않는다’ 상자 안 — 가 오늘 버그 ㄴ·ㄷ이 숨은 곳입니다. 출처: Lisamarie Babik, Wikimedia Commons (CC BY 2.0)

막혔을 때 — 사람을 부르기 전에 네 단계

“안 돼요”라고만 말하면 도우러 온 사람도 처음부터 다시 시작해야 합니다. 아래 네 단계를 밟은 뒤에 부르면, 부르는 동안 스스로 답을 찾는 일이 자주 생깁니다.

  1. 증상을 한 문장으로 적는다. “k를 3으로 바꿔도 정확도가 70.8%로 똑같다.” — ‘안 된다’가 아니라 무엇이 어떻게 되는지를 적습니다.
  2. 어디까지 맞는지 범위를 좁힌다. 분할까지는 맞나? 정규화까지는? 검사표 여섯 줄이 이 일을 대신해 줍니다 — ✘가 뜬 줄이 곧 범위입니다.
  3. 오류 메시지를 끝까지 읽는다. 마지막 줄(무엇이 났나)과 그 위 줄들(어디를 거쳤나)을 둘 다 읽습니다. 대부분은 안 읽어서 못 고칩니다.
  4. 가장 작은 재현 예를 만든다. 158행이 아니라 세 점짜리 TINY로 줄여 봅니다. 작아지면 손으로 답을 낼 수 있고, 손으로 답을 낼 수 있으면 그것이 곧 검사가 됩니다.

그다음에 사람을 부릅니다. 이 순서를 지키면 부를 때 이렇게 말하게 됩니다 — “k를 바꿔도 값이 같고, 검사 3이 3 대신 1을 냈어. nearest가 의심스러운데 같이 봐 줄래?” 이 한 문장이 ‘안 돼요’보다 열 배 빠릅니다.

4

역할은 파일 단위로 나눈다 — 합치기가 두 사람 몫을 지운다

갑·을·병은 성실했습니다. 각자 한 시간씩 썼고 각자 버그를 하나씩 잡았습니다. 그런데 셋이 같은 파일을 각자 내려받아 고쳤습니다. 합칠 때 무슨 일이 나는지 재 봤습니다.

합치는 방법살아남은 수정그 판을 돌리면
통째로 덮어쓰기 · 갑이 마지막에 저장ㄱ k=7 정확도 100.0% — 여전히 틀렸다
통째로 덮어쓰기 · 을이 마지막에 저장ㄴ ZeroDivisionError
통째로 덮어쓰기 · 병이 마지막에 저장ㄷ ZeroDivisionError
줄 단위로 합치기 (셋 다 살린다)ㄱ·ㄴ·ㄷ k=7 정확도 85.4% ✔
네 판 중 셋이 못 씁니다. 게다가 을·병이 이긴 판은 갑이 이미 고쳤던 그 오류로 다시 죽습니다 — “고쳤는데 또 그 오류가 난다”는 말은 대개 합치기 사고입니다.

그래서 팀에서 일을 나눌 때의 첫 규칙은 이것입니다. 역할을 파일 단위로 나눈다. ‘데이터 담당’·‘모델 담당’처럼 주제로 나누면 결국 같은 파일을 셋이 건드리게 됩니다. 어쩔 수 없이 한 파일을 나눠야 한다면 셋을 지키세요.

  1. 고칠 곳을 먼저 말로 나눈다. “나는 【0】만, 너는 파이프라인만, 너는 함수 상자만.”
  2. 짧게 자주 합친다. 한 시간 뒤가 아니라 버그 하나 잡을 때마다 합칩니다.
  3. 합친 뒤 검사표를 다시 돌린다. 합치는 과정에서 남의 수정이 지워졌는지는 검사표만 압니다. 눈으로 훑어서는 모릅니다.

합치기 전에 서로의 코드를 이 순서로 본다

코드 리뷰라는 이름이 붙으면 거창해 보이지만, 실제로는 여덟 줄짜리 점검표입니다. 오늘 우리가 만난 사고가 그대로 들어 있습니다.

#무엇을 보나오늘의 어느 버그
①숫자가 서로 안 맞는 곳 — 화면이 말하는 개수와 실제로 센 개수가 같은가ㄴ
②너무 좋은 결과 — 100%, 기준선과의 차이가 40%p 넘음. 먼저 의심하고 나서 기뻐한다ㄴ
③쓰이지 않는 인자 — 함수가 받은 k를 몸통에서 정말 쓰는가ㄷ
④복사한 두 줄 — 붙여 넣고 한 줄만 고쳤는가 (훈련/테스트, 앞/뒤, 최소/최대)ㄴ
⑤‘고치지 않는다’ 상자 — 베껴 온 부품이 원본과 글자까지 같은가ㄷ
⑥새로 붙인 열·설정 — FEATURES에 상수 열·글자 열이 섞이지 않았는가ㄱ
⑦순서 — 나누기 → 훈련으로 자 재기 → 정규화. 자를 먼저 재면 누수다(6차시)—
⑧검사표 — 고친 뒤 여섯 검사를 다시 돌렸는가. 합친 뒤에도 한 번 더—
⚠️ 점검표는 ‘눈으로 보기’입니다. 눈은 아래 두 가지를 놓칩니다. 검사표는 눈이 놓친 것을 잡고, 점검표는 검사표가 못 쓴 것을 잡습니다. 둘 다 있어야 합니다.
⚠ 눈으로는 못 잡는 두 가지 — 실제로 재 봤다

(ㄹ) dist를 2차원 전용판으로 되돌려 놓으면 오류가 안 납니다. k=7 정확도가 85.4% → 70.8%로 14.6%p 떨어지고 k=3은 12.5%p 떨어지는데, k=1에서는 오히려 2.1%p 올라갑니다. 그래서 k=1만 돌려 본 팀은 “좋아졌네”라고 말하게 됩니다.

(ㅁ) 자를 훈련이 아니라 전체로 재면(6차시의 그 누수) 더 무섭습니다. 일곱 개 k 가운데 값이 달라지는 것이 딱 한 개(k=1, +2.1%p)뿐이고 나머지 여섯은 0.0%p입니다. 누수는 정확도로 안 잡힙니다. 코드를 읽어서만 잡힙니다 — 나누고 → 훈련으로 재고, 그 순서입니다.

도구는 사다리다 — 그리고 생성형 AI에게 받은 코드

팀이 쓸 도구는 하나가 아닙니다. 티처블 머신 · 엔트리 · 파이썬은 어느 것이 더 훌륭한 도구가 아니라 사다리의 다른 칸이고, 팀의 실력과 남은 시간에 맞는 칸을 고르면 됩니다. 우리는 파이썬 칸에 있으니 그 칸의 규칙을 지킵니다.

막혔을 때 생성형 AI에게 물어도 됩니다. 다만 3단원에서 세운 비판적 자세가 그대로 적용됩니다. 받은 코드를 그냥 붙여 넣으면 안 되는 까닭은 도덕이 아니라 공학입니다 — 그 코드는 우리 데이터를 본 적이 없습니다. 우리 FEATURES에 상수 열이 있는지, 우리 nearest가 이미 망가져 있는지 모릅니다. 그래서 오늘의 검사표가 그대로 답이 됩니다.

  1. 돌려 본다. 읽어서 그럴듯하면 통과가 아닙니다. 오늘 버그 ㄴ·ㄷ도 읽기에는 그럴듯했습니다.
  2. 검사에 걸어 본다. 여섯 검사를 다시 돌립니다. 통과 수가 줄었다면 되돌립니다.
  3. 어디를 어떻게 받았는지 기록에 남긴다. 무엇을 물었고, 무엇을 받았고, 어디를 우리가 고쳤는지를 협업 기록표 한 줄로. 11차시 공유회에서 이 줄이 그대로 근거가 됩니다.
💻

손으로 ① — 버그 셋을 짝과 함께 잡는다

아래 파일이 갑·을·병이 합쳐 놓은 그 파일입니다. 588줄이지만 겁먹지 마세요. 여러분이 고칠 곳은 딱 세 곳이고, 나머지는 6차시·7차시에서 이미 본 골격 그대로입니다. 먼저 이 파일의 지도를 봅시다.

26~46
【0】 우리 팀 설정 — NAMES·FEATURES·SEED·K_LIST. 고칠 곳 하나가 여기 있습니다(33줄).
48~66
wpad·line — 표를 가지런히 찍는 도구. 2단원 4·5차시에서 그대로.
69~230
DATA 162줄 — 견본 페트병 표 160행. 읽지 않아도 됩니다. 자기 팀 데이터가 있으면 이 자리를 갈아 끼웁니다.
233~372
load·column·numeric_cols·mean·median·stdev·clean — 6차시의 읽기와 다듬기.
374~441
dist·minmax_fit·minmax_apply(2단원 5차시) · featurize·split·check_sizes(6차시).
443~478
nearest·knn·accuracy(2단원 8차시) · baseline(2단원 11차시). 고칠 곳 하나가 여기 있습니다(449줄).
480~516
【1】 파이프라인 — 읽고 · 다듬고 · 나누고 · 자 재고 · k를 돌린다. 고칠 곳 하나가 여기 있습니다(494줄).
518~582
【2】 검사표 — 빈칸 여섯 개(WANT1~WANT6). 오늘 첫 5분에 채웁니다.
584~588
오늘의 도전 네 가지.
💡 22분을 이렇게 씁니다

빈칸 여섯 5분 → ㄱ 3분 → ㄴ 5분 → ㄷ 6분 → 협업 기록표 3분.
운전대와 길잡이를 버그 하나마다 바꿉니다. 빈칸이 하나라도 남아 있으면 SyntaxError로 아예 실행되지 않으니, 빈칸이 먼저입니다.

증상은 차례로 드러난다

세 버그는 줄 서서 기다립니다. 하나를 고쳐야 다음 증상이 보입니다. 아래 표는 여러분 화면에 무엇이 나와야 하는지를 미리 적어 둔 것입니다. 화면이 이 표와 다르면 고친 곳이 다른 것입니다.

단계고친 것화면에 나오는 것검사표
0빈칸만 채웠다 ZeroDivisionError — ‘정제 160행 → 158행’ 두 줄만 찍히고 멈춘다 못 돈다
1ㄱ — FEATURES에서 "기기번호"를 뺀다 k 일곱 개가 모두 100.0% · 맞힌 수 110/110 · 화면의 테스트는 48 · 기준선 50.9% 3/6
2ㄴ — 494줄의 train_rows를 test_rows로 k 일곱 개가 모두 70.8%(34/48) · 기준선 60.4% · 차이 +10.4%p 4/6
3ㄷ — 449줄의 ranked[:1]을 ranked[:k]로 k=1 70.8% · k=3 81.2% · k=5 83.3% · k=7 85.4% · k=9 79.2% · k=15 75.0% · k=25 75.0% · 기준선 60.4% · 차이 +25.0%p 6/6
3단계의 표는 7차시의 표와 글자까지 같습니다. 그것이 “다 고쳤다”의 증거입니다 — ‘오류가 사라졌다’가 아니라 ‘검사가 통과했다’가 기준입니다.
⚠ 1단계에서 놓치기 쉬운 세 번째 단서

1단계의 기준선이 7차시의 60.4%가 아니라 50.9%로 나옵니다. 왜일까요? 테스트가 훈련과 같아지면 기준선까지 훈련의 것이 되기 때문입니다. 훈련 110개 중 ‘가능’이 56개라 56/110 = 50.9%가 된 것이지요. 정확도 100%보다 이 숫자가 더 조용한 단서입니다.

협업 기록표 — 고칠 때마다 한 줄

고친 뒤에 “고쳤다”고만 말하면 팀의 다른 사람은 그것을 되돌릴 수도, 이어받을 수도 없습니다. 무엇을 · 왜 · 무엇으로 확인했나 셋을 한 줄로 남깁니다. 아래 표는 화면에서 바로 적어도 되고, 공책에 옮겨 적어도 됩니다.

버그운전대 무엇을 고쳤나 (줄 번호와 함께)왜 그렇게 판단했나무엇으로 확인했나
ㄱ
ㄴ
ㄷ
AI에게
받은 것

‘무엇으로 확인했나’ 칸에 “돌아갔다”라고 쓰면 안 됩니다. “검사 6이 ✔로 바뀌었다”, “맞힌 수가 110/110에서 34/48로 바뀌었다”처럼 숫자나 판정으로 적으세요. 오늘 이 칸이 곧 10차시 실험 기록과 11차시 발표의 뼈대가 됩니다.

💻

손으로 ② — 버그 사냥터에서 증상을 눈에 익힌다

파이썬 파일에서 버그를 하나 고치는 데는 시간이 걸립니다. 아래 사냥터는 같은 계산을 브라우저에서 그대로 돌려, 버그를 스위치처럼 켜고 끄며 증상만 빠르게 견줘 보는 곳입니다. 나오는 숫자는 파이썬이 내는 숫자와 같습니다 — 158행을 110/48로 나누고 k-NN을 실제로 돌립니다.

세 가지로 씁니다. ① 파일이 막혀 있는 팀은 여기서 오늘 몫을 합니다. ② 다 고친 팀은 ‘셋 다 고치기’를 눌러 자기 화면과 견줍니다. ③ 씨앗을 바꿔 가며, 파일 한 벌로는 볼 수 없는 것을 봅니다 — 같은 버그가 어떤 데이터에서는 안 들킨다는 사실입니다.

🐛 버그 사냥터 — 세 스위치와 여섯 검사 INTERACTIVE

버그 셋을 하나씩 켜고 [▶ 돌려 보기]를 누르면 그 판의 결과가 그려집니다. ㄱ을 켜면 오류로 죽고, ㄴ은 정상처럼 보이는 틀린 값을 내며, ㄷ은 씨앗을 바꿔 가며 여러 번 돌려야 드러납니다. 아래 검사표에서 쓸 검사를 골라 보세요 — 고른 검사만으로 어느 버그가 잡히는지 판정이 나옵니다.

분할 씨앗
돌아갔나—
화면의 테스트—
실제로 채점한 수—
기준선—
가장 높은 k—
검사 통과—
쓴다검사기대실제판정가리키는 곳
1 dist가 뒤쪽 특성도 보는가5.0 ——거리 계산
2 정규화가 한가운데를 0.5로[0.5] ——정규화
3 nearest가 k개를 돌려주는가3 ——버그 ㄷ
4 knn이 k=3 다수결을 하는가"나" ——버그 ㄷ
5 테스트가 훈련에 섞이지 않았는가0 ——버그 ㄴ
6 폭이 0인 특성이 섞이지 않았는가0 ——버그 ㄱ
고른 검사로 잡히는 버그가 여기에 나옵니다.
[안내] 지금은 버그 셋이 다 살아 있고 씨앗은 42입니다. [▶ 돌려 보기]를 누르면 무슨 일이 나는지 보세요.

과제 ① — 네 판을 돌려 표를 채운다

스위치를 아래 순서대로 맞추고 한 판씩 돌려, 공책이나 아래 칸에 옮겨 적으세요. 씨앗은 42로 두고, 검사는 여섯 개를 다 씁니다.

스위치돌아갔나실제로 채점한 수 기준선가장 높은 k와 정확도검사 통과
ㄱ · ㄴ · ㄷ 다 켬
ㄴ · ㄷ (ㄱ 끔)
ㄷ 만 켬
셋 다 끔

두 번째 줄에서 ‘화면의 테스트’와 ‘실제로 채점한 수’를 나란히 보세요. 두 숫자가 다릅니다. 이것이 정확도 100%보다 확실한 증거입니다 — 100%는 의심할 이유이고, 이 어긋남이 증거입니다.

과제 ② — 반례를 만든다: 버그가 있는데 안 들키는 자리

이제 ㄷ만 켜 놓고 씨앗을 42 · 1 · 2 · 3 · 5로 바꿔 가며 돌려 보세요. ‘정직한 판 겹쳐 보기’를 켜 두면 회색 점선이 정직한 판입니다. 막대와 점선이 같은 높이인 k가 “버그가 있는데 안 들키는 k”입니다. 아래 칸에 그런 k를 모두 적으세요.

씨앗정직한 판의 k=1 정확도 버그판이 모든 k에서 내는 값안 들키는 k를 모두
42
1
2
3
5

다섯 줄을 다 채우면 마지막 칸에서 한 가지가 눈에 들어옵니다. 어느 씨앗에서든 k=1은 늘 안 들킵니다. 그리고 씨앗 하나에서는 k가 하나 더 늘어납니다. ‘재현이 어렵다’는 말은 이런 뜻입니다 — 같은 코드가 어떤 데이터에서는 옳은 답을 냅니다. k=1 하나만 돌려 본 팀에게는 이 버그가 어떤 데이터에서도 보이지 않습니다.

과제 ③ — 검사를 골라 본다

검사표의 ‘쓴다’ 칸을 꺼 보세요. 이 사냥터에서 확인할 것은 하나입니다 — 어떤 버그는 검사를 아무리 봐도 화면만으로는 못 잡습니다.

  • 검사를 하나도 안 쓰면: 버그 ㄴ이 살아 있어도 화면은 “정확도 100%”라고만 말합니다.
  • 검사 5만 켜면: 버그 ㄴ이 잡힙니다. 다른 둘은 안 잡힙니다.
  • 검사 3·4만 켜면: 버그 ㄷ이 잡힙니다. 다른 둘은 안 잡힙니다.
  • 검사 6만 켜면: 버그 ㄱ이 잡힙니다. 그리고 ㄱ이 켜져 있으면 검사 5는 아예 돌지 못합니다 — 판정 칸에 ‘—’가 뜹니다. 앞에서 죽으면 뒤의 검사는 아무것도 지키지 못하니까요.
ℹ️ 오늘의 도전 네 가지
  • 검사 6을 파이프라인 앞으로 옮겨 보세요. 오류 메시지가 어떻게 달라지나요?
  • 세 버그를 고친 뒤 k 표를 7차시의 표와 나란히 놓아 보세요. 같은가요?
  • 검사 1의 값을 [3,4,0,0,0]으로 바꿔 보세요. 그래도 잡히는 버그가 있고 놓치는 버그가 있습니다.
  • ranked[:k]를 ranked[:k+1]로 한 칸만 어긋나게 해 보세요. 실제로 재 보면 k=3과 k=5에서는 정확도가 2.1%p 올라가고 k=7에서는 6.2%p 떨어집니다. 점수가 올랐다고 옳아진 것이 아닙니다. 이 버그는 눈으로는 안 보이고 검사 3에서는 보입니다(값이 4로 나옵니다).
📖

정리 — 오늘 팀이 만든 것

오늘 팀이 손에 쥐고 나가는 것은 셋입니다.

  1. 돌아가는 파이프라인 파일 하나. 세 곳(33줄 · 449줄 · 494줄)을 고쳐 k 표가 7차시와 같아진 판입니다. 다음 시간 9차시가 이 파일 위에서 시작합니다.
  2. 검사표 6/6. ‘오류가 안 난다’가 아니라 ‘여섯 검사가 통과한다’가 오늘부터 우리 팀의 ‘다 됐다’입니다.
  3. 협업 기록표 네 줄. 무엇을 · 왜 · 무엇으로 확인했나. 10차시의 실험 기록과 11차시의 발표가 이 표에서 자랍니다.
💡 여기까지 못 했으면 이렇게 하세요

검사 3·4가 ✘로 남아 있다면 nearest의 마지막 줄을 보세요. 449줄입니다. return ranked[:1]을 return ranked[:k]로 바꾸면 됩니다. 그것만 고쳐도 k 표가 7차시와 같아지고 검사가 6/6이 됩니다.

ㄱ에서 막혀 있다면 33줄의 FEATURES에서 "기기번호" 하나만 지우세요. 검사 6이 곧바로 ✔로 바뀝니다.

파일을 아직 못 돌렸다면 위의 버그 사냥터만 해도 오늘 몫은 됩니다. 같은 계산이라 나오는 숫자가 같습니다. 과제 ①·②의 표 두 개를 채워 오세요.

ℹ️ 자기 팀 데이터가 아직 없어도 오늘은 끝난다

위 파일의 【0】과 DATA를 그대로 두고 돌리면 됩니다. 견본은 페트병 사진 160장을 사람이 눈으로 재어 적은 표이고, 지어낸 데이터입니다. 우리 학교의 실제 기록이 아니니 결과를 사실처럼 인용하지는 마세요. 오늘 배우는 것은 이 데이터의 값이 아니라 버그를 찾는 방법이라 견본으로도 다 배웁니다.

자기 데이터를 넣을 때 가장 먼저 만나는 오류 둘도 미리 알아 두면 좋습니다. NAMES를 안 고치고 DATA의 열 수만 바꾸면 ValueError: 열 수가 안 맞는 줄 [0, 1, 2]가 나고, FEATURES에 글자 열(예: 촬영조건)을 넣으면 TypeError: unsupported operand type(s) for -: 'str' and 'str'가 납니다. 둘 다 읽을 수 있는 문장으로 죽습니다 — 6차시가 load()에 방어를 세워 둔 까닭입니다.

다음 시간까지 할 것

9차시는 오늘 살려 낸 그 파일 위에서 2단원 12차시의 네 칸을 다시 꺼냅니다. “전체 85.4%”가 누구의 85.4%인지를 묻는 시간이에요. 그래서 다음 시간까지 팀이 정해 와야 할 것이 하나 있습니다.

  • 성능을 쪼개어 볼 ‘집단 열’ 하나를 정해 오세요. 견본에서는 촬영조건(밝음/어두움)입니다. 요일 · 학년 · 수집한 사람 — 무엇이든 좋지만 한 집단에 15개 이상이어야 그 %를 성능이라 부를 수 있습니다.
  • 협업 기록표를 팀 문서에 옮겨 두세요. 오늘 칸이 비어 있으면 10차시에 되살릴 수 없습니다.
  • 오늘 고친 세 곳을 팀원 모두가 각자의 파일에 반영했는지 확인하세요. 한 사람만 고친 파일을 다음 시간에 들고 오면 오늘의 합치기 사고가 그대로 되풀이됩니다.
✅

확인 문제

✍️ 문제마다 답을 쓰고 제출하기를 누르세요. 제출하면 모범 답안이 열리고, 제출한 답은 선생님께 전달됩니다.

1. 짝 프로그래밍에서 길잡이가 하는 일을 두 가지 쓰고, 왜 길잡이는 키보드를 잡지 않는지 설명하세요. 그리고 막혔을 때 사람을 부르기 전에 밟는 네 단계를 순서대로 쓰세요.
📖 모범 답안

길잡이가 하는 일: ① 화면을 읽으며 지금 쳐 넣는 줄이 하려던 것과 맞는지 확인한다. ② 의심스러운 곳을 질문으로 짚는다(“위아래 두 줄이 똑같은데 아래만 train인 건 일부러야?”).

키보드를 잡지 않는 까닭: 사람은 자기가 방금 쓴 줄을 쓴 대로가 아니라 쓰려던 대로 읽습니다. 치는 사람과 읽는 사람이 같으면 그 착각이 걸러지지 않아요. 오늘의 버그 ㄴ이 바로 ‘복사한 두 줄 중 아랫줄’에 있었는데, 이런 자리는 친 사람 눈에 가장 안 보입니다.

네 단계: ① 증상을 한 문장으로 적는다 → ② 어디까지 맞는지 범위를 좁힌다 → ③ 오류 메시지를 끝까지 읽는다 → ④ 가장 작은 재현 예를 만든다. 그다음에 사람을 부릅니다. ④가 중요한 까닭은, 작게 줄여서 손으로 답을 낼 수 있게 되면 그것이 곧 검사 한 줄이 되기 때문입니다.

2. 값이 모두 같은 열(예: 늘 3인 기기번호)을 FEATURES에 넣으면 왜 정규화에서 사고가 나는지 식으로 설명하세요. 그리고 clean() 안에 있는 if sd == 0: continue가 이 사고를 막아 주지 못하는 까닭도 쓰세요.
📖 모범 답안

식: 정규화는 (v − lo) / (hi − lo)입니다. 값이 모두 3이면 lo = hi = 3이므로 분모가 3 − 3 = 0이 됩니다. 0으로 나눌 수 없으니 파이썬이 ZeroDivisionError: division by zero를 냅니다. 뜻으로 풀면 이렇습니다 — “가장 작은 값을 0, 가장 큰 값을 1로 놓아라”는데 둘이 같은 값이면 놓을 자리가 없습니다.

clean()이 막아 주지 못하는 까닭: 그 줄은 이상치 판정을 건너뛰는 장치입니다. 표준편차가 0인 열에서 z 점수를 내면 0으로 나누기가 되니 그 열은 이상치 검사를 하지 않고 지나가는 것이지요. 정규화는 그다음에 별도로 일어나고, 거기까지 지켜 주지는 않습니다. 두 곳을 헷갈리면 “clean이 통과했으니 괜찮다”고 잘못 믿게 됩니다. 상수 열은 애초에 FEATURES에서 빼는 것이 답입니다 — 값이 모두 같은 열은 어떤 것도 구별해 주지 못하니까요.

3. (오늘 얻은 결과) 버그 ㄱ만 고친 1단계 화면에서 “이상하다”고 짚을 수 있는 것을 숫자와 함께 세 가지 쓰세요. 그중 어느 것이 증거이고 어느 것이 의심할 이유인지 가르세요.
📖 모범 답안

① 정확도가 일곱 개 k에서 모두 100.0% — 의심할 이유입니다. 드물지만 정말로 쉬운 문제일 수도 있으니 이것만으로 단정하면 안 됩니다.

② 화면은 ‘테스트 48’이라고 적었는데 맞힌 수는 110/110 — 증거입니다. 같은 화면 안의 두 숫자가 서로 어긋나므로, 어느 한쪽이 반드시 틀렸습니다.

③ 기준선이 7차시의 60.4%가 아니라 50.9% — 증거에 가까운 단서입니다. 테스트가 훈련과 같아지면 기준선까지 훈련의 것이 되기 때문입니다(훈련 110개 중 ‘가능’이 56개 → 56/110 = 50.9%).

그리고 검사 5가 0이어야 하는데 110을 냅니다. 이것이 가장 분명한 증거이고, 그래서 “먼저 의심하고, 검사로 확인한다”가 순서입니다.

4. (오늘 얻은 결과) 버그 사냥터에서 ㄷ만 켜고 씨앗을 2로 두고 돌리면, 버그가 있는데도 정직한 판과 값이 같아지는 k가 둘입니다. 그 둘을 쓰고, 왜 그렇게 되는지 설명하세요. 그리고 이 사실이 ‘k=1만 돌려 보는 팀’에게 뜻하는 바를 한 문장으로 쓰세요.
📖 모범 답안

k = 1과 k = 3입니다. 씨앗 2에서 정직한 판은 k=1 68.8% · k=3 68.8%로 값이 같고, 버그판은 k를 무시하고 늘 가장 가까운 하나만 보므로 모든 k에서 68.8%를 냅니다. 그래서 이 두 자리에서는 버그가 있는 판과 없는 판의 숫자가 구별되지 않습니다.

왜 그런가: 버그판은 늘 ‘정직한 k=1’처럼 답합니다. 따라서 원래 k=1과 답이 같아지는 k에서는 절대 들키지 않습니다. 그런 k가 몇 개인지는 데이터에 따라 달라져서, 씨앗 42·1·3·5에서는 k=1 하나뿐이지만 씨앗 2에서는 k=3까지 둘입니다.

k=1만 돌려 보는 팀에게: 이 버그는 어떤 데이터에서도 영영 보이지 않습니다. “내 컴퓨터에서는 되는데”라는 말이 나오는 자리가 여기이고, 그래서 k를 여러 개 돌리는 것이 성능을 높이기 위해서만이 아니라 버그를 드러내기 위해서이기도 합니다.

5. 어떤 팀이 “우리 모델 정확도 100% 나왔어요!”라고 합니다. 가장 먼저 무엇을 의심해야 하며, 그것을 무엇으로 확인하겠습니까? 또 오늘 만난 세 버그 가운데 가장 위험한 것이 무엇인지 까닭과 함께 쓰세요.
📖 모범 답안

가장 먼저 의심할 것: 테스트가 훈련에 섞였는가입니다. 시험 문제를 미리 보여 준 셈이면 100%는 당연하니까요.

무엇으로 확인하는가: 두 가지입니다. ① 화면이 말하는 테스트 개수와 실제로 채점한 개수가 같은지 본다(우리 판에서는 48 vs 110). ② 검사 5를 돌린다 — 테스트 점 가운데 훈련에도 있는 것의 수가 0이어야 하는데 110이 나옵니다. 100%는 증거가 아니라 의심할 이유이고, 증거는 이 두 숫자입니다.

가장 위험한 버그: ㄴ(조용히 틀리는 것)입니다. ㄱ은 죽어서 스스로 알려 주고, ㄷ은 설정을 여러 개 돌리면 드러납니다. 그런데 ㄴ은 오류도 없고 결과도 좋아 보여서 아무도 다시 보지 않습니다. 게다가 결과가 좋으면 사람은 더 의심하지 않지요. “너무 좋은 결과는 먼저 의심하고 나서 기뻐한다”가 점검표 ②번에 들어 있는 까닭입니다.

6. 팀원이 생성형 AI에게 받은 코드를 통째로 붙여 넣자고 합니다. 도덕이 아니라 공학의 이유로 왜 그대로 쓰면 안 되는지 설명하고, 그래도 쓰기로 했을 때 팀이 반드시 하는 세 가지를 쓰세요. 그리고 오늘의 합치기 사고를 되풀이하지 않으려면 다음 시간부터 우리 팀은 역할을 어떻게 나누어야 할지 한 문장으로 쓰세요.
📖 모범 답안

공학의 이유: 그 코드는 우리 데이터를 본 적이 없습니다. 우리 FEATURES에 상수 열이 있는지, 우리가 베껴 온 nearest가 이미 망가져 있는지 모릅니다. 읽어서 그럴듯한 것은 아무 보증이 되지 않아요 — 오늘의 버그 ㄴ·ㄷ도 읽기에는 완벽하게 그럴듯했습니다.

반드시 하는 세 가지: ① 돌려 본다. ② 여섯 검사에 걸어 본다(통과 수가 줄면 되돌린다). ③ 무엇을 물었고 무엇을 받았고 어디를 우리가 고쳤는지를 협업 기록표에 한 줄로 남긴다. 3단원에서 세운 비판적 자세가 여기서는 ‘검사표를 돌린다’라는 구체적인 행동이 됩니다.

역할 나누기: “주제가 아니라 파일 단위로 나누고, 어쩔 수 없이 한 파일을 나눌 때는 고칠 곳을 먼저 말로 나눈 뒤 짧게 자주 합치고 합칠 때마다 검사표를 다시 돌린다.” 오늘 재 보니 통째로 덮어쓰는 방식은 네 판 중 셋이 못 쓰는 판이 되었고, 그중 둘은 이미 고쳤던 오류로 다시 죽었습니다.

🔁 되돌아보기

오늘 우리는 성격이 다른 버그 셋을 죽는 것 → 조용한 것 → 가끔만인 것 순서로 잡았고, ‘다 됐다’의 기준을 오류가 안 난다에서 검사 여섯이 통과한다로 바꾸었습니다. 검사표는 3/6에서 4/6, 6/6이 되었지요. 다음 시간에는 되살아난 그 85.4%를 들고 물을 것입니다 — 그 85.4%는 누구의 85.4%인가?

🔎

더 알아보기

버그를 적어 둔 첫 기록, 베껴 온 부품이 부른 사고, 그리고 ‘줄 단위로 합친다’는 말의 속

모눈 작업 일지 한 쪽. 시각별 기록 사이에 나방 한 마리가 테이프로 붙어 있고, 옆에 Relay #70 Panel F (moth) in relay, 아래에 First actual case of bug being found라고 손글씨로 적혀 있다
역사

1947년의 나방 — 버그를 ‘기록한’ 첫 장면

사진은 하버드 대학교의 계전기 계산기 Mark II를 돌리던 팀의 작업 일지입니다. 1947년 9월 9일, 기계가 이상하게 동작하자 운영자들이 속을 뒤졌고, 70번 계전기(Panel F) 사이에 끼어 있던 나방을 찾아냈습니다. 그들은 나방을 테이프로 일지에 붙이고 “버그가 실제로 발견된 첫 사례”라고 적었습니다. 이 일지는 지금 미국 스미스소니언 국립 미국사 박물관에 있습니다.

흔히 이 나방에서 ‘버그’라는 말이 생겼다고 하지만 그렇지 않습니다. 기계의 결함을 버그라고 부르는 말은 토머스 에디슨의 편지에도 나올 만큼 오래되었고, ‘실제 사례’라는 문구 자체가 그 말이 이미 쓰이고 있었다는 농담입니다. 이 이야기를 널리 알린 사람은 이 팀에서 일했던 컴퓨터 과학자 그레이스 호퍼입니다.

오늘 차시와 이어지는 것은 나방이 아니라 일지의 짜임입니다. 시각(15:45) · 어디서(70번 계전기) · 무엇을 찾았나(나방) · 그다음 무엇을 돌렸나(16:30 arctan 계산 시작)가 한 쪽에 모두 적혀 있어요. 우리 협업 기록표의 ‘무엇을 · 왜 · 무엇으로 확인했나’와 같은 생각입니다. 기록이 남았기 때문에 여든 해 가까이 지난 지금도 그날 무슨 일이 있었는지 정확히 말할 수 있습니다.

사진: Mark II 작업 일지(1947년 9월 9일) · 출처: Courtesy of the Naval Surface Warfare Center, Dahlgren, VA., 1988., Wikimedia Commons (Public domain)

어두운 밤하늘 아래 발사대에서 조명을 받으며 서 있는 흰 Ariane 5 로켓. 바로 옆에 esa·ariane 글자가 적힌 흰 구조물이 붙어 있고, 둘레에 높은 철탑들이 서 있다
현장

‘고치지 않는다’ 상자가 로켓을 떨어뜨렸다 — Ariane 5 501편

1996년 6월 4일, 유럽의 새 로켓 Ariane 5의 첫 비행(501편)은 이륙하고 40초도 안 되어 궤도를 벗어나 부서졌습니다. 조사단이 찾아낸 원인은 로켓의 자세를 재는 관성 기준 장치의 소프트웨어였습니다. 이 소프트웨어는 앞 세대 Ariane 4에서 여러 해 문제없이 쓰던 것을 그대로 가져온 것이었어요.

Ariane 5는 비행 초반에 수평 방향 속도가 Ariane 4보다 훨씬 크게 나옵니다. 그래서 소프트웨어 안의 한 값이 커져, 64비트 실수를 16비트 정수로 바꾸는 자리에서 범위를 넘쳤습니다. 장치는 오류를 내고 멈췄고, 똑같은 소프트웨어를 돌리던 예비 장치도 같은 이유로 먼저 멈춰 있었습니다. 게다가 문제의 계산은 이륙 전 정렬에만 필요하고 이륙 뒤에는 아무 쓸모가 없는 부분이었습니다.

오늘의 버그 ㄷ과 뿌리가 같습니다. “검증된 부품이니 고치지 않는다”는 믿음은 그 부품이 놓인 환경이 같을 때만 맞습니다. 그리고 오류가 터진 자리(변환 한 줄)와 고쳐야 할 자리(새 로켓의 비행 자료로 다시 검사하지 않은 일)가 달랐다는 점은 버그 ㄱ과 같습니다. 예비 장치를 두 벌 두어도 같은 코드라면 같은 버그도 두 벌이라는 교훈도 남겼습니다. 사진은 501편이 아니라 2021년 12월 제임스 웹 우주 망원경을 싣고 발사대에 선 Ariane 5입니다.

사진: 제임스 웹 우주 망원경을 실은 Ariane 5(2021년 12월, 기아나 우주 센터) · 출처: Bill Ingalls, Wikimedia Commons (Public domain)

공통 조상 — 나누기 전 원본 33 FEATURES 에 기기번호 449 ranked[:1] 494 train_rows 갑의 판 33줄만 고침 을의 판 494줄만 고침 병의 판 449줄만 고침 줄마다 ‘조상과 달라진 쪽’을 고른다 33 ✔ · 449 ✔ · 494 ✔ — 셋 다 산다 같은 줄을 둘이 고쳤으면 ‘충돌’로 멈춘다
원리 더 깊이

‘줄 단위로 합친다’의 속 — 비교하는 판은 둘이 아니라 셋이다

개념 4의 표에서 통째로 덮어쓰면 네 판 중 셋을 못 썼고, 줄 단위로 합치면 세 수정이 다 살았습니다. Git 같은 버전 관리 도구가 줄 단위로 합칠 수 있는 비결은 공통 조상에 있습니다. 두 사람의 파일만 견주면 “33줄이 다르다”는 것만 알 뿐 누가 고친 것인지는 모릅니다. 갈라지기 전의 원본까지 세 판을 나란히 놓아야 “갑의 판만 조상과 달라졌으니 갑이 고친 것”이라고 판단할 수 있어요.

그래서 도구는 줄마다 조상과 달라진 쪽을 고릅니다. 갑이 33줄, 을이 494줄, 병이 449줄을 고쳤다면 세 줄이 모두 새 판에 들어갑니다. 두 사람이 같은 줄을 서로 다르게 고쳤을 때만 도구가 고르지 않고 ‘충돌’이라고 표시해 사람에게 넘깁니다. 역할을 파일 단위로, 적어도 줄 단위로 먼저 나눠 두라는 규칙은 이 충돌을 줄이는 방법입니다.

하지만 도구는 글자만 봅니다. 충돌 없이 합쳐졌다고 해서 합친 코드가 옳다는 뜻은 아닙니다 — 따로 떼어 놓으면 둘 다 맞는 수정이 한 파일에 모이면 틀릴 수도 있으니까요. 그래서 규칙 셋째가 “합친 뒤 검사표를 다시 돌린다”입니다. 합치기 도구가 줄을 지키고, 검사표가 뜻을 지킵니다.