


테스트를 먼저 써서, 다른 플랫폼을 깨뜨리지 않고 저수준 디버거 코드를 고친다
여러 OS와 백엔드에서 돌아가는 컴파일 언어용 디버거를 AI에게 고치게 할 때 쓴다. AI가 먼저 실패하는 테스트를 쓰고, OS별 코드를 별도 파일로 나누고, 공유 인터페이스를 바꿀 때 모든 백엔드를 확인하고, 커밋 메시지를 서브시스템 이름으로 시작하게 한다.
이 AI 규칙을 사용하려면
## 디버거를 고치기 전에, 그 동작을 맡은 계층을 읽는다
컴파일 언어용 디버거는 복잡한 다층 시스템이다. 이 안에서 효과적으로 작업하려면 디버깅 내부 구조, 바이너리 디버그 정보 포맷, OS별 프로세스 제어, 언어 런타임 내부 구조에 대한 이해가 필요하다.
모든 변경 사항을, 원시 속도보다 여러 플랫폼과 백엔드 전반에 걸친 정확성이 더 중요한 시스템을 다루는 일로 취급하라. 메모리 접근, 레지스터 처리, 백엔드 동작에서 발생한 미묘한 실수는 특정 플랫폼에서만 재현이 어려운 장애로 드러난다.
수정을 시작하기 전에 주변 계층을 읽고, 변경하려는 동작을 어느 추상화 계층이 소유하고 있는지 파악하라. 공유 인터페이스에 대한 변경은 그것을 구현하는 모든 백엔드로 파급되므로, 작업을 시작하기 전에 영향 범위를 가늠해야 한다.
## 코드를 바꾸기 전에 실패하는 테스트를 쓰고, 테스트를 건너뛰지 않는다
모든 코드 변경은 테스트 주도 개발(TDD)을 따라야 한다. 이것은 선택 사항이 아니다.
Red-Green-Refactor-Verify 사이클을 따르라:
1. **RED** - 먼저 실패하는 테스트를 작성한다. 올바른 이유로 실패해야 한다.
2. **GREEN** - 테스트를 통과시키는 데 필요한 최소한의 코드를 작성한다.
3. **REFACTOR** - 테스트를 계속 통과시키면서 코드를 정리한다.
4. **VERIFY** - 커밋하기 전에 전체 테스트 스위트를 실행한다.
필수 규칙:
1. 실패하는 테스트 없이 구현 코드를 먼저 작성하지 않는다.
2. 테스트 없이 코드를 커밋하지 않는다.
3. CI를 통과시키기 위해 테스트를 건너뛰거나 스킵 처리하지 않는다.
4. 기존 테스트를 비활성화하지 않는다 - 테스트를 고치거나 코드를 고친다.
5. 각 테스트는 하나의 구체적인 동작만 검증한다.
6. 테스트 이름은 테스트 대상 시나리오를 설명한다.
테스트를 먼저 작성하는 것이 어렵게 느껴진다면, 그 어려움은 변명이 아니라 신호다. 대개 함수가 너무 많은 일을 하고 있거나, 의존성이 지나치게 강하게 결합되어 있거나, 요구사항이 불명확하다는 뜻이다. TDD를 건너뛰지 말고, 그것이 드러낸 설계 문제를 고쳐라.
## 테스트 종류별 위치를 정하고, 매번 같은 결과가 나오게 한다
테스트가 있어야 할 위치:
- **단위 테스트**: 대상 코드와 같은 위치에 둔다.
- **통합 테스트**: 전용 엔드투엔드 테스트 디렉터리에 둔다.
- **플랫폼별 테스트**: 빌드 태그나 조건부 컴파일로 게이팅한다.
- **백엔드별 테스트**: 각 백엔드를 개별적으로 검증한다.
- **픽스처**: 미리 빌드된 바이너리가 아니라 테스트 실행 중에 컴파일되는 소스 형태로 유지하고, 경로 조회를 직접 만드는 대신 공유 픽스처 로케이터 헬퍼를 사용한다.
품질 기준:
- 구현이 아니라 동작을 테스트한다.
- 테스트당 하나의 단언(assertion)을 우선시해, 실패했을 때 원인이 하나로 좁혀지게 한다.
- 테스트 대상, 시나리오, 예상 결과를 밑줄로 구분한 패턴으로 명확하게 이름을 짓는다.
- 기존 테스트 유틸리티를 재사용하고 새로 만들지 않는다.
- 테스트는 결정적이어야 한다 - 임의값도, 경쟁 조건(race condition)도 없어야 한다.
- 출력 형식 테스트는 부분 문자열이 아니라 전체 예상 출력에 대해 단언한다. 전체 출력을 비교하면 여분의 공백, 필드 순서 변경, 형식 변경처럼 부분 문자열 검사로는 조용히 놓치는 회귀를 잡아낼 수 있다.
## 계층 사이에서 읽기 전용 인터페이스와 상태 변경 인터페이스를 나눈다
이 시스템은 관심사가 명확히 분리된 계층 구조로 이루어져 있다: 최상위의 CLI, 그 아래의 서비스/프로토콜 계층, 다시 그 아래의 고수준 연산, 그 아래의 프로세스/타겟 추상화, 그리고 최하위의 교체 가능한 백엔드 구현들(직접적인 OS 수준 프로세스 제어, 원격 프로토콜 클라이언트, 오프라인 덤프 분석, 커널 수준 트레이싱).
추상화 경계에서는 2단계 인터페이스 설계를 사용한다:
- 호출자가 상태를 관찰하는 데 쓰는 읽기 전용 공개 인터페이스.
- 상태를 변경하는 모든 연산을 담당하는 별도의 내부 인터페이스.
- 내부 인터페이스를 감싸는 상태 저장 래퍼 객체, 그리고 여러 타겟을 한 번에 관리하기 위한 그룹 추상화.
백엔드들은 내부 인터페이스 뒤에서 교체 가능하도록 구성된다. 이 인터페이스를 수정하면 그 변경은 보통 모든 구현에 적용되므로, 지금 눈앞의 백엔드 하나뿐 아니라 모든 백엔드를 고려해야 한다.
## OS별·아키텍처별 코드를 별도 파일로 나눈다
런타임 분기가 아니라 빌드 시스템이 선택하는, 파일명 기반 조건부 컴파일로 OS별·아키텍처별 코드를 격리하라 - OS별 소스 파일과 아키텍처별 소스 파일을 사용한다.
- 플랫폼 특화 코드를 범용 파일에 절대 넣지 않는다.
- 플랫폼 특화 코드는 실제로 그것이 필요한 백엔드(직접 OS 제어 백엔드와 원격 프로토콜 백엔드)에만 한정하고, 공유 핵심 로직에는 두지 않는다.
- 여러 아키텍처를 지원할 때, 레지스터·ABI·호출 규약 관련 세부사항은 아키텍처별 파일에 둔다.
이를 제대로 해내면 공유 핵심부는 이식 가능한 상태를 유지하고, 새 플랫폼을 추가하는 일은 범용 코드를 다시 쓰는 것이 아니라 파일을 추가하는 문제가 된다.
## 프로세스 메모리 읽기와 브레이크포인트 설정에서 흔한 실수를 피한다
1. **공유 상태를 직접 수정하지 않는다** - 락과 상태 일관성을 제공하는 내부 인터페이스 메서드를 거쳐야 한다.
2. **백엔드별로 연산 방식이 다르다** - 하나의 개념적 동작(예: 브레이크포인트 삽입)도 백엔드마다 구현 방식이 다르다. 단일한 메커니즘을 가정하지 마라.
3. **비슷해 보이는 추상화를 구분한다** - OS 스레드와 언어 수준의 동시성 단위는 같은 것이 아니다. 어떤 연산이 어느 쪽을 대상으로 하는지 파악해야 한다. 대부분의 연산은 원시 스레드가 아니라 언어 수준 단위에 작용한다.
4. **메모리 안전성** - 타겟 프로세스의 메모리를 읽을 때는 반드시 오류를 처리한다. 프로세스가 종료되었거나, 메모리가 언매핑되었거나, 주소가 유효하지 않을 수 있다.
5. **중복된 순회를 추가하지 않는다** - 이미 모든 단위(컴파일 단위, 심볼 등)를 순회하는 기존 패스가 있다면, 별도로 스캔하는 대신 그 패스에 단위별 검사를 추가한다.
6. **계측은 프롤로그 이후에 배치한다** - 브레이크포인트/프로브는 원시 진입점이 아니라 함수 프롤로그 이후에 설정한다. 진입점에 계측하면 런타임의 스택 체크 프롤로그가 재트리거될 수 있기 때문이다.
## 커밋 메시지를 서브시스템 이름으로 시작하고 바꾼 이유를 쓴다
서브시스템 기반 커밋 형식을 사용하라: 영향을 받은 서브시스템 이름을 콜론 뒤에 변경 내용을 짧게 요약한 제목 줄, 빈 줄, 변경 이유를 설명하는 본문 문단, 그리고 수정한 이슈를 참조하는 마무리 줄 순서로 작성한다.
- 제목 앞에 영향을 받은 서브시스템을 명시해, 이력을 영역별로 훑어볼 수 있게 한다.
- 제목 줄은 간결하게 유지하고 길이를 제한한다(약 70자 이내).
- 본문에서는 변경 이유를 설명하며, 줄바꿈을 지킨다(약 80자 기준).
- 변경의 계기가 된 이슈를 참조해, 각 변경이 그 원인과 연결되도록 한다.
구조화된 메시지는 변경 로그를 탐색 가능하게 유지하고, 어떤 변경이든 그 이유를 쉽게 재구성할 수 있게 해준다.
