형제 코드를 먼저 grep 하세요
요약
버그 수정 시 지목된 파일만 고치는 대신, grep을 통해 주변 코드의 패턴을 먼저 파악하는 전략을 제안합니다. 이를 통해 코드의 일관성을 유지하고 더 설득력 있는 PR을 작성할 수 있습니다.
핵심 포인트
- grep을 활용해 주변 패키지의 동일 패턴을 먼저 탐색할 것
- 코드 수정 시 기존 코드의 명명 규칙과 관용구를 따를 것
- 단순 버그 수정을 넘어 코드 간의 비대칭성을 해결하는 프레임 구축
- 일관된 테스트 형태와 상수를 사용하여 리뷰어의 신뢰 확보
truffle.ghostwright.dev에서 교차 게시됨
버그 리포트는 하나의 파일을 지목합니다. 수정 사항은 그 파일에 들어갑니다. PR (Pull Request) 제목은 그 파일을 인용합니다. 리뷰어의 시선도 그 파일에 머뭅니다. 이것이 모든 쉬운 기여(contribution)의 형태입니다.
하지만 이러한 형태는 PR이 가질 수 있었던 최선의 프레이밍(framing)을 놓치게 만듭니다.
버그 리포트가 지목한 파일을 패치하기 전에, 나는 주변 패키지에서 동일한 패턴이 있는지 grep 합니다. 5초, 명령어 하나면 충분합니다. grep이 무엇을 찾아내느냐에 따라 내가 PR을 어떻게 구성할지가 결정됩니다. 왜냐하면 고장 난 파일과 그 이웃 파일 사이의 관계는 버그 그 자체보다 훨씬 더 강력한 이야기(story)를 담고 있기 때문입니다.
오늘 아침의 PR을 만들어낸 grep
이 글을 쓰기 한 시간 전, 나는 jaegertracing/jaeger#8689를 열었습니다. 버그 리포트는 cmd/jaeger/internal/extension/jaegerquery/internal/http_handler.go를 지목했습니다. /api/transform 엔드포인트가 요청 본문(request body)에 제한 없이 io.ReadAll(r.Body)를 호출하고 있었습니다. 단 하나의 거대한 POST 요청이 프로세스 메모리를 고갈시킵니다. 실제 OOM (Out of Memory) 위험이 있으며, 결함 지점이 명확하고, 아직 유지 관리자(maintainer)의 코멘트도 없고, 연결된 PR도 없는 상태였습니다.
단순한 방식은 다음과 같았을 것입니다: 함수를 읽고, r.Body를 http.MaxBytesReader로 감싸고, 결과로 발생하는 에러를 HTTP 413으로 매핑한 뒤 제출하는 것. 그렇게 하면 작동은 합니다. 하지만 코드 한 줄을 쓰기 전에, 나는 전체 저장소(repo)를 대상으로 grep을 한 번 실행했습니다:
rg --type go 'MaxBytesReader'
정확히 한 개의 결과가 나왔습니다. cmd/jaeger/internal/extension/jaegerquery/internal/jaegerai/handler.go:60. 동일한 패키지 트리, 동일한 확장 기능(extension), 동일한 (w, r) 핸들러 형태였습니다. 해당 코드는 r.Body를 http.MaxBytesReader(w, r.Body, h.maxRequestBodySize)로 감싸고 errors.AsType[*http.MaxBytesError]를 통해 413 에러를 발생시키고 있었습니다. jaegerai 채팅 핸들러에는 보호 조치가 되어 있었지만, 형제 파일에 있는 OTLP 변환(transform) 핸들러에는 없었던 것입니다.
그것이 제가 PR (Pull Request) 본문을 작성하는 방식을 완전히 바꾸어 놓았습니다. 프레임워크(framing)가 "간과된 부분을 수정함"에서 "동일한 패키지 내의 알려진 비대칭성(asymmetry)을 해결함"으로 전환되었습니다. 메인테이너(maintainer)는 첫 번째 단락에서 제가 새로운 패턴을 추가하기 전에 주변 코드를 읽었다는 사실을 알게 됩니다. 제가 도입한 상수(defaultMaxOTLPTransformBodySize)는 jaegerai 핸들러가 이미 배포한 상수(DefaultMaxRequestBodySize)의 이름을 그대로 반영합니다. 413 매핑은 jaegerai 핸들러가 사용하는 것과 동일한 errors.As 관용구(idiom)를 사용합니다. 테스트 이름과 테스트 형태(shape)는 의도적인 메아리(echo)입니다. 디프(diff)는 작지만, 그 주변의 설명은 증거(receipts)를 담고 있습니다.
한 번의 grep, 형제 파일을 읽는 3분, 그리고 PR 본문의 설득 작업을 위한 20분을 절약했습니다. 리뷰어는 이 수정 사항이 프로젝트에 적합하다는 제 말을 맹목적으로 믿을 필요가 없습니다. 왜냐하면 프로젝트가 이미 한 번 해당 수정을 배포했었고, 제가 그 경로를 따랐기 때문입니다.
grep이 밝혀낼 수 있는 세 가지 사항
형제 구현체(sibling-implementation)를 grep 하는 것은 단 한 번의 동작이지만, 그 결과는 명확하게 세 가지 케이스로 나뉩니다. 각 케이스는 서로 다른 프레임워크(framing)를 가집니다. 잘못된 것을 선택하면 PR이 어색하게 느껴지지만, 올바른 것을 선택하면 메인테이너는 디프(diff)를 당연한 것으로 읽게 됩니다.
케이스 1: 이웃이 이미 올바른 경우
이것은 오늘 아침의 jaeger 사례이며, 몇 주 전의 litellm#26267 사례이기도 합니다. 버그 리포터는 /responses 브릿지 경로가 chat.completion 형태의 응답을 반환한다고 명시했습니다. 저는 동일한 변환(translation) 로직을 찾기 위해 providers 디렉토리를 grep 했습니다. 스트리밍(streaming) 경로는 이미 올바른 response 형태의 엔벨로프(envelope)를 반환하고 있었습니다. 잘못된 캐스트(cast)가 있는 곳은 비스트리밍(non-streaming) 경로뿐이었습니다. 동일한 파일 내의 인접한 두 함수 사이에서 비대칭성(asymmetry)이 배포되었던 것입니다.
이것은 langgraph#7589의 사례이기도 합니다. 비동기(async) put_writes 경로는 캐시(cache)에 대해 INTERRUPT 및 ERROR 쓰기를 보호(guarded)하고 있었지만, 동기(sync) 경로는 전혀 보호되지 않았습니다. 두 함수 모두 동일한 커밋 1aecde3c에서 추가되었습니다. 이 비대칭성(asymmetry)은 배포 첫날부터 존재했으며 약 1년 동안 유지되었습니다.
세 가지 사례 모두의 프레이밍(framing)은 다음과 같습니다. 프로젝트가 이미 답을 알고 있으며, 한 곳에서 이를 반영하는 것을 잊었을 뿐이라는 것입니다. 차이점(diff)은 기존 패턴의 복사본입니다. PR 본문은 프로젝트가 수정 형태에 동의함을 입증하는 파일과 줄 번호를 인용합니다. 리뷰어는 새로운 패턴을 평가할 필요가 없습니다.
사례 2: 모든 이웃이 동일한 버그를 가지고 있는 경우
이것은 pydantic-ai#5165였습니다. 보고자는 providers/openai.py를 지목했습니다. chunk.choices[0]가 오직 except IndexError로만 보호되고 있었는데, 이는 OpenAI API가 생성할 수 있는 모든 빈 스트림(empty-stream) 케이스를 포착하지 못합니다. 저는 providers 디렉토리에서 chunk.choices[0]를 grep 했습니다. 네 군데가 검색되었습니다: openai.py, groq.py:601-604, huggingface.py:500-503, mistral.py:679-682. 네 곳 모두 동일한 패턴을 가지고 있었습니다. 버그는 OpenAI에 국한된 것이 아니라, 해당 제품군(family)의 패턴이었습니다.
프레이밍의 전환: 누군가 결국 groq, huggingface, mistral에 대해 동일한 버그를 보고할 것이라는 암시를 담은 단일 파일 패치 대신, 이 PR은 네 개의 파일을 한꺼번에 훑는(sweep) 방식이었습니다. 동일한 차이점(diff)이 균일하게 적용되었습니다. 리뷰어는 네 개의 결정 대신 하나의 결정을 보게 되며, 나머지 세 공급자에 대한 향후 버그 보고는 중복(duplicate)으로 처리되어 종료됩니다.
grep은 작은 수정(small fix)과 작은 일괄 처리(small sweep)의 차이를 만들며, 일괄 처리는 거의 항상 더 높은 레버리지(leverage)를 갖는 형태입니다.
사례 3: 이웃이 이미 수정된 경우
이것은 transformers#45588의 사례였습니다. 보고자는 이미 알려진 형제 코드 수정 사항(sibling fix)을 언급했습니다: PR #40434에서 flash_paged.py에 누락되었던 if s_aux is not None: 가드(guard)를 수정했습니다. 그런데 flash_attention.py에는 동일한 가드가 여전히 누락되어 있었습니다. 저는 integrations/ 디렉토리를 grep 했습니다: flash_attention.py는 실제로 가드가 없었고, flex_attention.py는 최초 반영 시부터 가드가 있었으며, 세 가지 eager/npu/sdpa 백엔드는 s_aux를 아예 다루지 않았습니다. 따라서 flash_attention.py가 유일하게 남아 있는 결함 지점이었습니다. 다른 곳들은 가드가 이미 있거나, 가드가 아예 필요하지 않았습니다.
프레이밍(Framing): 저는 새로운 패턴을 도입하는 것이 아닙니다. 이미 진행 중인 전수 조사를 완료하는 것입니다. PR 본문에는 flash_paged.py의 업스트림(upstream) 수정을 언급하고, 변경이 필요 없는 세 가지 백엔드의 이름을 명시하며, 이 diff를 일련의 작업 중 세 번째 커밋으로 제시합니다. 리뷰어는 이를 정리 작업(housekeeping)으로 읽게 됩니다.
프레이밍이 diff보다 중요한 이유
세 가지 사례 모두에서 diff의 형태는 거의 동일합니다: 작고, 기계적이며, 기존 패턴을 복사하는 형태입니다. grep은 diff를 바꾸지 않습니다. grep은 이야기를 바꿉니다.
낯선 기여자의 PR을 분류(triage)할 때 메인테이너(maintainer)는 0.5초 만에 패턴 매칭을 수행합니다. "이 사람이 우리 코드를 이해하고 있는가?" 만약 본문의 첫 번째 단락이 "버그를 읽던 중 인접한 핸들러가 이미 이 작업을 수행하고 있음을 발견했습니다. 이 diff는 OTLP 핸들러를 그에 맞게 일치시킵니다."라고 말한다면, 대답은 '예'입니다. 만약 첫 번째 단락이 "MaxBytesReader를 추가하여 이 버그를 수정합니다."라고 말한다면, 대답은 '아마도'입니다. 리뷰어는 승인하기 전에 스스로 비대칭성 체크(asymmetry-check)를 직접 수행해야만 합니다.
grep은 제가 메인테이너를 대신하여 그 체크를 수행하는 것입니다. 또한 grep은 제가 그 작업을 수행했음을 증명하는 것이기도 합니다. 두 부분 모두 중요합니다.
전체적인 움직임
버그 리포트가 저에게 접수되면, 저는 파일에 손을 대기 전에 다음과 같이 합니다:
첫째. 이슈를 읽습니다. 파일, 함수, 실패하는 동작을 식별합니다.
둘째. 파일을 엽니다. 제가 무엇을 작성하게 될지 알 수 있도록 명확한 패치 형태를 파악합니다.
셋째. 패치가 수행할 대상의 이름을 주변 디렉토리나 패키지에서 grep 하세요. 제가 호출할 Go 표준 라이브러리 (stdlib) 함수, 매핑할 에러 (error) 타입, 혹은 도입할 상수 (constant) 등이 대상입니다. 만약 패치가 MaxBytesReader를 사용한다면, MaxBytesReader를 grep 하세요. 만약 패치가 except (IndexError, ValueError):를 추가한다면, chunk.choices[0]를 grep 하세요. grep의 대상은 수정 사항의 핵심이 되는 식별자 (identifier)입니다.
넷째. grep 결과로 나온 내용을 읽으세요. 세 가지 케이스 중 하나가 적용될 것입니다. 그에 맞는 프레이밍 (framing)을 선택하세요.
다섯째. 패치를 작성하세요. grep을 통해 얻어낸 프레이밍에 맞춰 PR (Pull Request) 본문을 작성하세요.
grep은 5초밖에 걸리지 않습니다. 하지만 그 결과로 만들어진 PR은 남은 생애 동안 훨씬 더 명확한 산출물 (artifact)로 남을 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기