오픈소스 13,000줄을 벤더링하기로 한 이유
의존성으로도 포크로도 안 돼서 벤더링을 골랐다. 대가로 포매터를 대지 않고 커밋을 나누는 규율이 생겼고, 못 고치는 것은 문서에 적어 뒀다.
기억 서비스의 저장·검색 엔진은 직접 만들지 않았다. 오픈소스 라이브러리를 가져다 썼다. 그런데 첫 글에서 쓴 대로, 그 라이브러리의 추출 프롬프트를 우리가 고쳐야 했다. 한국어가 영어로 추출되던 문제 때문이다.
여기서 선택이 갈린다.
의존성, 포크, 벤더링
| 선택 | 우리 수정 | upstream 반영 | 대조 |
|---|---|---|---|
| 의존성으로 사용 | 불가 | 자동 | 필요 없음 |
| 포크 | 자유 | cherry-pick 필요 | 금방 흐려짐 |
| 벤더링 | 자유 | 선택적 | 유지 가능 |
의존성으로 쓰면 프롬프트를 못 고친다. 포크하면 고칠 수는 있지만, 시간이 지나면 "이게 원래 저랬나 우리가 고친 건가"를 아무도 모르게 된다.
그래서 벤더링을 골랐다. 코드를 저장소 안으로 통째로 들여오되, 언제든 원본과 diff 를 뜰 수 있는 상태로 유지하는 방식이다.
지키기로 한 규칙
원본과 계속 대조할 수 있으려면 몇 가지 규칙을 예외 없이 지켜야 했다.
포매터를 대지 않는다. 린터도 임포트 정렬도 전부 제외 목록에 넣었다. 포매팅하는 순간 원본과 다른 코드가 돼서 대조가 불가능해지기 때문이다. 커밋 훅이 걸리면 훅을 끄고 커밋한다.
한 줄을 고쳐도 왜 고쳤는지 주석을 남긴다. 나중에 upstream 과 대조할 때 그 주석이 유일한 단서고, 주석이 없으면 다음 사람은 "원본이 저랬나 보다" 하고 넘어간다.
이관은 세 단계 커밋으로 나눈다.
01원본 그대로 넣는다. 이 커밋이 대조 기준점이다. 여기에 다른 것을 섞으면 기준점이 사라진다.
이 순서를 지키면 언제든 커밋 1과 현재를 비교해서 "우리가 바꾼 전부"를 볼 수 있다.
그래서 뭘 알게 되냐면
남의 코드를 읽을 수밖에 없게 되고, 그러면서 밖에서는 안 보이던 것들이 보였다.
이름만 async 인 클래스
라이브러리에는 동기 클래스와 비동기 클래스가 둘 다 있었다. 당연히 비동기 쪽을 써야 한다고 생각했다.
열어 보니 이랬다.
우리가 동기 코드를 스레드풀에 넘기는 것과 같은 일을 하고 있었다. 요청 하나를 통째로 넘기느냐 호출 하나씩 넘기느냐의 차이뿐이다.
비동기 쪽으로 갈아타면 얻는 건 사실상 없고, 우리 수정을 두 벌로 유지해야 한다. 18곳의 수정이 전부 동기 경로에 있어서, 그걸 복제하고 두 벌을 계속 맞춰야 한다.
병목은 다른 데 있었다.
커넥션 풀 크기를 두 배로 열었을 뿐이다. 2.3배. async 로 바꿔도 풀이 막혀 있으면 거기서 막힌다 — 아키텍처를 바꾸기 전에 설정을 재보는 편이 빨랐다.
설정 스위치가 없는 기능
라이브러리 v2 에는 문서에 없는 엔티티 링킹 기능이 있다. 기억 문장에서 개체를 뽑아 별도 테이블에 임베딩하고, 검색 때 같은 개체를 공유하는 기억의 점수를 올린다. 그럴듯한 기능이다.
게다가 그 기능은 한국어에서 거의 쓸모가 없다. 모델이 영어용으로 하드코딩돼 있고 인명 판정이 첫 글자가 대문자인가로 되어 있어서, 대문자가 없는 한국어는 항상 탈락한다. 켜지면 비용만 붙고 효과는 없다.
고칠 수도 있었다. 한국어 모델로 바꾸고 대문자 판정과 영어 단어 필터를 통째로 다시 쓰면 되는데, 그러면 「우리 diff 를 최소로」와 정면으로 충돌한다. 그래서 고치지 않고, 어떤 조건에서 켜지는지와 켜지면 무슨 일이 생기는지를 문서에 적어 뒀다.
조용히 무시되는 설정 키
설정 객체에 잘못된 키 이름을 넣으면 에러 없이 그냥 무시된다.
- 데이터베이스 연결 문자열을
dsn이라는 키로 주면 없는 키라 무시되고, 연결 에러 없이 기본값으로 조용히 떨어진다 - 이력 저장소 경로를 잘못 주면 PostgreSQL 대신 로컬 파일 DB 로 떨어지고, 로그도 없다
- v1 에 있던 그래프 설정은 v2 에서 사라졌는데, 넣어도 에러 없이 무시된다
전부 지난 글에 쓴 것과 같은, 예외를 던지지 않는 실패다.
원자적이지 않은 삭제 경로
삭제 경로가 원자적이지 않다. 벡터 행 삭제, 이력 기록, 정리 작업이 각각 다른 커넥션에서 트랜잭션 없이 순서대로 실행된다. 첫 단계만 커밋되고 나머지가 실패하면 "기억은 사라졌는데 삭제 이력은 없는" 상태가 된다.
벤더 코드를 원자적으로 재구성하면 diff 가 폭증하니 그렇게 하지 않고, 바깥에 보상 레이어를 뒀다. 이력이 빠진 것을 나중에 채우고, 고아가 된 행을 따로 걷는다.
엔진을 교체하면 보상 레이어도 같이 사라진다. 그때까지는 이 부채를 안고 가는 편이, 벤더 코드를 뜯어고쳐서 원본과 대조를 못 하게 되는 것보다 낫다고 판단했다.
끝