검색/큰 코드베이스에서 근본 원인을 고치고 공개 API 호환성 지키기

큰 코드베이스에서 근본 원인을 고치고 공개 API 호환성 지키기

AI가 크고 계층화된 코드베이스를 고칠 때 씁니다. 빌드 로그를 짧게 줄이고, 버그를 상류의 원인까지 따라가 고치며, 자기 변경분에 문제를 덮기만 하는 헬퍼가 없는지 살피고, 알기 쉬운 풀 리퀘스트를 씁니다.

이 AI 규칙을 사용하려면
## 빌드 로그를 짧게 줄이고 커밋 전에 포매터와 테스트 돌리기 - 빌드할 때는 모든 빌드 출력을 널 디바이스로 리다이렉트하고, 실패했을 때만 동일한 명령을 상세 출력 모드로 다시 실행한다. 이렇게 하면 성공한 빌드가 로그 출력에 토큰을 낭비하지 않는다. 예: `build-command >/dev/null 2>&1 || build-command`. - 네트워크에서 가져오는 대신 로컬에 캐시된 의존성을 우선 사용하고, 가능하다면 빌드 캐시나 컴파일러 캐시를 활성화해 증분 빌드 속도를 높인다. - 장시간 사람이 지켜보지 않거나 에이전트가 주도하는 빌드에서는, 실행이 사람의 응답을 기다리며 멈추지 않도록 구성 단계에서 모달 중단이나 어서션 대화상자를 비활성화한다. - 커밋할 때마다 포맷터를 실행하고, 파일을 수정하지 않고 검증만 하는 체크 전용 모드를 활용한다. 프로젝트 스타일을 따르지 않는 코드는 피할 수 있었던 리뷰 왕복을 강제로 만들어낸다. - 테스트 스위트는 저장소 루트에서 실행하고, 회귀 테스트는 프로젝트의 테스트 디렉터리 아래에 파일로 추가한다. GPU나 특수 하드웨어를 사용할 수 없을 때는 커버리지를 건너뛰는 대신 CPU나 인터프리터 테스트 모드를 사용한다. - 예를 들어 유닉스 셸에서 호스트 도구를 호출하는 등 크로스 셸 환경에서는, 잘못된 플랫폼용 바이너리가 아니라 의도한 호스트 바이너리가 실행되도록 올바른 실행 파일 확장자를 붙인다. ## 가드로 덮지 말고 상류의 진짜 원인 고치기 - 가능한 가장 작은 diff보다 더 큰 재작업이 필요하더라도, 구조적으로 올바르고 견고한 표현을 목표로 한다. - 증상이 아니라 근본 원인을 고친다. 출력 생성처럼 후반 단계에서 버그가 드러나더라도, 원인은 대개 그보다 앞선 상류 단계, 즉 이전의 변환, 정규화, 특수화, 로우어링, 혹은 데이터 표현 자체에 있다. 그 지점까지 추적해서 거기서 고친다. - 변경을 유지하기 전에 매번 질문한다. 왜 필요한가, 이 변경이 없으면 어떤 테스트가 실패하는가, 이것이 올바른 수정인가 아니면 문제가 표현 자체의 결함을 나타내는 신호인가. 변경 없이 실패하는 테스트를 하나도 댈 수 없다면, 그 변경은 아마 존재하지 말아야 한다. - 문제를 가리지 않는다. 잘못된 구조를 덮어버리는 가드, null 체크, 특수 케이스는 표현상의 버그를 숨기는 것이며, 올바른 입력에서는 절대 실행되지 않는 가드는 죽은 코드다. 하위 소비자 코드가 단순하게 유지되도록 표현 자체를 올바르게 만든다. - 입력의 형태를 캐묻는다. 특정 형태의 입력을 처리할 때마다, 그 형태 자체가 올바른지, 아니면 상류의 생성 지점을 고쳐야 하는지 질문한다. 그 형태가 진짜로 유효한 입력일 때만 그 자리에서 처리한다. - 편집 거리를 줄이는 것보다 올바른 표현을 우선한다. 두 표면 형태가 동등해야 한다면 동일하게 모델링하고, 개념적으로 순서가 없는 키-값 데이터는 위치나 인덱스가 아니라 키나 역할로 접근한다. - 작업 전체에 걸쳐 작업 기록을 남긴다. 문제와 그것을 보여주는 예시, 막힌 지점, 하나의 수정이 다음 수정으로 어떻게 이어지는지, 선택한 수정과 그것이 원칙에 맞는 이유, 그리고 기각한 대안들이다. 이를 변경 설명으로 압축하되 커밋되는 코드에는 남기지 않는다. ## 내 변경분에 잘못된 설계를 덮기만 하는 헬퍼가 없는지 살피기 - 사소하지 않은 변경을 확정하기 전에, 그 diff가 잘못된 표현을 보완하고 있는 징후는 없는지 검토한다. 이런 패턴이 이 계층에 속한다는 것을 증명하기 전까지는 위험 신호로 취급한다. - 새로운 재귀적 동등성 비교 함수나 일치 여부 판단 함수 같은 커스텀 의미적 동등성 헬퍼는, 흔히 서로 다른 두 표현이 나란히 살아남도록 방치되었음을 의미한다. 먼저 기존의 정규화, 치환, 동등성 비교 경로가 왜 이미 그것들을 동일하게 만들지 않는지 질문한다. - 검토되지 않은 헬퍼의 증식. 새로 추가되는 모든 헬퍼, 폴백, try 스타일 함수는 검토 대상으로 취급한다. 특히 실패하는 테스트 하나를 통과시키기 위해서만 존재하거나 기존 메커니즘의 일부를 재구현하는 함수는 더욱 그렇다. - 이미 검증된 의미 데이터로부터 표면 구문을 다시 만들어내는 의미-구문 재구성은, 대개 생성 지점이 잘못된 표현을 보존했다는 뜻이다. 검증된 의미 값 자체가 진실의 원천이어야 한다. - 피연산자, 참조, 소유권 체인을 따라가며 인수나 키, 부모를 다시 찾아내는 그래프 순회 기반 컨텍스트 재발견은, 대개 하류에서의 땜질이다. 대신 생성 지점에서 정본 형태를 만들어야 한다. - 소비 측 패치. 후반 단계가 이전 단계에서 넘어온 잘못된 형태를 패치해서는 안 되며, 특정 서브클래스에 대한 특수 케이스, 매직 네임, 인수 인덱스, 항목 순서 같은 하드코딩된 표현상의 세부사항은 강한 불변식을 필요로 하며 대개 정본을 구성하는 경계 지점에 속해야 한다. - 불가능한 형태를 조용히 처리하는 것. 계약 범위를 벗어난 형태에 대해 기본값을 반환하는 가드는 버그를 숨긴다. 그런 형태를 조용히 받아들이는 대신 어서션으로 걸러낸다. - 검토는 의심스러운 헬퍼 목록을 작성하는 것으로 시작한다. 새로 추가된 헬퍼마다 어떤 기존 메커니즘과 겹치는지, 그것이 없으면 어떤 테스트가 실패하는지 나열한다. 지적된 각 변경에 대해 정확한 입력 형태와 그 생성 지점을 명시하고, 그 형태가 정본인지 우연히 생긴 대안인지 판단하며, 생성 지점 쪽의 수정을 먼저 시도하고, 기존의 진실의 원천을 명시한 뒤, 되돌리기 훈련을 한다. 즉 변경을 제거하고 가장 작은 실패 테스트를 실행해 실제로 무엇이 깨지는지 추적한다. 단지 테스트를 통과시킨다는 이유만으로 지적된 변경을 그대로 남겨두지 않는다. ## 예시가 있는 주석을 쓰고 같은 정보는 한 곳에만 두기 - 함수 주석은 완전한 문장으로 작성한다. 먼저 그 함수가 무엇을 하는지 서술하고, 존재 이유가 명확하지 않다면 그다음에 밝히며, 사소하지 않은 동작에는 짧은 단편 대신 구체적인 예시를 포함한다. - 주석과 설명에는 대화체 예시를 사용한다. 추상적인 라벨을 설명 대신 쓰기보다는, 실제 코드와 함께 "이런 예를 생각해 보자:"라는 식으로 시작해, 무슨 일이 일어나는지, 어느 패스가 그 형태를 만드는지, 그 지역 코드가 어떤 불변식을 지키는지, 어느 소비자가 그것에 의존하는지를 단계별로 설명한다. - 작성하기 전에 재사용 가능한 것부터 찾고, 사소하지 않은 로직은 이름이 붙고 문서화된 헬퍼로 추출한다. 먼저 공유 유틸리티를 검색하고, 로직이 정말 새로운 것이라면 다단계 계산을 인라인 블록에 묻어두는 대신 의도가 드러나는 이름과 문서 주석을 붙인다. - 진실의 원천을 하나로 유지하고, 리팩터링 후에는 죽은 코드를 삭제한다. 주어진 대상은 정확히 한 곳에서만 매핑하거나 분류하며, 변경으로 인해 도달할 수 없게 된 분기나 헬퍼는 남겨두지 말고 제거한다. - 값마다 하나의 정본 표현만 유지하고 그 불변식을 어서션으로 확인한다. 동일한 논리적 값에 대한 두 번째 표현은 동등성, 아이덴티티, 중복 제거를 깨뜨린다. 특정 입력에 대해 어떤 형태가 절대 생성되지 않는다는 불변식이 보장될 때는 생성 지점에서 어서션으로 확인한다. - 계약을 벗어난 입력에는 요란하게 실패한다. 헬퍼가 제한된 입력 집합에서만 유효하다면, 그 집합을 벗어나는 것에 대해 조용히 기본값을 반환하는 대신 어서션으로 걸러낸다. ## 회귀 테스트를 넣고 다섯 부분으로 풀 리퀘스트 설명 쓰기 - 코드를 포맷팅한 다음, 변경 사항에 기본적으로 비호환성 없음(non-breaking) 표시를 하고, 바이너리 인터페이스나 언어 동작을 바꾸는 경우에는 호환성 깨짐(breaking)으로 표시한다. - 수정 전에는 실패했을 회귀 테스트를 포함한다. - 설명은 정해진 다섯 항목 구조로 작성한다. 동기(구체적인 예시와 함께 제시하는 문제), 제안하는 해결책(접근 방식과 그것이 원칙에 맞는 이유), 변경 요약(손댄 파일이나 영역과 각각이 하는 일의 목록), 개념과 용어(잘 알려진 기본 용어가 아니라 보고서가 의존하는 코드베이스 고유의, 혹은 미묘한 용어만 담은 짧은 용어집), 그리고 진행 과정 보고서다. - 진행 과정 보고서에서는 모든 변경을 논리적인 이유와 함께 설명한다. 연쇄적으로 발생한 문제라면, 그 원인이 된 테스트와 관련된 정확한 함수들을 명시한 코드 추적을 제시하고, 단순한 설명만으로 끝내지 않는다. 입력 형태를 가드하거나 특수 케이스로 처리하는 변경에는, 그 형태가 유효한 입력인지 아니면 생성 지점을 고쳤어야 했는지를 밝혀, 리뷰어가 수정이 올바른 계층에 있는지 확인할 수 있게 한다. - 당신과 같은 맥락을 갖고 있지 않은 리뷰어를 위해 작성한다. 구체적인 예시로 시작하고, 타입이나 함수 이름만 대는 대신 관련 코드 조각을 포함하며, 단계를 순서대로 설명한다. ## enum과 인터페이스는 끝에만 추가해 공개 API와 ABI 깨지 않기 - 공개 API 헤더에 대한 모든 변경은, 이미 이전 버전을 기준으로 컴파일된 호출자를 위해 바이너리 호환성과 소스 호환성을 지켜야 한다고 취급한다. - 열거형(enum): 기존 열거형 중간에 새 열거자를 삽입하지 않는다. 삽입하면 그 뒤의 모든 값이 밀리면서, 그 값을 저장하거나 비교하던 호출자를 조용히 망가뜨리기 때문이다. 새 열거자는 종단 카운트나 센티널 멤버 바로 앞에, 다음 순번 값을 명시해서 추가한다. 열거자를 폐기할 때는 원래의 정수 값은 유지한 채 제거 표시가 된 이름으로 바꾸고, 폐기된 값을 다시 재사용하지 않는다. - COM 스타일 인터페이스의 가상 메서드 테이블은 선언 순서로 레이아웃이 고정된다. 따라서 가상 메서드의 순서를 절대 바꾸지 않고, 메서드의 시그니처나 호출 규약을 절대 바꾸지 않으며, 새 메서드를 중간에 절대 삽입하지 않고(닫는 중괄호 앞 맨 끝에 추가한다), 메서드를 절대 제거하지 않는다. 대신 본문을 미구현 스텁으로 바꾸고 선언은 그대로 유지한다. - 클라이언트가 해당 인터페이스를 구현하거나 식별자로 조회할 수 있는 경우, 기존 공개 인터페이스를 그 자리에서 확장하지 않는다. 대신 자체 식별자를 가진 새로운 파생 인터페이스나 버전이 명시된 인터페이스를 추가하고, 기존 인터페이스는 기존 호출자를 위해 계속 지원한다.