기능을 하나씩 만들면서, 구현하며 배우게 되는 걸 적어보려고 한다. 그 첫 번째 기록이다.
이번에 만드는 기능은 이거다.
레시피 하나와 “내가 가진 재료 목록”을 넣으면, 그 재료만으로 성립하는 조리법이 나오는 함수.
계란말이 레시피가 있는데 당근이 없다면, 당근을 빼고도 말이 되는 조리법을 돌려주는 것이다. 구현에 들어가기 전에 설계 질문 몇 개에 스스로 먼저 답을 적고, 그 답을 평가받아봤다. 결과를 정리하면 — 맞힌 것보다 틀리고 빠뜨린 게 훨씬 많았다. 그게 이 글의 알맹이다.
Table of contents
내가 스스로 답해본 설계 질문들
먼저 이런 질문에 내 나름의 답을 적었다.
- 레시피를 파일에 어떻게 적을까? → 필수 재료 + 있으면 좋은 재료 목록
- 함수는 뭘 받아 뭘 돌려주나? → (레시피, 가진 재료) 받아서 조리법 반환
- “당근만 없음”과 “당근·부추 없음”을 어떻게 구분? → 가진 재료 목록에 있는지 없는지로
- 필수 재료(계란)를 체크 해제하면? → 계란은 무조건 들어가게 막는다
- 재료가 빠져 사라지는 조리 단계는? → LLM으로 변형하거나, 조합마다 레시피를 따로 등록
- 잘 됐는지 어떻게 검증? → 직접 눌러보고, AI한테 테스트 케이스를 짜달라고 한다
아래는 이 답들에 대한 평가다.
맞힌 것
- 입출력 방향은 맞았다. “(레시피 + 가진 재료)를 받아 조리법을 돌려준다.” 실제 설계가 그거다.
- 구분 방법도 맞았다. 나는 “리스트에 있는지 없는지로 판단하면 안 되나??”라고 자신 없게 적었는데 — 그게 답이다. 가진 재료 목록에 당근이 없으면 없는 거다. 더 복잡할 게 없었는데 괜히 자신 없어 했다.
- 이 기능의 정의를 내가 이미 적었다. “단순히 재료가 들어가는 항목만 빼는 게 아니라, 재료가 없을 때 유효한 레시피를 만들어주는 것.” 이 한 문장이 이 기능의 핵심이다.
- 제일 값진 답: “변형된 레시피가 실제로 그렇게 만들 수 있는 레시피인지 검증한다.” 코드 테스트로는 절대 못 잡는 종류의 실패를 정확히 짚었다.
틀린 것 5개
1. 재료를 ‘인덱스’로 가리키면 안 된다
재료를 순서 번호로 가리키면 이렇게 깨진다.
지금: 0=계란 1=식용유 2=소금 3=당근 4=부추
누군가 링크 공유: "3번 재료를 끈 계란말이" ← 당근 뺀 버전
한 달 뒤, 재료 목록을 정리하며 소금을 맨 뒤로 옮김
이후: 0=계란 1=식용유 2=당근 3=부추 4=소금
아까 그 링크: "3번 재료를 끈 계란말이" ← 이제 부추 뺀 버전이 열린다
링크를 받은 사람은 뭐가 바뀐지 모른다. 화면은 멀쩡하고 에러도 없다. 그냥 다른 요리를 본다.
→ 재료마다 뜻이 있는 이름표를 붙인다. carrot, chive. 순서를 아무리 바꿔도 carrot은 당근이다. 그리고 한 번 쓴 이름표는 다른 재료에 재사용하지 않는다.
2. 필수 재료라도 체크 해제를 ‘막으면’ 안 된다
계란 체크를 못 끄게 막고 싶었다. 그런데 이러면 잃는 게 있다. “이 재료로는 만들 수 없음”에 도달한 비율은, 필수 재료 판정이 너무 빡빡한지 알려주는 신호다. 계란을 못 끄게 막으면 이 숫자가 영원히 0이라, 내가 소금을 필수로 잘못 넣었는지조차 알 방법이 사라진다.
→ 끌 수 있게 두고 “이 재료로는 만들 수 없습니다”를 보여준다. 못 하게 막는 게 아니라 정직하게 말하는 것이다. (다만 “재료마다 필수 표시를 붙인다”는 뜻이었다면 그건 맞다.)
3. LLM은 ‘요청 시점’이 아니라 ‘저작 시점’에 쓴다
사라진 단계를 LLM으로 변형하겠다고 했는데, 시점이 틀렸다.
| 요청 시점 (사용자가 체크 끌 때) | 저작 시점 (개발 중 한 번) | |
|---|---|---|
| 언제 | 매번 | 한 번 |
| 결과 | 매번 다름 | 고정돼 파일에 저장 |
| 검증 | 불가능 (아직 안 나온 문장) | 사람이 읽고 고쳐서 커밋 |
| 틀리면 | 사용자가 음식을 망침 | 커밋 전에 잡힘 |
요청 시점 생성은 실무적으로도 깨진다. 같은 체크 조합으로 두 번 열면 다른 조리법이 나온다 → 기댓값이 없으니 테스트를 짤 수 없다. 어제 본 조리법을 오늘 다시 못 찾는다.
→ LLM은 쓰되 개발 중 초안 뽑는 도구로만. 뽑은 문장은 사람이 고쳐서 파일에 박아넣고, 사용자가 보는 건 항상 그 고정된 문장이다.
4. 조합마다 레시피를 ‘통째로’ 등록하면 폭발한다
조리법을 조합마다 통째로 등록하면:
비필수 재료 3개(소금·당근·부추) → 8벌
비필수 재료 4개 → 16벌
비필수 재료 5개 → 32벌
레시피 5개면 80벌. 한 벌 쓰는 데 18분이면...
→ 조리법 통째가 아니라 단계마다 대체 문장을 단다.
2. 당근과 부추를 곱게 채썬다.
├ 당근 없으면 → "부추를 곱게 채썬다"
├ 부추 없으면 → "당근을 곱게 채썬다"
└ 둘 다 없으면 → 이 단계 삭제
8벌이 아니라 문장 3개다. 그리고 이 3개는 소금이 있든 없든 그대로 쓴다 — 소금은 이 단계와 무관하니까. 이게 조합 폭발을 피하는 방법이다.
5. AI가 짠 테스트에는 함정이 있다
“AI한테 테스트를 짜달라고 한다”고 했는데, AI는 구현된 코드를 보고 테스트를 짠다. 그래서 코드가 틀려도 통과하는 테스트를 만든다. 코드가 “당근 없으면 2번 단계를 지운다”고 잘못 짜여 있으면, AI는 “2번이 지워지는지 확인”하는 테스트를 짜고, 통과한다. 사용자는 채썰 재료도 없이 다음 단계로 넘어간다.
테스트가 통과했다는 사실만으로는, 코드가 옳은지 테스트가 허술한지 구분되지 않는다.
→ 두 가지로 막는다. ① 테스트할 경우 목록을 구현 전에 정한다(코드에 맞춰지지 않게). ② 일부러 코드를 망가뜨려 본다 — 필수 재료 검사를 지웠는데도 테스트가 다 통과하면, 그 테스트는 아무것도 안 지키고 있는 것이다.
빠뜨린 것 6개
1. 조리 ‘문장’을 어디에 둘지 (데이터 모양)
나는 재료 목록만 답했다. 그런데 이 기능이 다시 쓰는 건 **문장(조리 단계)**이다. 재료는 그 문장을 고르는 조건일 뿐이다. 데이터의 절반을 빠뜨렸다.
2. 출력에 ‘재료 목록’도 같이 담아야 한다
조리법만 돌려주면 이렇게 어긋난다.
[재료] 계란 3개 · 식용유 · 소금 · 당근 1/4개 · 부추 조금 ← 당근이 남아 있다
[조리법]
1. 계란 3개를 볼에 풀고 소금을 약간 넣는다.
2. 부추를 곱게 채썬다. ← 당근 단계는 사라졌는데
본문과 재료 목록을 따로 계산하면 반드시 어긋난다. 한 번에 같이 만들어 돌려줘야 한다.
3. 함수가 원본 레시피를 고치면 안 된다
“체크할 때마다 함수를 부른다”고 했는데, 함수가 원본을 직접 고쳐버리면:
당근 끄기 → 2번이 "부추를 곱게 채썬다"로 바뀜 (원본이 사라짐)
당근 다시 켜기 → 원본이 없으니 "당근과 부추를..."을 복구 못 함
→ 화면엔 계속 "부추를 곱게 채썬다"
체크를 껐다 켰다 할수록 레시피가 점점 깎여나가고, 되돌릴 방법이 없다. → 함수는 원본을 읽기만 하고 결과를 새로 만들어 돌려준다.
4. 후보가 여럿일 때 누가 이기나
당근용·부추용·둘 다 없을 때 문장을 다 써뒀는데, 사용자가 둘 다 껐다.
당근 없음용 "부추를 곱게 채썬다" ← 당근 없으니 맞는 것 같다
부추 없음용 "당근을 곱게 채썬다" ← 부추도 없으니 맞는 것 같다
둘 다 없음용 (이 단계 삭제) ← 이것도 맞다
셋 다 조건에 걸린다. 기준이 없으면 컴퓨터는 첫 번째를 골라 **“부추를 곱게 채썬다”**를 내보낸다 — 부추가 없는데. 화면은 정상, 에러도 없고, 사용자만 없는 부추를 찾는다.
→ 조건이 더 구체적인 쪽이 이긴다. “둘 다 없을 때”는 재료 2개를 짚었고 “당근 없을 때”는 1개를 짚었다. 더 많이 짚은 쪽이 더 구체적이다. (보너스: “둘 다 없을 때” 문장을 안 써두면 1개짜리 둘이 동점 → 동점은 그 조합을 저작자가 안 썼다는 증거라 커밋 전에 잡힌다.)
5. 유령 참조 — 재료 이름이 안 적힌 단계
당근·부추를 둘 다 끄면 2번이 사라진다. 그런데 3번은?
2. 당근과 부추를 곱게 채썬다. ← 사라짐
3. 채썬 채소를 계란물에 섞는다. ← 남아 있다. "채썬 채소"가 어디서 왔나?
3번엔 재료 이름이 하나도 없다. 그래서 “빠진 재료 이름이 본문에 남았나” 검사로도 안 걸린다. 의존이 재료가 아니라 앞 단계의 결과물에 있기 때문이다. 이게 가장 안 보이는 함정이었다. → “3번은 2번이 있어야 성립한다”를 데이터에 적어둔다.
6. 손으로 다 눌러볼 수 없다
비필수 3개 → 확인할 조합 8 → 레시피 5개면 40번
비필수 5개 → 확인할 조합 32 → 레시피 5개면 160번
한 번 고칠 때마다 160번 클릭할 수는 없다. 손 확인은 몇 개만, 나머지는 자동으로 돌게 해야 한다.
정리
| 질문 | 판정 |
|---|---|
| 데이터 모양 | 절반 — 재료는 맞고 문장이 빠짐 |
| 입출력 | 맞음 — 단 출력에 재료 목록, 원본 안 고치기 추가 |
| 구분 방법 | 맞음 — 단 후보 여럿일 때 누가 이기나가 빠짐 |
| 필수 재료 | 절반 — 명시는 맞고 인덱스·해제 막기는 틀림 |
| 사라진 단계 | LLM 시점 틀림 / “따로 등록”은 단위 틀림 / 유령 참조 빠짐 |
| 검증 | 방향 맞음 — AI 테스트의 함정·자동화 필요가 빠짐 |
회고
솔직히 새로 배운 개념은 많지 않았다. 인덱스보다 이름표, 불변으로 다루기, 조합 폭발 피하기 — 대부분 어디선가 들어본 것들이다. 문제는 내가 아는 선에서 “그래서 이걸 지금 이 기능에 어떻게 적용하지”를 몰라서 잘못 답한 것들이었다.
그래서 이 기록의 값어치는 “몰랐던 걸 알게 됨”이 아니라, 아는 걸 실제 설계에 못 얹고 있던 지점을 정확히 들킨 것에 있다. 구현하기 전에 스스로 답을 적어두지 않았다면, 이 11개는 전부 코드를 다 짜고 나서 — 혹은 사용자가 없는 부추를 찾을 때 — 알게 됐을 것이다.
더 들여다볼 것
- 가장 구체적인 규칙이 이긴다(specificity) — CSS 선택자 우선순위나 패턴 매칭에서 익숙한 개념. 이 “조건이 더 많이 짚은 쪽이 이긴다”를 일반화하면 어디까지 쓰이는지.
- 불변 데이터로 다루기 — 원본을 안 고치고 새 결과를 만드는 패턴. 체크 토글의 되돌리기 문제가 사실 이 한 가지로 풀린다.
- 뮤테이션 테스팅 — “일부러 코드를 망가뜨려 테스트가 잡는지 본다”의 정식 이름. 테스트가 실제로 뭔가 지키고 있는지 검증하는 방법.
- 저작 시점 vs 요청 시점 — LLM을 빌드 타임에 쓰고 결과를 고정하는 것과, 런타임에 매번 생성하는 것의 트레이드오프. 재현성·검증가능성 기준으로.