과도한 고민, 범위 확장, 구조적 diff로 프로젝트를 망치는 법

2 days ago 3
  • 프로젝트는 바로 만들어 끝내는 흐름과 조사와 설계가 커지며 원래 문제를 놓치는 흐름으로 갈리기 쉬우며, 실제 진전에는 그냥 해보는 쪽이 더 앞설 때가 많음
  • Emacs용 fuzzy path 검색을 만들면서도 좋은 라이브러리의 부가 기능이 새 요구를 낳아 설계가 비대해졌고, 결국 필요하지 않은 앵커 기능 코드를 전부 버리며 YAGNI를 다시 확인하게 됨
  • 코드 diff에서는 줄 단위 비교가 함수나 타입 같은 상위 구조를 제대로 잡지 못하고, treesitter 기반 도구도 엔티티 매칭이 어긋나면 삭제와 추가를 길게 보여줘 읽기 어려워질 수 있음
  • 필요한 방향은 LLM 출력의 턴별 리뷰에 맞는 최소 범위 도구를 먼저 만드는 일이며, Rust용 엔티티 추출과 단순 매칭으로 시작해 상위 수준 변경 개요를 빠르게 보는 워크플로가 우선됨

과도한 고민과 범위 확장

  • 프로젝트는 바로 만들어 끝내는 흐름과, 선행 사례를 파고들다가 범위가 커져 정작 원래 문제를 해결하지 못하는 흐름으로 갈리기 쉬움
  • 주말에 만든 주방 선반은 커피를 마시며 설계를 정하고, 3D 프린트 행거를 몇 차례 수정한 뒤, 남은 자재와 페인트를 써서 주말 안에 완성됨
    • Ikea bin 행거용 CAD는 OnShape CAD로 공개돼 있음
    • 자재는 작업대에서 남은 것을 재사용했고, 모서리는 palm sander로 눈대중으로 다듬음
  • 이 선반은 정확히 주방에 맞는 물건을 만드는 일보다, 친구와 함께 목공을 즐기는 일이 주된 성공 기준이었고, 그 덕분에 세부 기준을 과하게 고민할 필요가 줄어듦
  • 반대로 구조적 diff 도구를 찾는 과정에서는 difftastic의 결과가 아쉬워 관련 도구와 워크플로를 4시간 동안 조사했지만, 결국 Emacs에서 쓸 더 나은 diff 워크플로라는 원래 기준으로 돌아가게 됨
  • 하드웨어 프로토타이핑 인터페이스, Clojure와 Rust를 섞은 언어, CAD용 언어 같은 오랜 관심사는 배경 조사와 작은 프로토타입에 수백 시간을 썼지만, 처음의 동기를 직접 해결하는 결과물로는 아직 이어지지 못함
  • 언어와 CAD 프로젝트에서는 Rust나 Clojure를 대체할지, 일부 문제만 다룰지, 학습용 놀이터면 충분할지, 상용 CAD를 바꿀지, 다른 사람에게도 유용해야 할지처럼 성공 기준이 흐릿함
  • 이런 질문을 검토하는 일은 가치가 있지만, 많은 것을 검토만 하기보다 실제로 많이 만들어보는 편이 더 낫다고 봄
  • 뒤늦게 보면 분명히 좋지 않은 결과물이 나오더라도, 그냥 해보는 쪽이 전체적으로는 더 앞서게 만듦

