“활성 고객”, “날짜”, “상사”, “기준값”처럼 같은 단어가 두 가지 정의를 감추고 있는 경우가 많다. 두 보고서가 맞지 않을 때는 거의 항상 이것이 원인이다.
감지 질문
두 가지 중 하나에만 참인 규칙이 있는가? 그리고 한 계산에서 두 수치를 합치거나 나눌 때, 같은 집합, 같은 척도, 같은 기간에서 나온 것인가?
경고 신호
“날짜는 같아요” · “뷰에서 이미 걸러져 있어요” · “둘 다 페소예요.”
예시와 해결 방법
예시
“날짜”는 이벤트 날짜일 수도, 적재 날짜일 수도, 파티션 날짜일 수도 있다. 또는 “상사”: 지금의 상사일 수도, 측정하려는 일이 일어났을 때의 상사일 수도 있다.
또 다른 예시
한 캠페인 테이블에는 회사 전체의 광고비가 들어 있었지만, 매출은 구매 100건 중 1건만 뽑은 표본에서 가져왔다. 두 열 모두 ‘페소’라고 적혀 있었고, 그 결과 모든 캠페인의 수익률이 음수로 나왔다.
해야 할 일
코드와 화면에서 변형마다 다른 이름을 붙인다. 이름이 어색해도 괜찮다. 그리고 서로 다른 출처의 두 수치를 나누거나 빼기 전에, 각 수치가 어디에서 왔는지 적어 둔다. 결과가 모든 경우에 똑같이 나온다면(모두 손해, 모두 이익), 사업 문제보다 척도 문제를 먼저 의심한다.
다른 것들이 이름으로 읽는 데이터를 바꿀 때는, 그것을 읽는 사람들의 목록을 적어 두어야 한다. 기억은 항상 하나를 빠뜨린다.
감지 질문
이것을 사용하는 사람들의 목록을 어디서 얻었는가? 검색에서인가, 기억에서인가?
경고 신호
“그것이 쓰이는 세 곳은 이미 다 처리했어요” · “그 사람들은 이미 봤어요” · “필요한 건 다 추가했어요.”
예시와 해결 방법
예시
모델이 바뀐 뒤 세 화면의 라벨을 갱신했다. 아무도 떠올리지 못한 네 번째 화면은 여전히 이전 모델의 선택지를 보여 주고 있었다.
또 다른 예시
한 장을 쓰면서, 독자가 앞 장에서 이미 세 함수를 안다고 기억에 의존해 썼다. 검색해 보니 그 장은 하나만 가르쳤고, 새 것처럼 소개한 주제는 다른 장에서 이미 설명되어 있었다.
예시 하나 더
변경을 저장할 때 쓴 파일 목록을 기억에 의존해 적었는데, 새 기능을 작동하게 하는 파일 하나가 빠져 있었다. 나중에 검토한 보류 변경 목록이 그것을 보여 주었다.
해야 할 일
변경 유형별로 영향 범위를 적은 지도를 유지한다. 그것을 빠짐없이 훑으면서 “변경함” 또는 “~이므로 해당 없음”이라고 말한다. 변경으로 새 소비자가 생긴다면, 같은 변경에 추가한다. 그리고 출처를 인용하기 전에, 그 출처에서 주장하는 문장을 그대로 옮겨 적는다. 누군가 다른 사람이 무엇을 했다거나 보았다고 말하는 것도 확인해야 한다.
내 코드 바깥의 무엇과 이 결과를 대조할 것인가? 그리고 글이 시스템의 동작이라고 말하는 모든 것, 즉 예시든, 규칙이든, 옵션 설명이든, 실제 엔진에서 실행해 보았는가?
경고 신호
“내 컴퓨터에서는 돌아가요” · “오류가 안 났어요” · “원래 이렇게 동작해요, 그냥 설명하는 거예요.”
예시와 해결 방법
예시
객관식 문제에서 숫자를 따옴표가 붙은 텍스트와 비교하면 오류가 난다고 주장했다. 일부 다른 엔진에서는 실제로 그렇지만, 사람들이 실제로 쓰던 엔진에서는 쿼리가 실행되어 행을 반환했다.
또 다른 예시
한 글은 이름 붙은 블록이 자기 쿼리 안에서만 살아 있다고 올바르게 설명했다. 그런데 그 글의 예시는 같은 블록을 세 개의 쿼리가 공유한다고 가정하고 있었다. 실행해 보니 두 번째 쿼리가 오류를 냈다. 테이블이 존재하지 않았다.
예시 하나 더
한 페이지의 모든 예시를 실행했는데도 문장 세 개가 여전히 거짓이었다. 맨 끝에 쓴 옵션 설명은 아무도 실행해 보지 않은 줄의 동작을 설명하고 있었다. 오류를 읽는 규칙(“이런 메시지가 나오면 저것이 빠진 것”)은 같은 페이지의 다른 단계와 모순되었다. 그리고 소개글은 어떤 함수가 “결과를 돌려준다”고 했지만, 실제로는 돌려주지 않았다.
해야 할 일
알려진 수치, 원천 시스템, 이전 기간과 대조한다. 실제 환경에서 테스트한다. 그 환경의 권한, 라이브러리, 실제로 돌아가는 코드 사본을 그대로 쓴다. 시스템 동작에 대한 모든 주장은 적기 전에 한 번은 실행해 본다. 예시 속에 지나가듯 나오는 주장이나 맨 마지막에 쓴 주장도 마찬가지다. 주장이 “만”, “항상”, “절대” 같은 일반 규칙이라면, 그것을 깨뜨릴 사례를 찾아서 그것도 실행한다. “정수로 변환하면 정수만 읽는다”는 주장은 소수점이 있는 텍스트로 확인했더니, 소수가 있는 숫자에 대해서는 거짓이었다. 분수로 말할 때(“여섯 중 다섯”)는 전체가 무엇인지 밝힌다. 가상의 시나리오도 확인한다. 표본 크기, 차이, p값이 서로 일관되어야 한다.
항상 통과하는 테스트는 고장 났을 수 있다. 그것을 알 수 있는 유일한 방법은 실제 오류에 대해 실패하는 모습을 직접 보는 것이다.
감지 질문
내 검사기를 실제로 나타날 형태의 오류에 대해 테스트했는가? 그리고 올바른 경우에 대해서도 테스트했는가?
경고 신호
“이미 통과하니까 제대로 설정된 거예요.”
예시와 해결 방법
예시
특정 단어를 금지하는 테스트가 소문자 버전만 찾고 있었다. 대문자나 악센트가 붙으면 그냥 통과했다.
또 다른 예시
연습을 위해 중복 레코드 37개를 심었는데, 개수가 92로 나왔다. 데이터에 이미 자연스럽게 일치하는 항목이 있었기 때문이다. 저장하기 전에 심은 개수와 비교하는 검사가 이것을 잡아냈다.
예시 하나 더
들여쓰기된 코드를 감지하는 규칙이 텍스트로 시작하는 줄을 플래그했다. 통과해야 하는 경우가 그 모양이 아니었던 것이다. 그리고 텍스트 비교가 작동한다는 것을 확인하는 테스트는, 비교가 고장 났는데도 통과했다. 다른 검토가 오류를 먼저 거부했기 때문이다.
해야 할 일
두 가지 사례로 보정한다. 하나는 반드시 무언가를 찾아야 하는 사례, 다른 하나는 아무것도 찾으면 안 되는 사례다. 태어난 날 실제로 한 번 돌려 본다. 올바른 사례도 나쁜 사례만큼 중요하다. 올바른 것을 거부하는 검사기도 고장 난 것이며, 엄격해 보이기 때문에 알아채기 더 어렵다. 올바른 사례에는 내가 직접 쓰지 않은 것을 포함해, 올바른 것의 실제 형태가 들어가야 한다. 다른 순서, 다른 이름 같은 것들. 각 검사는 스스로 보정되어야 한다. 다른 검사가 오류를 먼저 잡았다면, 내 검사는 한 번도 시험받지 않은 것이다. 그리고 이미 읽은 것을 다시 읽기만 하는 검토(“두 번째 실행”이 같은 파일을 다시 읽는 것)는 실패할 수 없다. 무언가 잘못되었다면 실패할 수 있는 대조로 바꾼다.
모든 자동 검토는 “관련 있는 것”만 거르는데, 그 필터 때문에 문제가 있는 바로 그 자리가 보이지 않을 수 있다.
감지 질문
내 필터는 무엇을 빠뜨리는가? 결과를 믿기 전에 적어 둔다.
경고 신호
“차이가 48개 나왔어요, 목록은 이미 있어요” · “테스트가 전부 돌아요.”
예시와 해결 방법
예시
디자인 감사 도구가 자식 요소가 없는 요소만 검토했다. 검토가 필요한 라벨 안에는 도움말 아이콘이 들어 있었기 때문에, 도구는 그 라벨을 한 번도 보지 못했다.
또 다른 예시
테스트가 모든 페이지를 돌며 항상 첫 번째 선택지를 표시하고, 쿼리를 하나씩 실행했다. 늘 통과했지만, 정답이 실제로 받아들여지는지는 한 번도 확인하지 않았다.
예시 하나 더
글이 바뀌었는지 판단하려고 공백을 무시했다. 그 결과 바로 그 글, 즉 공백과 줄바꿈으로 텍스트를 배치하는 방법을 설명하는 페이지가 빠져 버렸다. 정규화도 필터링이다.
해야 할 일
필터가 빠뜨리는 것을 적고, 제외된 항목의 표본을 직접 확인한다. 그리고 필터 때문에 눈이 멀었다는 것을 알게 되면, 이미 만들어 둔 비슷한 다른 필터도 찾아본다. 코드의 공백을 전부 지우는 검토는 두 단어를 하나로 붙여 올바른 코드를 거부했다. 같은 교훈을 다른 정규화에서 얻은 직후였는데도 말이다.
아무것도 깨뜨리지 않으면서 반복되는 변경은, 한 번이 아니라 두 번 실행해서 확인해야 한다.
감지 질문
이미 적용된 것이 있는 대상에 내 변경을 실행해 보았는가? 나 다음에 이 같은 텍스트를 건드리는 다른 프로세스는 무엇인가?
경고 신호
“잘 돌았으니 끝났어요.”
예시와 해결 방법
예시
스크립트가 두 번째 실행에서 아무것도 바꾸지 않는지 확인하려고, 같은 실행의 체크섬을 두 번 비교했다. 그다음에는 서로 다른 파일 목록에서 얻은 두 체크섬을 비교했다. 두 비교 모두 “같다” 또는 “다르다”고 말했지만 아무것도 증명하지 못했다. 제대로 된 비교는 같은 파일로, 한 명령 안에서 체크섬을 구하고, 스크립트를 실행하고, 다시 체크섬을 구하는 것이다.
해야 할 일
같은 출발점에서 두 번 실행하고 파일을 비교한다. 두 번째 실행은 아무것도 바꾸지 않아야 한다.
숫자가 바뀌어야 할 때 바뀌지 않았거나, 여러분이 보고 있는 순간에 바뀌었다면, 다음 순서로 의심한다. 캐시, 같은 테이블에 쓰는 다른 프로세스, 여러분이 들여다보고 있는 도구, 그리고 맨 마지막에야 시스템 자체.
감지 질문
나는 시스템을 보고 있는가, 아니면 캐시, 복사본, 또는 오래된 스냅샷을 지니거나 중간에 바꿔 버린 도구를 보고 있는가?
경고 신호
“이미 고쳤는데 여전히 똑같아요” · “이번엔 정말 제대로 입력했어요.”
예시와 해결 방법
예시
한 표의 열이 절반으로 줄어든 것처럼 “보였다”. 몇 분 뒤에는 완전했다. 다른 프로세스가 표를 다시 만들고 있는 중에 여러분이 보고 있었던 것이다.
또 다른 예시
페이지에 새 이미지가 “로드되지 않았다”. 파일은 멀쩡했다. 테스트 서버가 시작된 뒤에 만들어진 폴더를 보지 못했던 것이다.
예시 하나 더
스크립트가 텍스트 패턴을 “잘못 계산했다”. 스크립트는 멀쩡했다. 코드를 넘겨주는 콘솔이 스크립트가 읽기 전에 모든 \n을 실제 줄바꿈으로 바꿔 버린 것이다. 그리고 셀들이 메모리를 공유하는 노트북에서는, 나타났어야 할 오류가 나타나지 않았다. 변수가 앞선 셀에서 저장된 채로 남아 있었기 때문이다.
해야 할 일
재난을 선언하기 전에 몇 분 뒤 두 번째 스냅샷을 찍는다. 도구가 이미 같은 방식으로 여러분을 실망시켰다면, 계속 들여다보지 말고 도구를 바꾼다. 같은 실수가 반복되는 것은 운이 나쁜 게 아니라 잘못된 도구를 쓰고 있다는 뜻이다. 중간에 \n이나 \\을 바꾸는 콘솔은 코드를 넘기기에 적합하지 않다. 코드는 파일에 담고, 그 내용을 건드리지 않는 도구로 쓴 뒤, 그 파일에서 실행한다. 그리고 기계적인 것(이미 세 번 실패한 명령)은 기억에 의존하지 말고 가드로 막는다.
각 페이지를 쓴 사람이 검토용 스크린샷도 찍었는데, 위험하다고 생각한 단계만 찍었다. 검토자는 원본 파일을 읽었다. 스크린샷이 없는 단계는 아무도 보지 않았다.
해야 할 일
한 사람이 실행하고, 다른 사람이 결과를 읽어서 확인한다. “내가 이미 했다”는 검증이 아니라, 이제 검증할 수 있게 되었다는 신호다. 그리고 검증하는 사람은 실행한 사람과 같은 경로로 보지 않는다(교훈 14 참고). 둘 다 원본만 읽는다면, 둘 다 화면은 보지 못한다.
공식 문서는 모든 내용을 같은 권위로 말한다. 담당자, 일정, 응답 시간처럼 지어낸 세부사항도 진짜 정보와 똑같이 읽힌다.
감지 질문
이 세부사항을 정말 알고 있는가, 아니면 그럴듯해 보여서 가정하고 있는가?
경고 신호
“당연히 그럴 거예요.”
예시와 해결 방법
예시
어떤 절차에 아무도 합의한 적 없는 24시간 응답 시간이 적혀 있었다. 그럴듯해 보여서 적었는데, 일단 공개되자 약속이 되어 버렸다.
해야 할 일
빠진 내용은 눈에 띄게 표시한다. 예를 들어 빨간색 ‘확인 대기’처럼. 그리고 아는 사람에게 묻는다. 틀린 표현보다 중립적인 표현이 낫다.
혼동하지 말 것
이 표시는 결정하는 사람을 위한 것이지, 최종 독자를 위한 것이 아니다. 공개된 글 안의 ‘확인 대기 중’은 그곳에 있으면 안 되는 또 다른 데이터다. 확인되기 전에 문서를 내보내야 한다면, 빠진 부분을 주장하지 않는 중립적인 표현을 쓰고, 대기 항목은 보고서나 프로젝트 기록에 남긴다.
텍스트에서 단어를 찾았다고 해서 그것이 화면에 나온다는 뜻은 아니다. 텍스트는 문서의 다른 곳에 있는데, 그것을 보여 주어야 할 형식은 비어서 나올 수 있다.
감지 질문
독자가 보는 그대로 그 요소를 봤는가, 아니면 원본에서 단어(또는 코드)만 찾았는가?
경고 신호
“단어는 파일에 있어요” · “한 줄 고치는 거예요.”
예시와 해결 방법
예시
번호가 붙은 세 단계가 빈 글머리 기호로 나왔다. 그 단어들이 문서의 다른 곳에 있었기 때문에 자동 검사는 통과시켰다.
또 다른 예시
“오른쪽에 열이 더 있습니다”라는 안내를 추가하고 코드가 있으니 완료로 처리했다. 그런데 한 번도 나타나지 않았다. 일반 표 스타일이 그것을 켜는 조건을 지워 버렸기 때문이다. 스크린샷을 보고서야 다른 사람이 알아챘다.
예시 하나 더
답 선택지에 파일상으로는 “합계: {2 * 78}”이라고 적혀 있었다. 페이지가 그려질 때 시스템은 중괄호 안을 계산해 “합계: 156”을 보여 주었다. 두 선택지가 똑같아진 것이다. 화면을 본 사람만 이것을 알아챘다.
변형
리뷰어가 판단하는 스크린샷이 아무도 도달하지 않는 화면을 보여 줄 수 있다. 한 강좌의 캡처 도구는 코드 연습마다 넘어가기만 하려고 채운 한 줄을 답으로 넣었다. 그래서 “이후” 스크린샷에는 그 채움 줄과 “예상 결과가 아닙니다”라는 메시지가 보였다. 리뷰어는 정답을 맞힌 뒤 학습자가 보는 화면을 한 번도 보지 못했다. 데스크톱에서는 캡처에 헤더와 측면 메뉴까지 담겨서, 수업이 읽을 수 없는 띠처럼 나왔다. 리뷰용으로 캡처할 때는 각 단계에 성공한 사용자가 하듯 답하고, 검토 대상 부분만 캡처한다.
해야 할 일
“이 세 단계가 글머리 기호로 나오는지”처럼 정확한 요소를 확인하고, 독자가 볼 페이지를 적어도 한 번은 본다. 작은 수정도 확인한다. 다른 규칙이 그것을 덮어쓸 가능성이 가장 낮다고 예상되는 곳이 바로 그 자리이기 때문이다. 그리고 페이지에 새로운 요소(코드 블록, 고정 헤더)를 처음 쓸 때는 모바일에서도 본다.
결과의 마지막 줄만 보거나 다른 명령으로 파이프를 거치면, 정확히 “실패했다”고 말한 부분이 버려질 수 있다. 실패한 테스트의 잘린 출력은 통과한 테스트의 잘린 출력과 똑같아 보인다.
감지 질문
나는 판정(몇 개가 통과했는지, 몇 개가 실패했는지, 어떻게 끝났는지)을 읽고 있는가, 아니면 출력의 일부만 읽고 있는가?
경고 신호
“화면에 오류가 안 떴어요” · “마지막 몇 줄이면 충분해요.”
예시와 해결 방법
예시
한 페이지가 테스트가 “오류 없이 끝났다”는 이유로 검토 완료 처리되었다. 결과가 끝부분만 보여 주는 명령을 거쳤고, 테스트가 실패했다는 알림은 그 과정에서 사라졌다.
또 다른 예시
테스트 스위트가 실패한 테스트를 자동으로 다시 실행했다. 두 번째에 통과한 테스트는 실패로 세지 않고 “불안정”으로만 표시했고, 원인은 숨겨졌다. 단계를 바꿀 때마다 서버에 페이지를 다시 요청했기 때문이다. 재시도가 없었다면 실패가 드러났고 측정할 수 있었을 것이다.
해야 할 일
상태를 바꾸기 전에 판정 줄을 찾는다. 명령을 연결한다면, 첫 번째 명령의 실패가 끝까지 전달되는지 확인한다. 출력에 판정이 없다면 그것은 “통과했다”가 아니라 “모른다”는 뜻이다(교훈 1 참고). 그리고 전체 출력을 파일에 저장하고 판정을 거기서 찾는다. 그러면 미리 얼마나 읽을지 정해 둘 필요가 없다.
모든 페이지에서 하는 일, 예를 들어 모든 콘텐츠를 읽거나 색인을 다시 계산하는 일은, 페이지가 열 개일 때는 비용이 적지만 백 개일 때는 많이 든다. 서서히 커지기 때문에, “가끔 실패한다”는 상황이 오기 전까지 아무도 알아채지 못한다.
감지 질문
모든 요청마다 실행되는 이 코드는 모든 콘텐츠를 훑는가? 지금은 얼마나 걸리는가, 두 배면 얼마나 걸리는가, 많은 요청이 동시에 오면 얼마나 걸리는가?
경고 신호
“로컬에서는 빨리 로드돼요” · “간헐적인 실패예요.”
예시와 해결 방법
예시
사이트의 모든 페이지가 목차를 만들기 위해 모든 콘텐츠를 읽고 파싱했다. 페이지 하나만 보면 0.4초였다. 하지만 사이드바가 한 번에 약 40개를 요청했고, 합치면 16초가 걸렸다. 몇 달 뒤 페이지를 전환할 때 테스트가 실패하기 시작했고, 그 실패는 테스트 자체의 문제처럼 보였다.
해야 할 일
사람들이 쓰는 방식 그대로, 컴파일된 서버에서 모든 요청을 함께 넣어 측정한다. 페이지 하나만 재지 않는다. 서버가 도는 동안 바뀌지 않는 것은 캐시하고, 실제로 느리게 만든 그 경우로 측정하는 테스트를 남긴다.
여러 사람이 동시에 일하면 각자 자기 부분은 분명히 본다. 하지만 공유하는 것(테스트, 데이터, 프로젝트 전체의 문서)에서는 변경들이 합쳐지면서 충돌한다. 이름이 겹치고, 사본이 뒤처지고, 다른 사람이 아직 끝내지 못한 작업을 테스트가 읽는다. 통합은 붙여 넣기가 아니라 일이다.
감지 질문
두 부분이 동시에 건드리는 파일은 무엇이며, 누가 통합하는가? 내 테스트는 다른 사람이 아직 끝내지 못한 작업을 읽고 있는가?
경고 신호
“우리는 각자 자기 것을 테스트했어요” · “실패는 다른 부분에 있어요.”
예시와 해결 방법
예시
네 사람이 동시에 각자 한 부분을 쓰면서, 공유 부분을 결과물로 남겼다. 통합하자 두 결과물이 공유 테스트에 이미 있는 이름을 썼고, 테스트가 돌지 않게 되었다. 다른 테스트는 확인 대상의 사본을 보관하고 있었는데, 그 부분은 바뀌었지만 사본은 바뀌지 않았다. 한 테스트 실행은 다른 사람이 아직 그리고 있던 이미지 때문에 실패했다. 결국 각 부분은 따로 저장할 수 없었다. 공유 파일에 이미 모든 사람의 변경이 들어 있었기 때문이다.
해야 할 일
공유 부분은 한 사람이 통합하고, 이름이 충돌하면 멈추는 단계를 둔다. 테스트는 사본이 아니라 원본을 읽게 한다. 모두 끝난 뒤에 전체 테스트 스위트를 한 번 돌리거나, 아직 바뀌고 있는 부분을 뺀 채로 돌리고, 어떤 부분을 뺐는지 말한다. 그리고 파일을 공유하는 부분은 함께 저장해서, 저장된 각 버전이 전체로 작동하게 한다.
파일이 스크립트나 다른 조각들로 조립되는 경우, 그 파일은 원본이 아니라 결과물이다. 파일을 직접 고치면, 다음에 생성될 때 아무 경고 없이 사라진다.
감지 질문
이 파일을 다른 무엇(스크립트, 템플릿, 조각 조립)이 쓰는가? 다시 생성하면 내 변경이 남아 있는가?
경고 신호
“파일에서 직접 고쳤어요” · “방금 다시 생성했어요.”
예시와 해결 방법
예시
각자 제출한 섹션으로 메모를 조립했다. 세 행을 메모에서 직접 고쳤는데, 제출물에서 다시 만들자 옛 세 행이 돌아왔다. 다른 작업에서는 검토자가 템플릿에서 만들어진 페이지의 대체 텍스트를 손으로 고쳤다. 작성자가 페이지를 다시 생성하자 수정이 사라졌다.
해야 할 일
파일을 고치기 전에 그것이 어디서 오는지 확인한다. 원본(템플릿, 스크립트, 제출물)을 고치고 다시 생성한다. 결과물을 고쳐야 한다면, 같은 단계에서 원본도 함께 고친다. 생성된 결과물을 검토하는 사람은 무엇이 바뀌었는지 말해서, 원본으로 옮겨질 수 있게 한다. 그리고 원본에서 파일을 다시 만들기 전에, 나올 결과와 비교한다. 차이가 나는 부분은 모두 사라질 수정이다.
예시가 설명을 확인해 주려면 경쟁하는 설명이 다른 결과를 냈어야 한다. 두 설명이 똑같이 예측한다면, 예시는 먼저 떠오른 쪽을 믿게 만들 뿐이다.
감지 질문
같은 결과를 낼 수 있는 다른 설명은 무엇인가? 내 예시가 그것을 배제하는가?
경고 신호
“숫자가 그것을 확인해 줘요.”
예시와 해결 방법
예시
한 수업은 퍼널의 첫 단계를 5포인트 올린 것과 마지막 단계를 올린 것을 비교했다. 첫 단계가 이겼고, 본문에는 “방문이 더 많은 곳에서 더 가치가 있다”고 적혀 있었다. 그런데 첫 단계는 전환율이 가장 낮은 단계이기도 했고, 그것이 실제 원인이었다. 첫 단계의 전환율이 더 좋은 퍼널에서는 방문이 더 적어도 마지막 단계가 이긴다. 다른 프로젝트에서는 한 페이지가 2.7초 걸려 로드된다는 이유로 탓을 들었다. 페이지를 하나씩 요청하는 테스트는 수정 없이도 통과했다. 진짜 원인은 요청 40개가 동시에 나가는 것이었다.
해야 할 일
무엇인가를 가르치거나 보고하기 전에, 경쟁하는 설명을 적고 둘이 다르게 예측하는 사례를 만든다. 같은 크기의 부분끼리 비교하고, 예시의 크기를 바꿔 보고, 일반적인 주장은 글로 쓰기 전에 시뮬레이션 데이터로 측정한다. 둘을 가려낼 수 없다면, 그 예시는 두 설명 모두와 양립한다고 적는다.
이미 아는 사람은 무의식적으로 하는 단계를 보지 못한다. 그래서 그 사람의 첫 예시에는 새로운 것이 한꺼번에 여러 개 등장하고, 초보자는 첫 줄에서 길을 잃는다.
감지 질문
내 첫 예시에서, 아무것도 모르는 사람에게 새로운 것은 몇 개인가? 내가 요구하는 모든 기호, 단어, 버튼 이름, 동작이 이미 가르쳐졌는가?
경고 신호
“누구나 알잖아요” · “한 줄일 뿐이에요” · “흔한 단어예요.”
예시와 해결 방법
예시
초보자를 위한 프로그래밍 강좌는 한 셀에 새로운 것이 여섯 개나 한꺼번에 나오는 것으로 시작했다. 강의 계획은 자료형이 무엇인지, 그리고 =가 오른쪽에서 왼쪽으로 저장하고 읽는다는 것을 가르치지 않았다. 강좌 운영자는 한 문장으로 말했다. “학생들에 대해 많은 것을 가정하고 있어요.” 이후 세 강의는 “수식을 끌어다 놓듯이”라고 했지만, 끌기를 가르친 강의는 없었다. 그전까지는 “수식을 아래로 복사하기”만 가르쳤다.
해야 할 일
쓰기 전에 예시가 당연하다고 여기는 것(기호, 단어, 클릭, 학습자가 이미 만들었어야 할 것)을 목록으로 적고, 그것을 먼저 가르친다. 한 번에 새로운 개념 하나씩, 기호는 처음 나올 때마다 설명한다. 버튼 이름과 맞는 모든 단어, 그리고 요구하는 모든 동작을 기호를 찾을 때처럼 검색한다. 가능하다면 자동 검사로 각 예시가 쓰는 것과 이미 가르친 것을 비교하게 한다.
몇 달마다 바뀌는 도구에 관한 수업이나 가이드는, 저자가 기억하는 것을 바탕으로 쓰면 쓰는 도중에 이미 낡아 버린다. 첫 독자가 읽기도 전에 전제가 틀렸을 수 있다.
감지 질문
이 글의 전제 중 어떤 것이 도구가 오늘 할 수 있는 일에 의존하는가? 그것을 마지막으로 언제, 어떤 출처로 확인했는가?
경고 신호
“원래 이렇게 동작해요” · “다들 이렇게 해요.”
예시와 해결 방법
예시
“AI로 하는 분석” 모듈의 계획은 “AI는 당신의 데이터를 볼 수 없으니, 표를 붙여 넣는 대신 표를 설명하라”로 시작했다. 강좌 운영자는 이제 에이전트가 데이터베이스에 직접 연결되어 질의를 스스로 실행하고 숫자를 돌려준다고 지적했다. 출처와 날짜가 있는 조사로 확인해 보니, 2년 전에는 채팅창에서 코드를 복사하는 방식이었다. 여러 수업이 여전히 유효한 것을 중심으로 다시 쓰였다. 질문을 정의하기, 에이전트를 감독하기, 숫자를 확인하기.
해야 할 일
빠르게 바뀌는 도구에 대해 쓰기 전에, 출처와 날짜가 있는 현재의 실제 사용법을 찾아본다. 도구가 지금 하는 일, 무엇이 무료인지, 사람들이 실제로 무엇을 쓰는지 확인한다. 다음 버전에도 살아남는 기술(틀 잡기, 감독하기, 검증하기)을 중심으로 글을 짜고, 도구별 단계는 한 곳에 모아 쉽게 갱신할 수 있게 한다. 확인하지 못한 것은 표시하고, 발행하기 전에 직접 해 본다.
소스 파일을 셸의 문자열 도구로 다시 쓰면(읽고, 바꾸고, 다시 쓰기) 코드는 다른 용도로 만들어진 규칙을 거치게 된다. 기본 인코딩, 연산자 우선순위, 따옴표와 이스케이프 규칙이 그것이다. 명령만 보면 변경이 맞아 보이는데, 파일은 명령이 하려는 것처럼 보였던 내용과 다르게 나온다.
감지 질문
이 코드 변경은 편집기를 거치고 있는가, 아니면 셸의 읽기, 바꾸기, 쓰기를 거치면서 그 인코딩, 우선순위, 따옴표 규칙까지 함께 적용받고 있는가?
경고 신호
“그냥 간단히 바꾸는 거라 명령 하나로 끝내면 돼요” · “여러 군데를 한꺼번에 바꾸는 게 더 빨라요”.
예시와 해결 방법
예시
한 세션에서 편집이 두 번 있었고, 둘 다 Windows PowerShell 5.1에서였다. 스크립트를 읽고, 바꾸고, 다시 쓰는 작업(Get-Content, -replace, Set-Content -Encoding utf8)은 파일을 시스템의 ANSI 코드 페이지로 읽은 뒤 UTF-8로 다시 썼다. 그 결과 파일 안의 ✓와 · 는 모두 ✓와 ·로 바뀌었고, 스크립트의 자체 점검도 깨진 기호를 출력했다. 이어서 교체할 내용의 목록을 `@('old', 'new' + "n" + 'more')`로 쓴 작업에서는 줄이 빠졌다. PowerShell에서는 쉼표가 +`보다 강하게 묶이므로, 각 쌍이 네 개의 항목이 되었고 새 텍스트 중 첫 조각만 쓰였다.
해야 할 일
코드는 편집기 자체의 편집 도구와 쓰기 도구로 바꾼다. 변경을 스크립트로 해야 한다면, 스크립트를 파일에 쓰고, UTF-8을 읽고 쓰며 문자열 리터럴이 단순한 인터프리터로 실행한다(node fix.mjs, python -I fix.py). 그다음 diff를 본다. 의도한 것보다 많은 줄이 바뀌었거나 건드리지 않은 문자가 바뀌었다면, 그것은 셸이 한 짓이다.
연습문제는 그 기술을 건너뛰는 사람이 다른 결과를 얻을 때에만 그 기술을 확인한다. 시작점, 예시, 지름길이 이미 올바른 답을 만들어 낸다면, 그 연습문제는 모두를 통과시킨다. 연습문제에 적용한 교훈 13이다. 검사는 실수를 거부하는 것을 한 번이라도 본 뒤에야 가치가 있다.
감지 질문
내가 가르치는 것을 정확히 건너뛰거나, 연습문제를 건드리지 않고 제출하는 사람은 어떤 결과를 얻는가? 그것이 올바른 결과와 다른가? 두 계산이 우연히 같아지는 경우(첫 그룹, 첫 단계, 전체 합계)를 골라 버리지 않았는가?
경고 신호
“맞는 답이 나오니까 작동하는 거죠.”
예시와 해결 방법
예시
한 연습문제는 날짜 필터를 써서 여름에 나온 가장 큰 구매를 찾으라고 했다. 그런데 전체 표에서 가장 큰 구매도 여름에 있었다. 날짜 필터 없이도 같은 숫자가 나왔고, 연습문제는 그것을 받아 주었다. 다른 연습문제는 코드에 =가 들어 있어야 한다고 요구했다. 결과를 손으로 total = 1945라고 적어도 통과했다. 검사는 기술이 아니라 글자만 보았다.
해야 할 일
모든 연습문제를 세 번 풀어 본다. 손대지 않은 채로, 가장 가능성 높은 실수를 넣고, 올바르게. 세 번째만 통과해야 한다. 그다음 같은 지름길 목록을 모든 연습문제에 적용한다. 손으로 입력한 숫자, 끝에 답을 덧붙인 시작 코드, 원인이 아니라 증상을 손으로 고친 것, 빈 필수 단어를 쓴 경우, 앞선 시도에서 남은 메모리를 가지고 실행한 경우. 허용 오차는 정답과 가장 흔한 실수 사이의 거리보다 작게 유지하고, 학습자가 쓸 법한 올바른 형태 세 가지를 적어서 옳은 답을 거부하지 않게 한다.
질문은 답이 이미 눈에 보이는 순간 측정을 멈춘다. 그림, 핵심 아이디어, 다른 연습문제의 설명, 단계 제목, 앞선 수업 등 어디에서든 그렇다. 정답을 맞힌 사람은 방금 그것을 읽었을 수도 있다.
감지 질문
학습자가 답하기 전에 이 그림, 이미지, 문구를 보는가, 여기서든 앞선 수업에서든? 정확한 숫자만이 아니라 근사치(“약 25”), 캡션에 적힌 결론, 제목, “방금 보았듯이…”, 또는 앞선 연습문제에서 가장 흔한 오답이 화면에 무엇을 보여 주는지도 포함한다.
경고 신호
“그냥 맥락일 뿐이에요” · “제목일 뿐이에요” · “캡션이에요.”
예시와 해결 방법
예시
올바른 수치가 담긴 삽화가 그것을 묻는 연습문제 바로 위에 있었다. 한 수업은 변수에 새 값을 저장하면 무슨 일이 일어나는지 묻는데, 그 단계의 제목은 ‘다시 저장하면 대체된다’였다. 마지막 퀴즈 아래에 있던 요약을 학습자가 답하기 전에 읽어 버렸다. 첫 시도 뒤에 나타나는 피드백 상자는, 실패한 시도라 해도, 답의 방향을 보여 주었다.
해야 할 일
질문 앞에 보이는 모든 것에서 각 답을 찾아본다. 첫 실패한 시도 뒤에 나타나는 내용, 시작 코드를 실행했을 때 출력되는 것, 내려받을 수 있는 파일, 그림에서 셀 수 있는 것까지 포함한다. 단계 제목과 학습 목표는 마지막에 쓰고, 각 예측의 결과 옆에 두고 읽는다. 자동 검사는 정확한 수치를 찾지만, 근사치와 바꿔 쓴 표현은 답을 염두에 두고 읽는 사람만 찾는다.
문제의 문장으로 볼 때 다른 선택지도 옳다고 방어할 수 있다면, 좋은 이유로 ‘틀린’ 것을 고른 사람은 틀렸다고 채점되고, 정답을 맞힌 사람은 운이 좋았던 것일 수 있다. 그 질문이 측정하는 것은 빠진 문장이지, 개념이 아니다.
감지 질문
문제의 문장과 수업에서 가르친 내용만으로, 다른 선택지를 방어할 수 있는가? 그것을 배제하려면 어떤 사실이 필요하며, 그 사실은 적혀 있는가?
경고 신호
“당연히 그런 뜻이죠” · “분명히 이런 의미예요.”
예시와 해결 방법
예시
한 질문은 “매출에 할인이 없었을 때” 빈 할인액을 어떻게 해야 하는지 물었다. 수업은 사실로서 비어 있는 값은 그대로 비워 둔다고 가르쳤으므로 “그대로 둔다”도 방어할 수 있었다. 예상 답인 0으로 채우는 것은 문제가 말하지 않은 것에 달려 있었다. 목표가 전체 판매의 평균이라는 것이다. 다른 문제에서 “핵심 지표는 두 번째 달에 결제하는 사람의 비율이다”라는 문장은 누구의 비율인지 말하지 않았다. 해석에 따라 두 버전의 결과가 같기도 하고 다르기도 했다.
해야 할 일
틀린 선택지마다, 문제와 수업만 사용해서 왜 틀렸는지 한 줄씩 적는다. 배제하려면 적혀 있지 않은 사실이 필요하다면, 그것을 문제에 추가한다. 계산의 목적, 시스템의 규칙, 비율의 분모, 데이터에 대해 알려진 사실 등이다. 분류 연습에서는 각 항목을 자기 범주만이 아니라 모든 범주에 대해 확인한다.
각 문항은 그 자체로는 괜찮은데도, 전체 세트가 정답을 드러낼 수 있다. 정답이 항상 두 번째에 있거나, 항상 가장 길거나, 항상 중간 길이다. 학습자는 작성자보다 패턴을 빨리 알아챈다.
감지 질문
선택지의 위치와 길이만 읽고 본문은 읽지 않았을 때, 정답을 세 번에 한 번보다 자주 맞히는가?
경고 신호
“문항마다 확인했고, 전부 맞아요.”
예시와 해결 방법
예시
한 모듈의 마지막 퀴즈에서 정답은 일곱 문항 중 여섯 문항에서 두 번째 선택지였다. 강좌 전체에서는 정답이 가장 긴 선택지인 문항이 55 %였다. 선택지가 세 개일 때 우연히 맞힐 확률은 3분의 1인데도 그랬다. 길이를 손으로 맞춘 뒤에는, 한 수업의 일곱 문항 중 여섯 문항에서 정답이 가장 짧은 선택지가 되었고, 연이은 세 수업의 모든 문항에서는 정답이 중간 길이가 되었다.
해야 할 일
선택지를 보여 줄 때 섞는다. 그 순서는 정답이 원래 어디에 적혔는지와 무관해야 한다. 파일마다 가장 긴 것, 가장 짧은 것, 중간 것의 비율을 재고, 작은 파일들은 모듈 단위로 묶어서 몇 문항이 패턴을 가리지 않게 한다. 집계 결과를 원래 목표와 비교해 읽는다. 균형을 맞추는 작업은 단서를 없애는 대신 자리만 옮길 수 있다.
힌트는 틀린 시도 뒤에 나타난다. 힌트가 답을 주거나, 풀이 줄을 알려 주거나, 정답과 가장 흔한 실수를 가르는 순서를 알려 준다면, 두 번째 시도는 기술이 아니라 베끼기를 측정하게 된다.
감지 질문
이 힌트를 읽은 사람이 그 기술 없이도 다음 시도를 통과할 수 있는가? 힌트가 답, 풀이 줄, 연습문제가 측정하는 단계, 또는 맞춰야 할 앞선 숫자를 말하고 있는가?
경고 신호
“그냥 살짝 찔러 준 것일 뿐이에요” · “이미 한 번 해 봤잖아요.”
예시와 해결 방법
예시
자동 검사가 있는 실습 277개를 검토하면서, 힌트 82개를 49개 파일에서 바꿨다. 어떤 힌트는 풀이 줄을 그대로 알려 주었다(“두 번째 것이 …”). 어떤 힌트는 정답을 흔한 실수와 가르는 순서를 정해 주었다. “앞선 결과와 대조하기” 연습에서 힌트는 “2.2 수업에서 얻은 결과와 맞아야 합니다”라고 했다. 앞선 숫자를 기억하는 사람은 그 단계를 거치지 않고도 그 숫자를 입력할 수 있었다.
해야 할 일
힌트는 가장 가능성 높은 실수의 원인에 대해 쓴다(“위치는 1이 아니라 0부터 시작한다”). 이미 가르친 내용만 인용한다. “앞선 숫자와 맞아야 한다”는 답한 뒤 보여 주는 해설에 남긴다. 거기서는 두 방법이 같은 숫자를 낸다는 것이 검증이라는 것을 가르칠 수 있다. 힌트는 따로 검토하지 말고, 연습문제에서 가장 흔한 실수에 비추어 검토한다.
사이트가 레코드를 어떤 방식으로 저장하든, 한 번의 손상된 쓰기가 그 뒤에 오는 것까지 함께 무너뜨릴 수 있다. 레코드를 줄 단위로 덧붙이면, 잘린 줄이 다음 레코드를 그 위에 붙여 버리고, “손상된 줄”을 건너뛰는 읽기 코드는 둘 다 조용히 버린다. 모든 레코드를 한 파일에 담아 읽고, 바꾸고, 통째로 다시 쓰는 방식이라면, 그 파일을 읽을 수 없게 되는 순간 새 레코드는 모두 실패하고, 아무도 지켜보지 않는 콘솔만 이를 알린다. 이것은 교훈 24의 반대쪽이다. 교훈 24에서는 파일이 비워지고, 여기서는 그 뒤에 오는 것이 사라진다.
감지 질문
저장소가 내일 손상되면, 다음 방문자의 레코드에는 무슨 일이 생기고, 그 사실은 누가 알게 되는가?
경고 신호
“손상된 줄은 건너뛰면 돼요” · “그냥 끝에 덧붙이는 거예요” · “쓰기는 원자적이니까 파일이 깨질 수 없어요”.
예시와 해결 방법
예시
첫 번째 웹 시뮬레이션의 두 실행 모두에서, 양식은 각 메시지를 JSON 한 줄로 저장했고 관리자 패널은 읽지 못하는 줄을 건너뛰었다. 파일 끝에서 줄이 잘린 상태에서 다음 메시지가 “Thanks!”라는 답을 받았고, 저장은 되었지만 관리자 패널에는 끝내 나타나지 않았다. 두 버전 중 하나에는 “손상된 줄: 건너뜀”이라는 주석이 있었다.
또 다른 예시
웹 시뮬레이션을 다시 돌렸을 때 두 실행 모두, 모든 메시지는 JSON 파일 하나에 담겼고 임시 파일에 쓴 뒤 이름을 바꾸는 방식이라 쓰는 도중 충돌이 나도 더는 피해가 없었다. 하지만 파일이 다른 방식으로 손상되면(손으로 고친 경우, 디스크가 가득 찬 경우, 잘못 복원된 경우), 새로 방문하는 사람마다 “Internal error”를 받았고, 관리자 페이지에는 아무것도 보이지 않았으며, 유일한 신호는 서버 콘솔의 스택 트레이스뿐이었다.
해야 할 일
새 레코드가 이전 레코드에 기대지 않게 한다. 줄바꿈으로 끝나는 파일에 덧붙이거나(끝나지 않으면 레코드를 줄바꿈으로 시작한다), 레코드마다 자기 파일에 저장한다. 모든 레코드를 담은 한 파일을 통째로 다시 쓰는 방식은 피한다. 읽을 때는 읽지 못한 것의 개수를 세고, 그 숫자를 소유자가 볼 수 있는 곳에 표시한다. 테스트는 저장소를 손으로 손상시킨 뒤(마지막 줄을 짧게 자르고, 파일을 잘라 낸다) 레코드를 하나 보내 확인한다.
인터넷에 열린 양식은 사람만 채우지 않는다. 자동 제출을 막는 장치가 없고, 메시지를 읽는 사람이 그것을 정리할 수 없다면, 첫 번째 쓰레기 물결이 실제 고객을 묻어 버린다.
감지 질문
내일 가짜 메시지가 천 개 도착한다면, 주인은 진짜 메시지 하나를 어떻게 찾고, 나머지는 어떻게 지우는가?
경고 신호
“작은 사이트라 아무도 공격하지 않아요” · “제한은 나중에 추가해요.”
예시와 해결 방법
예시
웹 시뮬레이션의 두 실행 모두, 문의 양식은 스크립트로부터 몇 초 만에 650개의 메시지를 받았고, 모두 “감사합니다”라는 내용이었다. 관리자에게는 페이지도, 삭제 버튼도 없었다. 관리자 페이지는 100만에서 600만 픽셀 높이로 늘어났고, 모바일에서는 메시지 본문이 화면 밖으로 밀려났다. 두 번 모두 빌더는 이것을 보류 항목으로 기록했다.
해야 할 일
처음부터 저렴한 제동 장치를 둔다. 사람은 채우지 않는 숨김 필드, 주소당 분당 제출 횟수 제한. 그리고 최신순으로 정렬된 페이지가 있는 관리자, 삭제하거나 스팸으로 표시하는 방법. 스크립트로 수백 개의 메시지를 보내고, 휴대폰으로 관리자를 열어 테스트한다.
method와 action이 있는 양식은 JavaScript가 실패해도 작동하는 대체 경로를 약속한다. 서버가 JavaScript가 기대하는 응답만 한다면 그 약속은 거짓이 되고, 사람은 코드만 바라보게 된다.
감지 질문
이 JavaScript 파일이 로드되지 않으면, 사람은 제출할 때 무엇을 보는가?
경고 신호
“어차피 모두 JavaScript가 있잖아요” · “혹시 몰라서 action은 남겨 둘게요.”
예시와 해결 방법
예시
웹 시뮬레이션의 두 실행 모두, 양식은 JavaScript로 제출했고 method="post"를 선언했다. JavaScript 없이 제출하자, 한 버전에서는 서버가 JSON 한 줄로 제출을 거부했고 메시지는 사라졌다. 다른 버전에서는 메시지를 저장했지만, 빈 페이지에 {"ok":true}만 보여 주었다. 두 번째 버전의 요약은 JavaScript가 없으면 제출하지 않는다고 말하고 있었다.
해야 할 일
둘 중 하나를 정하고 테스트한다. 서버가 일반 제출을 받아서 페이지로 응답하거나(감사 페이지로 리디렉션), 아니면 없는 대체 동작을 양식이 선언하지 않는다. JavaScript를 끄고 한 번 제출해 본다.
틀린 비밀번호에 매번 즉시 응답하는 로그인은 누구나 분당 수천 번 시도할 수 있게 한다. 강력한 기본 비밀번호는 소유자가 직접 비밀번호를 정하기 전까지 이 문제를 가린다. 그리고 소유자가 정한 비밀번호가 짧아도 이를 막을 장치가 없다.
감지 질문
이 로그인에는 1분 동안 틀린 비밀번호를 몇 개나 시도할 수 있고, 다섯 번째 실패 이후에는 무슨 일이 일어나는가?
경고 신호
“기본 비밀번호가 강하니까 괜찮아요” · “관리자 페이지뿐이에요”.
예시와 해결 방법
예시
웹 시뮬레이션을 다시 돌렸을 때 두 실행 모두, 관리자 페이지는 비밀번호를 요구했지만 실패한 시도를 제한하지 않았다. 틀린 비밀번호 200개를 연속으로 보내자 1초도 안 되어 200번의 거부가 돌아왔고, 차단된 것은 없었다. 기본 비밀번호는 길고 무작위였지만, 빌더의 요약은 소유자에게 새 비밀번호를 설정하는 방법을 알려 주면서도 길이를 요구하지는 않았다.
해야 할 일
반복된 실패를 늦추거나 막는다. 주소별과 계정별로, 기다리는 시간을 점점 늘리는 방식으로 적용한다. 소유자가 비밀번호를 정할 때는 최소 길이를 요구한다. 테스트는 틀린 비밀번호를 연달아 스무 번 보내고, 마지막 시도들이 거부되는지 확인한다.