Proven C Book←↑→

103 일이 굴러가는 방식 — 어휘와 절차

먼저 알아야 할 것

102장 빌드와 시험 · 게이트·골든·시험의 갈래
53장 오류와 계약 · 계약이라는 생각

돌아보기

102장에서 무엇을 어디에 둘지, 무엇을 시험할지를 정했다. 그러면 남은 것은 그대로 하면 되는 일 아닌가 — 여기에 또 무엇을 배울 것이 있는가?

답. 남은 것은 일하는 방식이고, 그것은 배치와 다른 문제다. 혼자 짤 때는 방식이 머릿속에 있어도 된다. 그러나 사람이 셋만 되어도 「이거 시험했어?」라는 물음에 세 사람이 서로 다른 것을 떠올린다. 한 사람은 손으로 돌려 본 것을, 한 사람은 자동으로 도는 것을, 한 사람은 아직 안 짠 것을 생각한다.

★ 그래서 이 장은 낱말로 시작한다. 같은 것을 같은 이름으로 부르게 되는 순간 팀의 대화가 짧아지고, 짧아진 만큼 어긋남이 준다.

이 장의 필요성과 맥락

102장이 프로젝트를 세울 때 펼치는 장이라면, 이 장은 일을 굴릴 때 펼치는 장이다. 여기 나오는 것들은 큰 프로젝트가 실제로 쓰는 장치이지만, 하나같이 혼자 짜는 프로그램에서도 오늘 쓸 수 있다 — 오히려 혼자일 때 익혀 두어야 여럿이 되었을 때 설명하지 않아도 된다.

이 장이 끝나면

먼저 시험을 둘러싼 낱말 넷을 익힌다 — 픽스처(시험이 딛고 서는 판), 증인(결함이 돌아오면 우는 검사), 음성 검사(그 검사가 정말 무는지 확인하는 일), 그리고 래칫(늘면 안 되는 수). 그다음 검사를 언제 얼마나 돌릴지 정하는 티어를 보고, 마지막으로 한 건의 일이 들어와서 커밋으로 끝나기까지의 절차를 본다. 낱말마다 한 줄 정의와 실물 하나를 함께 둔다.

이 장에서 답할 질문

  1. 범위를 미리 적으라는 것이, 하다가 발견한 문제를 그냥 두라는 뜻인가?

103.1 이름이 없으면 같은 것을 다르게 부른다#

먼저 이 장에서 쓸 낱말을 한자리에 모은다. 뒤에서 하나씩 실물과 함께 볼 것이므로, 지금은 훑고 지나가도 된다.

낱말한 줄로
픽스처 / fixture시험이 딛고 서는 고정된 준비물 — 입력 파일, 정답 파일, 미리 만든 상태
증인 / witness「이 결함이 돌아오면 운다」고 세워 둔 검사 하나
회귀 / regression고쳤던 결함이 되돌아오는 것. 증인이 막는 대상
음성 검사 / negative test일부러 고장 내어 검사가 정말 무는지 확인하는 일
래칫 / ratchet늘면 안 되는 수를 적어 둔 기준선 — 톱니바퀴처럼 한 방향으로만 움직인다
티어 / tier검사의 깊이. 바뀐 것에 따라 어디까지 돌릴지 정한다
게이트 / gate통과 못 하면 못 들어가는 자동 검사 묶음(102장)
스필 / spill작업 중 발견했지만 이번 범위 밖이라 적어 두고 넘기는 것

표 103.1 — 이 장의 낱말

★ 이 낱말들은 어느 도구의 이름이 아니다. 도구는 프로젝트마다 다르지만 부르는 자리는 같다. 그래서 낱말만 익혀 두면 처음 보는 저장소에서도 무슨 일이 벌어지는지 읽힌다.

103.2 픽스처 — 시험이 딛고 서는 판#

시험 하나를 돌리려면 준비물이 있어야 한다. 읽을 입력, 견줄 정답, 미리 만들어 둔 임시 폴더, 정해진 값이 들어 있는 자료. 그 준비물 묶음을 「픽스처(fixture)」라 부른다.