범위 확장의 보존 법칙

  • 무턱대고 만드는 시간에도 한계는 있고 균형이 필요하며, LLM agent로 코드를 많이 쓴 뒤 결국 전부 버린 경험이 다시 YAGNI를 떠올리게 만듦
  • Emacs에서 쓸 Finda 스타일의 전체 파일시스템 fuzzy path 검색을 만들고 싶었고, 예전에 같은 기능을 손코딩으로 만든 적이 있어 LLM을 감독하면 몇 시간 안에 끝낼 수 있다고 봄
  • 처음에는 계획용 대화에서 Nucleo를 추천받았고, 잘 설계되고 문서화돼 있어 smart caseUnicode normalization 기능을 얻기 위해 채택함
    • 예시로 쿼리 foo는 Foo와 foo를 모두 맞추지만, Foo는 foo를 맞추지 않음
    • cafe와 café 처리도 같은 맥락으로 다뤄짐
  • 문제는 좋은 라이브러리 자체가 아니라, Nucleo가 앵커 기능도 지원한다는 점이었음
  • 파일 경로만 있는 코퍼스에서는 줄 시작 앵커가 쓸모없어 보여, 이를 path segment 기준 앵커로 해석하려고 함
    • 예시로 ^foo는 /root/foobar/는 맞추지만 /root/barfoo/는 맞추지 않게 하려 함
  • 이를 효율적으로 처리하려면 인덱스가 세그먼트 경계를 저장해야 하고, 각 세그먼트에 대해 쿼리를 빠르게 검사할 수 있어야 함
  • 여기에 ^foo/bar처럼 슬래시가 들어간 앵커 쿼리도 처리해야 했고, 세그먼트 단위 검사만으로는 /root/foo/bar/baz/ 같은 경로를 제대로 매칭하기 어려워짐
  • 이 설계를 두고 몇 시간을 더 보냈고, LLM과 아이디어를 주고받고, Nucleo 타입을 감싸는 코드를 만든 뒤, 코드가 지나치게 비대하고 마음에 들지 않아 결국 더 작은 래퍼를 직접 다시 작성함
  • 쉬고 난 뒤 Finda에서 앵커 기능이 필요했던 적이 떠오르지 않고, 경로 코퍼스에서는 쿼리 앞이나 뒤에 /를 붙여 대부분의 앵커 역할을 대신할 수 있다는 점을 깨닫게 됨
    • 파일명 끝에 대한 앵커만 예외로 남음
  • 결국 앵커 관련 코드는 전부 버렸고, LLM과 다른 사람들과의 논의 없이 처음부터 직접 썼을 때보다 여전히 이득이었는지는 확신하기 어려움
  • 프로그래밍 속도가 빨라질수록 그만큼 불필요한 기능, rabbit hole, 우회로도 함께 늘어나는 보존 법칙 같은 것이 있는 듯함

구조적 diffing

  • 코드에서 diff는 보통 파일 두 버전 사이의 줄 단위 변경 요약을 뜻하며, unified view에서는 추가와 삭제를 +, -로 표시함
  • 같은 diff는 좌우 비교 형태로도 렌더링할 수 있고, 변경이 복잡할수록 이런 형태가 읽기 더 쉬워질 수 있음
  • 줄 단위 diff의 문제는 함수나 타입 같은 상위 구조를 인식하지 못한다는 점이며, 중괄호가 어찌어찌 맞아떨어지면 서로 다른 함수에 속한 경우에도 표시가 생략될 수 있음
  • difftastictreesitter가 제공하는 concrete syntax tree를 이용해 이런 문제를 줄이려 하지만, 버전 간 엔티티 매칭이 항상 잘 되지는 않음
  • 직접 계기가 된 diff에서는 struct PendingClick이 양쪽에서 서로 대응되지 않고, 왼쪽에서는 삭제, 오른쪽에서는 추가된 것으로 표시됨
  • 왜 매칭에 실패하는지는 파고들지 않았지만, 전체 diff가 더 길어지더라도 PendingClickRequest와 PendingClick이 양쪽에서 대응되게 보는 편이 더 낫다고 판단함

