


AI가 시스템 코드를 타입과 오류 처리까지 꼼꼼하게 작성하게 하기
엄격한 시스템 코드베이스에서 AI의 도움을 받을 때 사용합니다. AI가 작성한 시간의 세 배 이상 코드를 검토하고, 모든 오류 경우를 다루며, 규칙을 타입에 담고, 새 데이터 형식과 옛 형식이 서로 맞게 하며, 커밋마다 빌드가 되는 작은 커밋을 만듭니다.
이 AI 규칙을 사용하려면
## AI가 쓴 코드를 작성 시간의 세 배 이상 들여 검토하기
어시스턴트를 속도 배율기가 아니라 품질 배율기로 취급한다. 그렇게 절약된 시간은, 도움 없이 하는 작업이 도달할 수 있는 수준을 넘어서는 엄밀함에 쓴다.
- 탁월함을 목표로 한다: 더 많은 엣지 케이스를 다루고, 이해하기 쉽도록 리팩터링하며, 남아 있는 TODO를 정리하고, 번거로운 일들을 해내고, 버그 0을 목표로 한다.
- 생성하는 시간보다 훨씬 오래 리뷰한다. 생성된 코드는 그것을 만드는 데 걸린 시간의 최소 3배를 리뷰에 쓰면서, 모든 줄과 모든 설계 결정을 따져보고 그것을 깨뜨릴 방법을 적극적으로 찾는다.
- 결과의 소유자는 작성자다. 방향에 대한 피드백만 명시적으로 요청하는 경우가 아니라면, 초안을 완성된 작업물로 넘기지 않는다.
- 주의 깊은 사람의 리뷰였다면 잡아냈을 케이스가 빠져 있는 등, 신경 쓰지 않고 만들어진 흔적이 보이는 작업물은 수정을 거치지 않고 그대로 거절될 수 있다.
- 코드 변경이 관련된 대화의 시작 시점에, 그리고 풀 리퀘스트를 열기 직전에 다시 이 기준을 선언해, 작성자와 리뷰어가 같은 기준을 공유하게 한다.
## 정확성을 타입에 맡기고 모든 엣지 케이스 다루기
편리한 구현보다 올바른 구현을 택하고, 가능한 한 많은 정확성을 타입 시스템으로 밀어 넣어, 실수가 프로덕션에서 실패하는 대신 컴파일에서 실패하게 한다.
- 오류 공간 전체를 모델링한다. 지름길도 없고, 단순화된 오류 처리도 없다.
- 경쟁 조건, 시그널 타이밍, 운영체제 간 동작 차이를 포함해 모든 엣지 케이스를 처리한다.
- 정확성 제약을 타입에 담는다: 도메인 값을 위한 래퍼 타입, 복잡한 생성을 위한 빌더 패턴, 상태 전이가 중요한 경우 제네릭을 이용한 타입 상태, 그리고 복제 대신 빌림.
- 선택의 여지가 있는 곳에서는 어디서든 런타임 검사보다 컴파일 타임 보장을 택한다.
- 가시성은 가능한 한 좁게 유지한다. 모듈 바깥의 무언가가 정말로 필요로 할 때에만 항목의 가시성을 넓힌다.
- 안정적이고 외부에서 소비되는 패키지의 공개 타입은 상위 호환을 위해 확장 가능하도록 표시한다. 안정성을 약속하지 않는 내부 패키지에는 이것이 필요 없지만, 오류 타입은 그래도 이것을 원할 수 있다.
- 공유 가변 상태 대신 메시지 전달이나 액터 모델을 사용해, 데이터 경쟁이 생길 수 없게 한다.
- 빠르게 끝내는 것보다 세부 사항을 정확히 맞추는 것이 더 중요하다.
## 도움이 되는 오류 메시지를 주고 모든 OS에서 같은 동작 유지하기
사용자에게 보이는 동작은 마무리 손질이 아니라 설계 입력이다.
- 맨 오류 문자열이 아니라 구조화되고 도움이 되는 진단을 만들고, 메시지가 사람에게 닿는 곳에서는 어디서든 풍부한 진단 라이브러리를 사용한다.
- 진행 상황 보고는 반응성 있고 정보를 주도록 만든다.
- 기반 기능이 서로 다른 곳에서도 플랫폼 간 동작을 일관되게 유지하고, 한 플랫폼을 다른 플랫폼 위에서 흉내 내지 말고 각 플랫폼을 그 플랫폼의 네이티브 메커니즘으로 구현한다.
- 사용자에게 보이는 메시지는 명확한 현재 시제로 쓴다: "the tool now supports"라고 쓰고, "the tool now supported"라고 쓰지 않는다.
- 복수형 처리는 공유 헬퍼 모듈에 집중시키고, 그 모듈의 함수들이 개수에 맞는 단수형 또는 복수형을 반환하게 한다. 호출 지점에 복수형 처리 로직을 인라인으로 넣지 말고 새 헬퍼를 그 모듈에 추가한다.
## 설계를 조금씩 키우고 선택한 이유를 설계 문서에 남기기
설계는 추상적이고 지나치게 일반적인 것이 아니라, 구체적이고 조합 가능하게 유지한다.
- 처음부터 완벽한 아키텍처를 잡으려 하지 말고 설계를 점진적으로 발전시킨다.
- 설계 결정과 그 트레이드오프를 프로젝트와 함께 보관되는 설계 문서에 기록해, 나중에 읽는 사람이 왜 그런 형태를 골랐는지 알 수 있게 한다.
- 확신이 서지 않을 때는 추측을 추상화로 굳히지 말고 탐색하고 반복한다.
## 사용자 원인 오류와 내부 버그를 나누고 자세한 맥락 붙이기
오류는 사용자가 보는 표면의 일부이며, 성공 경로와 똑같은 설계상의 주의를 받을 자격이 있다.
- 문자열로 표현된 오류 대신 derive 기반 오류 타입을 사용하고, 집합이 커지면 관련된 오류들을 종류(kind) 열거형 아래로 묶는다.
- 세부 정보를 하나의 메시지로 뭉개지 말고, 오류에 풍부하고 구조화된 컨텍스트를 실어 나른다.
- 2계층 모델을 사용한다. 사용자로 인한 오류와 외부 오류는 의미론적 종료 코드로 매핑되는 별개의 타입을 이루고, 내부 프로그래밍 오류는 그와 분리되며 패닉해도 된다.
- 오류 표시 텍스트는 "failed to ..." 같은 감싸는 문구에 넣었을 때 올바르게 읽히는, 소문자로 시작하는 문장 조각으로 쓴다.
## 설정의 알 수 없는 항목을 경고하고 설정 오류를 알기 쉽게 하기
역직렬화 결정이, 사용자가 앞으로 보게 될 모든 설정 오류의 품질을 좌우한다.
- 설정에서 무시되거나 알 수 없는 경로를 추적하고 보고해, 오타가 조용히 아무 일도 하지 않는 대신 경고로 드러나게 한다.
- 역직렬화 중에 중첩 구조를 절대 평탄화하지 않는다. 평탄화에 필요한 내부 버퍼링이 알 수 없는 필드 경고를 저하시킨다. 대신 필드들을 구조체로 명시적으로 복사한다.
- 태그 없는 배리언트 역직렬화에 절대 의존하지 않는다. 그것은 형편없는 오류 메시지를 낳는다. 대신 정확한 기대 문자열을 갖는 커스텀 비지터를 작성한다.
## 저장·통신 형식을 바꾸기 전에 옛 버전과 새 버전이 서로 읽고 쓰는지 확인하기
디스크에 기록되거나 네트워크로 전송되는 구조에 대한 모든 변경은, 반영되기 전에 호환성 매트릭스 전체를 따라 추적해야 한다.
- 새 데이터를 다루는 옛 리더: 여전히 역직렬화할 수 있는가, 그리고 정보를 잃지는 않는가?
- 옛 데이터를 다루는 새 리더: 선언된 기본값이 단지 유효하기만 한 값이 아니라 올바른 값을 만들어 내는가?
- 새 데이터를 다루는 옛 라이터: 필드를 누락하지 않고 왕복할 수 있는가? 이것이 가장 자주 놓치는 경우다.
- 포맷 버전은 선제적으로 올린다. 새 필드가 의미상 중요해질 것이라면, 기본값이 아닌 값이 처음 기록될 때가 아니라 그 필드를 추가하는 시점에 버전을 올린다. 그러지 않으면 더 오래된 빌드가 데이터를 다시 써 넣을 때 조용히 데이터를 망가뜨린다.
- 기본값은 필요조건이지만 충분조건은 아니라고 여긴다. 기본값은 옛 리더가 새 데이터를 파싱할 수 있게 해 주지만, 옛 라이터는 여전히 자신이 모르는 필드를 누락시킨다.
## 비동기는 필요한 곳에만 쓰고 작업을 무제한으로 띄우지 않기
비동기는 입출력과 동시성을 위한 도구이지, 기본 스타일이 아니다.
- 멀티스레드 비동기 런타임을 사용하되, 비동기는 그것이 필요한 코드에만 가두고 나머지는 전부 동기로 유지한다.
- 구조적 동시성을 사용해, 동시에 진행되는 작업에 인위적으로 긴 수명을 주지 않아도 되게 한다.
- 대기열에 들어간 작업은 제한 없이 스폰하지 말고 백프레셔를 인식하며 스케줄링한다.
- 잡 제어가 프로세스를 일시 중단하고 재개할 수 있는 곳에서는 일시 정지 가능한 타이밍 프리미티브를 제공해, 중단된 시간이 타임아웃에 산입되지 않게 한다.
## 집계용 모듈에 로직을 두지 않고 임포트는 맨 위에 쓰기
모듈 경계는 관례만이 아니라 구조와 가시성으로 강제된다.
- 집약 모듈 파일은 공개 항목을 다시 내보내는 용도로만 사용한다. 사소하지 않은 로직은 구현 서브모듈이나 더 구체적인 모듈에 속한다.
- 플랫폼별 코드는 조건부 컴파일 뒤의 플랫폼별 파일들로 분리해 두고, 그 사이에 공유 인터페이스를 둔다.
- 테스트 헬퍼는 전용 모듈이나 파일에 둔다.
- 타입과 함수는 모듈의 맨 위에서 임포트한다. 함수 본문 안에서는 절대 임포트하지 않는다. 유일한 예외는 게이트된 임포트가 필요한 조건부 컴파일 함수다.
- 타입 자체를 임포트하는 쪽을 택하고, 모듈을 대신 임포트하는 것은 그것이 관례적인 표기인 곳에서만 한다. 시그니처에서 완전 정규화 경로는 피한다: 부모 모듈을 임포트하고 그것만으로 한정한다.
- 패턴 매칭을 위해 열거형 배리언트를 임포트하는 것은 괜찮다.
## 데이터를 복사하지 말고 빌리거나 공유하기
나중에 최적화하기보다, 작업 자체를 피하는 표현을 고른다.
- 불변 데이터는 복사하지 말고 참조 카운팅이나 빌림으로 공유한다.
- 짧은 문자열을 많이 다루는 코드에서는 작은 문자열에 최적화된 타입을 사용한다.
- 복제와 참조에 주의를 기울인다. 빌림으로 이미 표현되는 자연스러운 트리 구조나 소유권 구조를 데이터가 갖고 있는 곳에서는 복제를 피한다.
- 가능한 곳에서는 데이터를 전부 메모리에 버퍼링하지 말고 이터레이터로 스트리밍한다.
## 커밋 전마다 포맷을 돌리고 의존성 버전을 한곳에서 관리하기
기계적인 일관성은 강제하기에는 싸지만, 나중에 소급 적용하기에는 비싸다.
- 모든 소스 파일은 프로젝트의 저작권 줄과 라이선스 식별자로 시작한다.
- 언어 에디션을 고정하고, 커밋할 때마다 그 전에 프로젝트의 포매팅 명령을 실행한다. 포매팅은 지속적 통합에서 강제되므로, 포맷되지 않은 커밋은 거기서 실패한다.
- 린트를 억제할 때는 허용하는 형태가 아니라 기대하는 형태를 사용해, 해당 린트가 더 이상 발생하지 않게 되면 억제 자체가 한 번 경고를 내고 죽은 억제가 쌓이지 않게 한다.
- 어떤 린트가 문자열에 포맷된 텍스트를 덧붙이는 것을 금지하는 곳에서는, 포맷된 문자열을 밀어 넣는 대신 프로젝트의 쓰기 매크로를 사용한다.
- 모든 의존성 버전은 워크스페이스 루트에서 중앙 집중식으로 관리하고, 내부 패키지는 정확한 버전으로 고정한다.
- 자명하지 않은 의존성 및 기능 선택에는 주석을 단다. 특히 보안이나 크기를 이유로 어떤 기능을 비활성화한 곳에는 반드시 단다.
## 주석에는 무엇을 하는지보다 왜 그렇게 하는지 쓰기
- 주석은 무엇이 아니라 왜를 설명한다.
- 함수 본문 안에서 서술하듯 늘어놓지 않는다. 자명하지 않거나 특별한 부분, 또는 의도에 더 깊은 설명이 필요한 곳에만 주석을 단다.
- 모듈 수준 문서는 그 모듈의 목적과 책임을 밝힌다.
- 코드 주석은 항상 마침표로 끝낸다.
- 헤딩과 제목에는 타이틀 케이스를 절대 쓰지 않는다. 항상 문장 케이스를 쓴다.
- 옥스퍼드 콤마를 항상 사용한다.
- 관사를 생략하지 않는다. "the file has a newer version"이라고 쓰고, "file has newer version"이라고 쓰지 않는다.
## 프로젝트 지정 테스트 러너로 실행하고 기존 픽스처 재사용하기
프로젝트가 테스트를 실행하는 방식대로 테스트하고, 이미 존재하는 테스트 모델을 재사용한다.
- 단위 테스트와 통합 테스트에는 프로젝트가 지정한 테스트 러너를 사용한다. 스위트가 지정된 러너의 실행 모델에 의존할 때, 언어의 기본 러너로 물러나지 않는다.
- 로컬에서 빌드한 도구 사본은 새 플래그나 출력 포맷처럼 도구 자체의 동작 변경을 검증할 때에만 사용한다. 통합 테스트는 빌드 산출물로부터 자신의 내부 프로세스를 스폰하므로, 스위트 자체는 표준 러너로 실행한다.
- 메인 러너가 문서 테스트를 지원하지 않을 때는, 그것을 지원하는 명령으로 문서 테스트를 실행한다.
- 단위 테스트는 그것이 테스트하는 코드 옆에, 통합 테스트는 전용 패키지에, 공유 픽스처는 픽스처 패키지에, 테스트 유틸리티는 지원 패키지에 둔다.
- 손으로 쓴 스폿 체크보다 픽스처 패키지의 기대 결과 모델을 우선한다.
- 이미 존재하는 도구에 먼저 손을 뻗는다: 매개변수화된 케이스, 속성 기반 테스트, 스냅샷 어서션, 커스텀 하네스, 개선된 어서션 출력. 새 메커니즘을 추가하기 전에 무엇이 있는지 확인한다.
## 커밋 제목에 범위를 붙이고 모든 커밋이 빌드되게 하기
히스토리는 리뷰 대상 산출물이므로, 각 커밋을 리뷰의 단위로 취급한다.
- 각 커밋 제목 앞에 그 커밋이 건드리는 스코프를 대괄호로 붙이고, 그 뒤에 짧은 설명을 쓴다. 툴체인 최소 버전, 릴리스, 지속적 통합 변경처럼 여러 영역에 걸친 작업에는 메타 스코프를 사용한다.
- 버전 올림 커밋과 릴리스 준비 커밋은 자체의 고정된 제목을 따르며, 보통 릴리스 도구가 만들어 낸다.
- 본문은 단순 과거 시제와 현재 시제로 쓴다: 예전에는 무슨 일이 일어났는지, 지금은 무슨 일이 일어나는지, 어떤 테스트가 추가되었는지.
- 커밋 메시지는 마크다운이다. 제목 줄에는 백틱을 쓰지 않되, 본문에서는 쓴다.
- 커밋은 원자적으로 만든다. 각 커밋은 하나의 논리적 변경 단위다.
- 히스토리는 이분 탐색 가능하게 유지한다. 모든 커밋은 빌드되어야 하고 모든 검사를 통과해야 한다.
- 관심사를 분리한다. 포매팅 수정과 리팩터는 동작 변경과 따로, 자기만의 커밋에 속한다.
## 동시 실행 러너를 디스패처와 실행기로 나누고 메시지로 잇기
동시성 러너는 한 컴포넌트가 바깥 세계를 소유하고 다른 컴포넌트가 실행을 소유할 때 가장 이해하기 쉽다.
- 디스패처는 바깥 세계와 상호작용하고, 모든 소스에서 오는 이벤트를 선형화하며, select 구문으로 그 소스들을 다중화하고, 현재 무엇이 실행 중인지에 대한 권위 있는 상태를 보유한다. 동기화 지점들이 보고를 경쟁 상태로부터 자유롭게 유지한다.
- 실행기는 작업 단위들을 비동기 상태 기계로 스케줄링하고 실행하며, 재시도, 타임아웃, 프로세스 스폰, 출력 캡처를 처리한다. 각 단위는 자기 상태에 대한 진실의 원천이며, 브로드캐스트가 아니라 디스패처로 향하는 전용 채널을 갖는다.
- 상태를 직접 공유하지 않는다. 모든 것은 메시지로서 컴포넌트 사이를 오간다.
- 실패 공간 전체를 모델링하고, 일시 정지 가능한 타이머를 사용해 중단 시그널과 재개 시그널이 타이밍을 왜곡하지 않게 한다.
- 각 플랫폼의 네이티브 프로세스 격리 및 시그널 기능을 사용하고, 조건부 컴파일과 공유 인터페이스 뒤의 플랫폼별 모듈로 그것을 보호하며, 플랫폼 차이와 트레이드오프를 주석에 문서화한다.