102장의 골든 시연에 이미 실물이 있었다. 이름만 안 붙였을 뿐이다.

자리무엇
in/two-lines.txt픽스처 — 시험이 먹는 고정된 입력
expected/two-lines.txt픽스처 — 굳혀 둔 정답
wordcount.c시험 대상. 픽스처가 아니다
run.sh시험을 돌리고 견주는 틀

표 103.2 — 골든 시연에서 무엇이 픽스처였나

★ 픽스처의 조건은 하나다 — 매번 같아야 한다. 어제와 오늘이 다르면, 시험이 실패했을 때 「코드가 틀렸나, 준비물이 달라졌나」를 가릴 수 없다. 그 순간 그 시험은 답을 주는 물건이 아니라 물음을 하나 더 만드는 물건이 된다.

그래서 픽스처로 쓰면 안 되는 것들이 있다.

무엇왜
지금 시각·난수돌릴 때마다 달라진다. 필요하면 값을 고정해 넣는다
네트워크 너머의 것남의 사정으로 시험이 빨강이 된다
먼저 돈 시험이 남긴 파일시험의 순서가 결과를 바꾼다 — 가장 찾기 어려운 종류의 사고다
내 기계에만 있는 경로남에게서 안 돈다(102장의 「내 기계에서는 됩니다」)

표 103.3 — 픽스처로 삼으면 안 되는 것

말의 뿌리도 그 성질에 있다. 건물에 붙박이로 달린 조명이나 배관을 영어로 fixture 라 부르는데, 옮겨 다니지 않고 늘 그 자리에 있다는 뜻이 시험 쪽으로 건너온 것이다.

103.3 증인 — 검사는 결함의 묘비다#

버그를 하나 고쳤다고 하자. 그 자리에 무엇을 남길 것인가. 아무것도 남기지 않으면 그 버그는 다시 돌아온다. 사람이 바뀌고, 코드가 고쳐지고, 아무도 그 사연을 모르기 때문이다. 되돌아온 결함을 회귀라 부른다(102장에서 이름만 스쳤다).

막는 방법은 하나다. 고친 자리마다 「이 결함이 돌아오면 우는 검사」를 하나 세워 둔다. 그것이 「증인(witness)」이다.

★ 그리고 순서가 중요하다 — 고치기 전에 세운다. 고친 뒤에 세우면 그 검사가 정말 무는지 알 수 없다. 처음부터 초록인 검사와, 결함을 잡아 빨강이었다가 고쳐서 초록이 된 검사는 겉보기에 같지만 값어치가 전혀 다르다.

examples/ch103/witness/run.sh

#!/bin/sh
# 증인을 *고치기 전에* 세운다.
#
#   ① 결함이 있는 판에 대고 돌린다 --- 증인이 울어야 한다(빨강).
#      울지 않으면 그 증인은 아무것도 지키지 않는 것이다.
#   ② 고친 판에 대고 돌린다 --- 조용해야 한다(초록).
#
# 이 순서가 요점이다. 초록만 본 검사는 *무는지 모르는* 검사다.
set -eu
cd "$(dirname "$0")"
CC=${CC:-cc}
CFLAGS="-std=c23 -Wall -Wextra"

printf '== (1) the version with the defect (a witness must cry)\n'
$CC $CFLAGS -o witness-buggy find-buggy.c witness.c
if ./witness-buggy; then
    printf '   * no witness cried --- this witness is useless\n'
    rc=1
else
    printf '   -> it cried, as intended\n'
    rc=0
fi

printf '\n== (2) the fixed version (all must be quiet)\n'
$CC $CFLAGS -o witness-fixed find.c witness.c
./witness-fixed || rc=1

rm -f witness-buggy witness-fixed
exit $rc

실행 결과

== (1) the version with the defect (a witness must cry)
  ok   finds a value in the middle
  ok   finds the first
  FAIL * finds the last - one cell of edge  (got -1, wanted 4)
  ok   an absent value gives -1
  ok   an empty array gives -1 too
a witness cried
   -> it cried, as intended