구조적 diff 도구와 참고 자료

  • 가장 완성도 높고 신중하게 다듬어진 semantic diff 도구로는 semanticdiff.com을 꼽고 있음
    • 독일의 작은 회사가 제공하며, 무료 VSCode 플러그인과 GitHub PR diff를 보여주는 웹 앱이 있음
    • 다만 원하는 워크플로의 기반으로 삼을 수 있는 코드 라이브러리는 제공하지 않음
    • semanticdiff vs. difftastic 글에는 유용한 세부 사항이 많고, difftastic이 Python에서 의미 있는 들여쓰기 변화조차 보여주지 못하는 문제도 포함돼 있음
    • 저자 중 한 명은 HN 댓글에서 treesitter를 의미론 처리에 쓰다가 벗어났다고 밝히며, 문맥 의존 키워드와 lexer 동작 때문에 파싱이 실패해 async 같은 이름을 파라미터로 쓴 경우에도 도구가 멈출 수 있다고 적음
  • diffsitter는 treesitter 기반이며 MCP server를 포함함
    • GitHub star 수는 많지만 문서화는 그다지 잘돼 보이지 않았고, 동작 방식을 설명한 자료를 찾기 어려웠음
    • difftastic 위키에는 트리의 leaf에 대해 longest-common-subsequence를 수행한다고 적혀 있음
  • gumtree는 2014년의 연구·학술 배경에서 나온 도구임
    • Java가 필요해 Emacs에서 빨리 쓸 도구라는 개인 용도에는 맞지 않음
  • mergiraf는 Rust로 작성된 treesitter 기반 merge-driver임
    • architecture overview가 잘 정리돼 있고, 내부적으로 Gumtree 알고리듬을 사용함
    • 문서와 그림을 보면 세심하게 작성된 프로젝트라는 인상을 줌
    • semanticdiff.com 저자는 HN 댓글에서 GumTree가 결과를 빨리 내놓지만, 여러 후속 논문 개선안을 적용해도 항상 나쁜 매칭을 돌려주는 경우가 꽤 있었고, 결국 매핑 비용을 최소화하는 dijkstra 기반 접근으로 전환했다고 적음
  • weave는 Rust로 작성된 또 다른 treesitter 기반 merge-driver임
    • 화려한 랜딩 페이지, 많은 GitHub star, MCP server 등 전체 인상이 다소 과장돼 보임
    • 엔티티 추출 크레이트 sem을 살펴봄
    • 핵심 diff 코드는 괜찮지만 다소 장황하고, 엔티티 매칭은 greedy 알고리듬을 사용함
    • 데이터 모델은 파일 내부 이동을 감지하지 못하며, 그런 이동은 의미가 클 수 있음
    • 신뢰하려면 더 강한 언어 통합이 필요해 보이는 heuristic 기반 impact 분석도 많이 들어 있음
      • sem diff --verbose HEAD~4를 실행했을 때 실제로 바뀌지 않은 줄이 바뀐 것으로 표시되는 버그 출력도 만남
    • 80%쯤 끝난 가상적 유용 기능이 너무 많아 기반으로 쓰기에는 맞지 않았지만, 3개월 만에 이 정도를 만든 점은 높게 평가함
  • diffast는 2008년 학술 논문의 알고리듬을 바탕으로 AST의 tree edit-distance를 계산함
    • 전용 파서를 통해 Python, Java, Verilog, Fortran, C/C++를 지원함
    • example AST differences gallery가 잘 정리돼 있음
    • 정보를 tuple 형태로 내보내 datalog에 활용할 수 있음
  • autochrome는 Clojure 전용 diff 도구이며 dynamic programming을 사용함
    • 시각적 설명과 예시 walkthrough가 매우 좋음
  • Tristan Hume의 Designing a Tree Diff Algorithm Using Dynamic Programming and A*는 tree diff 알고리듬 설계 글로 참고 가치가 큼

원하는 워크플로와 최소 범위 계획

  • 주된 사용 사례는 LLM 출력의 턴별 리뷰이며, agent가 한 번에 1만 줄 넘는 코드를 마구 생성하게 두지 않음
  • agent에는 범위가 정해진 작업을 맡기고 몇 분 뒤 돌아와 전체 변경 개요를 본 다음, Emacs에서 직접 수정하거나 전부 버리고 다시 시도하거나, 아예 직접 다시 작성하고 싶어 함
  • 원하는 워크플로는 어떤 타입·함수·메서드가 추가·삭제·변경됐는지 상위 수준 개요를 먼저 보는 것임
  • 그 위에서 각 엔티티별 텍스트 diff를 빠르게 펼쳐 보고, 요약을 세부 diff로 자연스럽게 확장할 수 있어야 함
  • 또 변경을 다른 곳으로 이동하지 않고 바로 수정할 수 있어야 하며, diff 화면에서 file 화면으로 전환하지 않는 inline 편집을 원함
  • 지향점은 Magit의 변경 검토·staging 워크플로를 파일·줄 단위가 아니라 엔티티 단위로 옮기는 것임
  • 이번에 다시 떠올린 최소 범위 교훈에 맞춰, 먼저 Rust만 대상으로 treesitter 기반 엔티티 추출 프레임워크를 직접 급히 만들 계획임
  • 매칭은 우선 단순한 greedy 방식으로 시작하고, diff는 명령줄에 렌더링하려고 함
  • 이 정도가 특정 커밋에서 difftastic보다 나은 결과를 내면, 이후에는 Magit 같은 더 상호작용적인 Emacs 워크플로에 연결하려고 함
    • 가능하면 Magit 자체를 재사용할 가능성도 열어둠
    • 새 언어 지원은 필요할 때마다 추가하려고 함
    • 이후에는 단순 greedy 대신 점수 기반의 전역 매칭도 탐색할 수 있음
  • 충분히 만족스러우면 공개할 수도 있지만, GitHub star나 HN karma를 모으는 일은 목표가 아니며, 혼자 조용히 쓰는 도구로 남겨둘 수도 있음
  • 때로는 그저 선반 하나만 원할 때가 있다는 문장으로, 과한 확장 대신 필요한 것만 만드는 태도를 다시 묶어줌
Read Entire Article