네 번의 실패
이날은 메시지 배달 API를 설계하면서 네 번 실패한 날이다.
“언제 배달할지”와 “깨울지 말지”를 하나의 필드로 표현하고 싶었다.
근데 두 개의 직교하는 축을 하나로 합치는 게 어려웠다.
다섯 번째에서 정답을 찾았다.
그리고 Bun 런타임 마이그레이션을 측정 데이터로 거절한 것도 있었다.
메시지 배달 API가 필요했다.
확장이 메시지를 보낼 때, “지금 바로”인지 “다음 단계에”인지 “쉬고 있을 때”인지 지정할 수 있어야 했다.
그리고 “메인 에이전트를 깨울지 말지”도 지정해야 했다.
첫 번째 시도: 이름으로 시점 표현
“실행 중”, “실행 후”, “다음 턴”이라는 이름을 썼다.
근데 “midRun이 정확히 언젠지” 이름만 봐서는 알 수 없었다.
이름이 시점을 명확히 표현하지 못했다.
기각.
두 번째 시도: 4개 값
“이번 단계”, “다음 단계”, “대기”, “다음 사용자 요청” 4개 값으로 정리했다.
근데 “대기”에 문제가 있었다.
“쉬고 있을 때”인데, “쉴 때 깨워라”와 “쉴 때 깨우지 마라”를 구분할 수 없었다.
별도의 깨움 여부 설정 축을 추가해서 해결하려 했다.
근데 이러면 값이 2개 축의 조합이 된다.
하나의 필드로 표현하려는 목표에 어긋난다.
기각.
세 번째 시도: 그래도 4개 값
깨움 여부 설정을 유지하면서 4개 값을 개선했다.
근데 여전히 두 축이 혼재됐다.
호출하는 쪽이 두 개의 필드를 동시에 설정해야 했다.
복잡도가 줄지 않았다.
기각.
네 번째 시도: 여전히 막힘
3번째와 비슷한 방식으로 또 시도했다.
결과는 같았다.
두 축을 하나로 합칠 수 없었다.
다섯 번째: idle을 둘로 쪼갰다
정답은 idle을 둘로 쪼개는 거였다.
“쉴 때 깨우기”는 “쉴 때 깨워라”다.
“대기”는 “쉴 때 깨우지 마라”다.
하나의 값이 “언제”와 “깨울지”를 동시에 표현한다.
왜 이게 정답인가
“대기”를 둘로 쪼개면, 깨움 여부 설정 축이 필요 없어진다.
“쉴 때 깨우기”가 자체적으로 “깨운다”를 포함하고 “대기”가 자체적으로 “안 깨운다”를 포함한다.
두 축이 하나의 값으로 합쳐진다.
다른 값들(“이번 단계”, “다음 단계”, “다음 사용자 요청”)은 깨움 여부가 자명하다.
“이번 단계”은 당연히 깨운다.
“다음 사용자 요청”는 당연히 안 깨운다.
깨움 여부가 모호한 건 “대기”뿐이었다.
그래서 “대기”만 쪼개면 된다.
설계의 핵심: 모호한 지점만 쪼갠다
전부를 쪼갤 필요가 없다.
모호한 지점만 쪼개면 된다.
나머지는 자명하니까 그대로 둔다.
“전부 쪼개서 8개 값”이 아니라 “하나만 쪼개서 5개 값”이 더 단순하다.
13개 호출 지점 검증
5개 값으로 확정한 후, 6개 확장의 13개 호출 지점을 전부 매핑했다.
기존의 깨움 설정: true는 “쉴 때 깨우기”으로, 깨움 설정: false는 “대기” 또는 “다음 단계”으로 변환됐다.
100% 호환됐다.
번역 계층으로 구현했다
pi 코어를 수정하지 않았다.
확장 내부에서 5개 값을 pi의 내부 프리미티브로 번역한다.
코어를 건드리지 않으니까 런타임 호환성 부담이 없다.
Bun 런타임 마이그레이션: 측정 데이터로 거절
Bun 런타임으로 마이그레이션할지 검토했다.
성능을 측정했다.
Node 22.16은 웜 중앙값 659밀리초. Bun 1.3.14는 541밀리초.
Bun이 18% 빠르다.
이 18%는 의미가 없다
pi는 LLM API 호출과 네트워크 입출력에 병목이 있는 장기 실행 프로그램이다.
시작 속도 120밀리초 차이는 노이즈다.
LLM 응답은 수 초가 걸린다.
시작 속도가 120밀리초 빨라도 전체 실행 시간에는 의미가 없다.
진짜 장벽: code-index
Bun에서 code-index 확장이 치명적으로 실패한다.
code-index가 node
import하는데, Bun은 이를 지원하지 않는다.node
bun 포팅할 수는 있지만 그다음 node-llama-cpp라는 네이티브 애드온이 또다시 벽이다.네이티브 네이티브 애드온 파일 파일은 Bun에서 로드할 수 없다.
Bun으로 마이그레이션하면 code-index를 포기해야 한다.
code-index는 코드 의미 검색의 핵심이다.
포기할 수 없다.
결론: Node에 머문다
측정 데이터가 말해준다.
시작 속도는 빨라지지만 의미 없다.
code-index가 안 된다.
Node에 머무른다.
바이트 동일 깃 히스토리 재구성
이날 가장 즉시 사용 가능한 기법이었다.
63개의 지저분한 커밋을 5개의 논리적 커밋으로 재구성했다.
최종 트리는 원본과 바이트 단위로 동일했다.
핵심 원칙: 코드를 다시 입력하지 마라
논리적 단위마다 git checkout <source> -- <files>로 파일을 가져온다.
코드를 다시 입력하지 않는다.
드리프트가 불가능하다.
가져온 후 git diff HEAD <source> -- <fileset>으로 검증한다.
차이가 비어 있어야 한다.
비어 있으면 바이트 동일이다.
교차 파일 처리
한 파일이 두 개 이상의 논리적 단위에 걸쳐 있으면, 통째로 한 단위에 할당한다.
아니면 git checkout -p로 헝크 단위로 가져온다.
헝크 단위는 검증 비용이 높으니까 최소한으로 쓴다.
마무리
이날의 핵심은 두 가지였다.
첫째, 설계에서 모호한 지점만 쪼개라. 전부를 쪼갤 필요가 없다.
“대기”만 모호했으니까 “대기”만 쪼갰다.
4개 값에서 8개 값이 아니라 5개 값이 됐다.
모호한 걸 쪼개고 자명한 건 그대로 둔다.
둘째, 측정 데이터로 결정해라. Bun이 18% 빠르다.
근데 그 18%가 의미 있는 차이인가? pi는 LLM 병목 프로그램이다.
시작 속도는 노이즈다.
그리고 code-index가 안 된다.
측정 데이터가 “아니오”라고 말하면, 거절해야 한다.
그리고 깃 히스토리 재구성은 “다시 입력하지 마라”가 핵심이다.
코드를 다시 입력하면 드리프트가 생긴다.
체크아웃으로 가져오면 바이트 동일이 보장된다.