== (2) the fixed version (all must be quiet)
  ok   finds a value in the middle
  ok   finds the first
  ok   * finds the last - one cell of edge
  ok   an absent value gives -1
  ok   an empty array gives -1 too
all five witnesses are quiet

시연이 그 순서를 그대로 보인다. 이진 탐색에서 lo <= hi 를 lo < hi 로 적는 흔한 실수를 두고, 증인 다섯을 먼저 세웠다.

  1. 결함이 있는 판에 대고 돌리면 「맨 뒤를 찾는다」 하나만 운다. 나머지 넷은 조용하다 — 증인이 어디가 잘못됐는지까지 짚어 준다는 뜻이다.
  2. 고친 판에서는 다섯이 다 조용하다.

그리고 증인 하나하나에 왜 중요한가가 문장으로 붙어 있다.

check("★ 맨 뒤를 찾는다 — 경계 한 칸", find(sorted, n, 50),  4);

수가 아니라 이 문장이 자산이다. 몇 달 뒤 이 검사가 울었을 때, 사람이 읽는 것은 「검사 3번 실패」가 아니라 「경계 한 칸을 못 본다」이기 때문이다.

반례. 고친 다음에 검사를 짜기

가장 흔한 순서이고, 가장 값이 낮은 순서다. 이미 고친 코드에 대고 짠 검사는 반드시 초록이므로, 그것이 무는 검사인지 아무것도 안 하는 검사인지 구분할 방법이 없다.

실제로 아무것도 안 하는 검사가 만들어진다 — 함수를 부르지 않고 상수만 견주거나, 조건을 거꾸로 적었거나, 예외를 삼켜 버리는 검사들이다. 그런 검사는 있다는 사실만으로 사람을 안심시키므로, 없는 것보다 나쁘다.

고칠 시간이 없다면 순서만이라도 지킨다 — 결함을 재현하는 최소한의 코드를 먼저 만들고, 그것이 우는 것을 눈으로 본 다음에 고친다.

103.4 음성 검사 — 그 검사가 정말 무는가#

증인의 순서에서 배운 것을 규칙으로 세우면 이렇게 된다. 새 검사를 지으면 반드시 한 번 고장 내 본다. 이것을 「음성 검사(negative test)」라 부른다 — 「없는 것을 없다고 하는가」가 아니라 「있는 것을 있다고 하는가」를 확인하는 일이다.

방법은 간단하다. 검사가 잡아야 할 결함을 일부러 넣고, 검사가 우는지 보고, 되돌린다.

증상무슨 뜻인가
고장 냈는데 조용하다검사가 그 자리를 안 본다 — 지금 고친다
고장 냈더니 다른 검사가 운다검사들이 겹쳐 있다 — 어느 쪽이 진짜 주인인지 정한다
고장 안 냈는데 운다검사가 헛짚는다. 이대로 두면 사람이 검사를 무시하게 된다
뜻대로 그것만 운다쓸 만한 검사다

표 103.4 — 음성 검사로 드러나는 것

실제 사례. 이 책이 그렇게 짓는다

이 책의 검사기(102장의 게이트들)는 전부 이 순서로 지어졌다. 검사를 하나 만들면 고친 자리를 일부러 되돌려 놓고 돌려 본다. 잡히면 복구하고, 안 잡히면 검사를 고친다.

★ 실제로 그 과정에서 검사기가 두 번 눈감고 있던 것이 드러났다. 한 번은 정규식이 한국어 인용부호 안을 통째로 건너뛰고 있었고, 또 한 번은 원고가 줄을 접어 쓰는데 검사가 줄바꿈에서 멈춰 여러 줄에 걸친 문장을 통째로 놓치고 있었다. 둘 다 검사는 초록이었다 — 볼 것을 안 보고 있었을 뿐이다.

그래서 규율이 하나 늘었다. 초록인 것과 보고 있는 것은 다르다.

103.5 래칫 — 늘면 안 되는 수#

