"똑같은 부분은 같이 쓰고, 소유가 달라지는 딱 그 자리에서 갈라져. 한 줄 일찍도 한 줄 늦게도 아니고."
가운데는 같이 써
Pippa Go 엔 이미지를 실어 나르는 흐름이 둘 있어. Prompt Macro 는 대화 턴 없이 composer 를 돌리는 유틸리티고, Ask 는 canonical 스트리밍 챗이야. 둘 다 똑같은 상황에서 출발해. 사용자가 이미지를 골랐고, 그게 지금 기기 로컬 staging 에 blob 이랑 메타데이터로 앉아 있는 상황. 그 staged blob 을 실제로 쓸 수 있는 바이트랑 제대로 된 media type 으로 바꾸는 일은 어느 흐름이든 똑같아. 그래서 staged-media resolver 하나에서 한 번만 처리해. 이걸 두 군데에 복사해 두면 전형적인 drift 버그가 나거든. 인코딩이랑 방향이랑 media type 감지가 두 복사본에서 조금씩 어긋나기 시작해.
여기서부터는 일부러 갈라져
바이트를 다 풀어냈으면 이제 두 흐름이 갈라져. 그 갈라짐은 우연이 아니라 원칙에 따른 거고. 소유 경계에서 갈라지는 거야. 이미지가 어떻게 되느냐는 그 결과를 누가 갖느냐에 달렸으니까.
- Prompt Macro 는 utility 봉투로. Macro 는 대화가 아니라 apply-free 유틸리티야. 그래서 이미지가 가벼운 utility 봉투에 실려. 인라인으로 인코딩한 바이트로. canonical 상태를 안 만드는 one-shot 호출한테 딱 맞는 모양이지.
- Ask 는 canonical 봉투로. Ask 는 진짜 대화 턴을 만드니까 이미지가 canonical 첨부로 업로드되고 chat 메시지는 그걸 참조해. canonical 턴의 나머지랑 똑같이 백엔드가 갖는 거야.
풀어낸 바이트는 같은데 봉투가 둘인 거야. Macro 의 결과는 일회용이고 Ask 의 결과는 canonical 이니까. 이 갈라짐은 군더더기가 아냐. 소유 모델이 코드 밖으로 비쳐 보이는 거지.
규칙은 하나야. 경계까지 같이 가고, 경계에서 갈라지기
이건 어디든 가져다 쓸 수 있는 설계 감각이야. 두 흐름이 앞부분은 같고 뒤에서 달라지면, 같은 부분을 컴포넌트 하나로 빼내고 요구사항이 실제로 달라지는 지점에서 갈라지게 해. 여기선 결과 이미지를 누가 갖느냐가 그 지점이고. 덜 공유하면 resolver 를 두 벌 갖게 되고(drift), 더 공유하면 Macro 랑 Ask 를 둘 다 안 맞는 봉투 하나에 욱여넣게 돼(leaky abstraction). staged-media resolver 는 그 선을 제대로 그었어. '진짜 바이트랑 media type 을 손에 넣었다' 까지는 공통이고, 그 바이트가 어디 사느냐부터는 흐름마다 다른 거야.