프로젝트에는 「0 이면 좋지만 당장 0 으로 못 만드는」 수들이 있다. 경고의 수, 아직 못 고친 결함의 수, 시험이 안 도는 자리의 수 같은 것들이다. 이런 수를 다루는 방법은 둘 가운데 하나다 — 못 본 척하거나, 늘지 못하게 막거나.

뒤쪽이 「래칫(ratchet)」이다. 지금의 수를 파일에 적어 두고, 그보다 늘면 게이트가 운다. 톱니바퀴가 한 방향으로만 도는 것과 같아서 붙은 이름이다.

상황래칫이 없으면래칫이 있으면
경고 300 개301 개가 되어도 아무도 모른다301 이 되는 순간 막힌다
하나 고쳤다수가 299 인 채 굴러간다사람이 기준선을 299 로 조인다
급해서 하나 늘리고 싶다조용히 는다기준선을 올리려면 그 줄을 고쳐야 한다 — 흔적이 남는다

표 103.5 — 래칫이 있을 때와 없을 때

★ 마지막 줄이 래칫의 진짜 값어치다. 늘리는 것을 금지하는 것이 아니라 눈에 보이게 만드는 것이다. 정말 필요하면 기준선을 올리면 된다. 다만 그 순간 그 변경이 커밋에 남고, 리뷰에서 「이건 왜 늘었나」를 묻게 된다.

그리고 조이는 쪽은 사람이 한다. 수가 줄었다고 기계가 자동으로 기준선을 낮추면, 어느 날 우연히 줄었다가 다시 늘 때 막지 못한다 — 래칫이 사라지는 것이다.

103.6 티어 — 늘 전부 돌릴 수는 없다#

검사를 많이 세울수록 전부 돌리는 데 오래 걸린다. 그러면 사람은 안 돌리기 시작한다 (102장의 make clean 과 같은 이야기다). 그래서 검사에도 깊이를 둔다. 바뀐 것에 따라 어디까지 돌릴지 정하는 것이 「티어(tier)」다.

깊이무엇을 돌리나언제
얕게빌드 · 서식 · 빠른 단위 시험저장할 때마다, 몇 초
보통단위 시험 전부 · 골든커밋 전, 몇십 초
깊게통합 · 느린 시험 · 여러 설정으로 빌드내보내기 전, 몇 분

표 103.6 — 깊이를 나누는 예

두 가지 규율이 붙는다.

  1. 깊이는 바뀐 것이 정한다. 문서만 고쳤다면 깊게 돌릴 까닭이 없고, 핵심 자료구조를 건드렸다면 얕게 넘어가면 안 된다.
  2. ★ 올릴 수만 있다. 「급하니 이번엔 얕게」는 규율을 무너뜨리는 첫걸음이다. 깊이를 내려서 통과시키는 물건이 아니다.

103.7 한 건의 일 — 결정과 실행을 가른다#

이제 절차다. 요청 하나가 들어와서 커밋으로 끝나기까지, 큰 프로젝트가 실제로 밟는 걸음은 대체로 이렇다.

걸음무엇을 하나빠뜨리면
① 가른다무엇이 결정이고 무엇이 실행인가혼자 정해 버린 결정이 나중에 뒤집힌다
② 잰다추측 대신 실제로 재어 본다엉뚱한 자리를 고친다
③ 범위를 적는다이번에 건드릴 자리를 미리 적어 둔다일이 끝없이 번진다
④ 고친다한 번에 한 뜻되돌릴 수 없는 커밋이 된다
⑤ 검증한다그 깊이에 맞는 검사를 통과시킨다남의 시간으로 검증하게 된다
⑥ 적는다커밋 메시지에 왜를 남긴다몇 달 뒤 아무도 이유를 모른다

표 103.7 — 한 건의 일이 지나는 걸음

★ 첫 걸음이 가장 자주 빠지고 가장 비싸다. 결정과 실행은 다른 종류의 일이다.

이 둘을 섞으면 양쪽이 다 나빠진다. 실행까지 물으면 일이 안 굴러가고, 결정까지 혼자 하면 나중에 통째로 되돌리게 된다.

두 번째 걸음도 그만큼 값한다. 「느린 것 같다」·「아마 여기가 문제일 것이다」로 시작한 작업은 자주 헛다리를 짚는다. 재고 나서 계획이 뒤집히는 일이 흔하고, 그것이 정상이다. 뒤집혔다면 그 사실을 기록에 남긴다 — 다음 사람이 같은 자리를 다시 짚지 않도록.

문. 범위를 미리 적으라는 것이, 하다가 발견한 문제를 그냥 두라는 뜻인가?

답. 아니다. 적어 두고 넘기라는 뜻이다. 그것을 「스필(spill)」이라 부른다.

일을 하다 보면 범위 밖의 문제가 반드시 눈에 띈다. 그때 선택지는 셋이다 — 지금 고친다, 못 본 척한다, 적어 두고 넘긴다. 첫째는 한 커밋에 두 뜻이 섞여 되돌리기 어려워지고, 둘째는 그냥 잃어버린다. 셋째가 답이다.

적는 자리는 거창할 필요가 없다. 이슈 하나, 백로그 한 줄, 파일 맨 아래 목록 한 줄이면 된다. 요점은 발견을 잃지 않으면서 이번 일의 경계를 지키는 것이다.

103.8 조용한 실패 금지 — 그리고 커밋은 결정의 기록이다#

마지막으로 이 장 전체를 관통하는 규율 하나가 있다. 조용히 실패하지 않는다.

잘랐으면 잘랐다고 말한다. 못 쟀으면 못 쟀다고 말한다. 검사를 건너뛰었으면 건너뛰었다고 말한다. 침묵은 언제나 「검사했고 괜찮다」 로 읽히기 때문이다.

이렇게 하면읽는 사람은 이렇게 읽는다
들어가지 않는 값을 잘라 넣는다「값이 들어갔다」
실패한 검사를 목록에서 빼 둔다「그 검사는 통과했다」
「대개는 됩니다」를 적지 않는다「언제나 됩니다」
못 재는 것을 0 으로 적는다「재어 보니 0 이었다」

표 103.8 — 조용한 실패의 얼굴들

★ 마지막 줄이 특히 위험하다. 「0 이다」와 「검사가 0 이라고 답했다」는 다른 문장이다. 검사의 범위가 곧 그 검사의 약속이므로, 무엇을 안 보는지도 함께 적어야 한다.

그리고 커밋 메시지다. 코드는 무엇을 했는지 말하지만 왜 는 말하지 못한다 (102장의 설계 기록과 같은 이야기다). 그래서 커밋에는 이것들을 적는다.

  1. 무엇을 왜 고쳤는가 — 특히 되돌린 것이 있으면 그것도
  2. 어떤 수가 어떻게 움직였는가(래칫이 있다면)
  3. 배운 것 한 줄 — 다음 사람이 같은 자리를 다시 짚지 않도록

복습 정리

기억할 것요지
어휘같은 것을 같은 이름으로 불러야 대화가 짧아진다
픽스처시험이 딛고 서는 준비물. 매번 같아야 한다
증인고친 자리마다 하나. 「이 결함이 돌아오면 운다」
순서★ 증인은 고치기 전에 세운다 — 무는지 알 수 있는 유일한 순간
음성 검사새 검사는 반드시 한 번 고장 내 본다. 초록인 것과 보고 있는 것은 다르다
래칫늘면 막고, 줄이는 것은 사람이 조인다
티어바뀐 것이 깊이를 정한다. 올릴 수만 있다
결정과 실행결정은 묻고 실행은 한다. 섞으면 양쪽이 나빠진다
스필범위 밖의 발견은 적어 두고 넘긴다
조용한 실패침묵은 「검사했고 괜찮다」로 읽힌다

표 103.9 — 일이 굴러가는 방식 — 기억할 것

여기까지가 여럿이 오래 짓는 일의 뼈대다. 다음 장은 이 모든 것을 한 영역에 대고 본다 — C 가 가장 깊이 뿌리내린 자리인 임베디드에서, 어떤 컴파일러와 어떤 연장을 들고 일하는지다.