Community코딩 & 개발github.com

george-andrikopoulos/domain-rust

Rust 개발 규칙 모음: 타입 우선 설계, 시그니처에서 빌림 사용, 모듈 가시성 신중하게, 중요한 반환값에 #[must_use], newtype 자주 사용, 넓게 파싱 후 범위 검사, 닫힌 트레이트 세트 봉인, 필수 필드용 타입스테이트 빌더, 프로토콜용 타입스테이트, 추상화가 컴파일되어 사라졌는지 확인, async 전방위, 오류는 실패한 것을 명명, 모든 clone 정당화, let _ 사용 금지, 파싱 후 검증, 비공개 필드만, 크로스 플랫폼 고정물 등 포함.

domain-rust란 무엇인가요?

이 스킬은 Rust 프로젝트에 적용할 수 있는 20가지 이상의 구체적인 엔지니어링 규칙을 정의합니다. 이 규칙들은 타입 안전성, 소유권 관리, 오류 처리, 모듈 설계 및 성능에 중점을 둡니다. Claude Code, Cursor, Codex 등 AI 코딩 어시스턴트가 Rust 코드를 작성하거나 리뷰할 때 따라야 할 지침을 제공합니다. 주요 규칙으로는 '타입 우선 설계'(R:design-types-first)와 'newtype 자주 사용'(R:newtype-liberally)이 있으며, 이는 도메인 개념을 타입으로 명확히 구분하고 컴파일러가 잘못된 상태를 방지하도록 합니다. '시그니처에서 빌림 사용'(R:borrow-in-signatures)과 '모든 clone 정당화'(R:justify-every-clone)는 불필요한 할당을 방지하고 소유권을 명확히 합니다. '파싱 후 검증'(R:parse-dont-validate)과 '넓게 파싱 후 범위 검사'(R:parse-wide-then-range-check)는 경계에서 데이터를 검증하고 내부에서는 안전한 타입을 사용하도록 강제합니다. 또한 'async 전방위'(R:async-all-the-way)와 '비동기 내 block_on 금지'는 비동기 코드에서 블로킹 호출을 방지하여 성능 저하와 교착 상태를 예방합니다. '오류는 실패한 것을 명명'(R:errors-name-what-failed)은 오류 열거형에 식별 값을 포함시켜 호출자가 정확히 매칭하고 처리할 수 있게 합니다. 'SKILL.md'에는 각 규칙에 대한 상세한 설명과 함께, 일부 규칙은 'hook'에 의해 자동으로 강제됨을 명시합니다. 이 규칙들은 Claude Code, Cursor, Codex 등 여러 AI 플랫폼에서 사용할 수 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-rust

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Domain: Rust 규칙

Async는 모든 계층에서 async [R:async-all-the-way]

부분적으로 hook no-block-on-in-async에 의해 강제됨; async 내부에서 런타임 스레드가 블로킹되는 모든 다른 방식(동기 파일 및 네트워크 I/O, await를 걸친 std::sync 락, spawn_blocking으로 넘기지 않은 긴 CPU 작업) 및 src/ 트리 외부의 모든 Rust 파일은 이 지침만으로 유지됨.

async 코드 내부에서는 block_on을 사용하지 마세요. async 런타임은 많은 태스크를 적은 수의 스레드에 멀티플렉싱하므로, 블로킹 호출은 하나의 태스크만 지연시키는 것이 아니라 해당 기간 동안 워커를 풀에서 제거하여 거기서 실행될 모든 태스크를 지연시킵니다. 피해자는 블로킹한 코드와 무관하므로, 증상은 완전히 다른 곳에서 설명할 수 없는 지연 시간으로 나타나며, 사후에 원인을 추적하기 거의 불가능합니다.

block_on은 동기 코드가 async에 진입하는 경계(main, 테스트, C 라이브러리의 콜백)에서만 올바릅니다. 내부로 진입한 후에는 계속 내부에 있어야 합니다. async I/O, await를 넘어서도 가드가 유지되어야 하는 곳에서는 async 인식 락, 그리고 진정으로 블로킹되는 작업(CPU 바운드 계산 또는 동기 타사 클라이언트 등)에는 spawn_blocking을 사용하세요.

이는 await를 걸쳐 유지되는 std::sync::Mutex 가드에도 동일하게 적용됩니다. 블로킹으로 표시되는 것은 없지만, 태스크가 디스케줄링되는 동안 락을 보유하므로, 경쟁하는 태스크가 자신의 워커 스레드를 블로킹하고 실패는 락 경합 대신 교착 상태로 나타납니다.

현재 스레드 런타임에서 중첩된 block_on은 성능이 저하되는 것이 아니라 즉시 교착 상태가 됩니다. 이것이 정직한 실패입니다. 멀티스레드 경우는 동일한 실수를 스레드 수 뒤에 숨기지만, 부하가 증가하면 숨는 장소가 사라집니다.

함수가 소유해야 하지 않는 한 시그니처에서 빌림을 사용하세요 [R:borrow-in-signatures]

String 대신 &str, Vec<T> 대신 &[T], PathBuf 대신 &Path를 사용하세요. 시그니처는 함수가 필요로 하는 것이 무엇인지에 대한 진술이며, 소유된 매개변수는 "이것을 내가 보관하겠다"는 의미입니다. 따라서 본문이 읽기만 하는 경우 시그니처는 거짓이 되고 모든 호출자는 할당 비용을 지불합니다.

함수가 값을 저장하는 경우에만 매개변수를 소유하세요. 그 경우 impl Into<String>이 예의바른 형태입니다. String을 보유한 호출자는 비용 없이 이동시키고, &str을 보유한 호출자는 의식적으로 한 번 할당합니다. 반환 시에는 &String 대신 &str을 반환하세요. 추가 간접 참조는 호출자에게 아무것도 제공하지 않으며 필드의 표현을 공개 API에 고정시킵니다.

비용은 단일 할당이 거의 아닙니다. 문제는 소유된 매개변수가 전파된다는 것입니다. 호출자는 이를 만족시키기 위해 복제하고, 그 호출자의 호출자는 그것을 만족시키기 위해 복제하며, 생각 없이 선택된 시그니처는 각각이 지역적으로 필요해 보이는 복제의 열이 됩니다. R:justify-every-clone은 이러한 복제가 표면화되는 곳입니다. 이 규칙은 복제가 생성되지 않도록 하는 방법입니다.

로직 전에 타입을 먼저 작성하여 설계하세요 [R:design-types-first]

작성 이후 2번 반복됨; 가장 최근 2026-11-01.

설계가 드러날 때까지 타입을 스케치한 다음 로직을 작성하세요. 타입 수준 스케치는 테스트 우선 레드(red) 단계가 추구하는 실행 가능한 명세이며, 더 강력합니다. 테스트는 동작 공간의 지점을 샘플링하는 반면, 타입은 전체 공간을 제약하고 컴파일러가 코드가 존재하는 한 모든 곳에서 컴파일 타임에 증명합니다. 타입이 올바르면 구현의 대부분이 스스로 작성되고, 많은 잘못된 구현은 컴파일되지 않습니다. Rust에서 이 순서는 테스트 우선 기본값을 무효화합니다. 테스트가 제거되는 것이 아니라, 테스트가 올바른 도구인 계층으로 강등됩니다. 타입이 인코딩할 수 없는 동작 법칙에 대한 속성 테스트, 과거 버그에 대한 회귀 고정 장치로서의 단위 테스트입니다. 각 보장을 보유할 수 있는 가장 강력한 계층에 적용하고, 리뷰 시에는 먼저 "통과하는가"가 아니라 "이러한 보장 중 어떤 것이 더 높은 계층으로 이동할 수 있는가"를 물어보세요.

오류 열거형은 실패를 식별하는 값을 전달합니다 [R:errors-name-what-failed]

부분적으로 hook no-string-error에 의해 강제됨; 필드 없는 변형, 식별 값을 생략한 변형, 및 인식할 수 없는 값을 전달하는 변형과 한 줄로 구분할 수 없는 포괄적인 Unknown(String)은 이 지침만으로 유지됨.

모든 개별 실패는 thiserror 열거형의 변형이며, 각 변형은 인스턴스를 식별하는 값을 전달합니다:

#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
    #[error("{addr}에 대한 TCP 연결 실패")]
    Tcp { addr: SocketAddr, #[source] source: io::Error },
    #[error("{elapsed:?} 후 시간 초과")]
    Timeout { elapsed: Duration },
}

이로 인해 문자열이 제공할 수 없는 두 가지가 따릅니다. 호출자는 match를 사용할 수 있습니다. Timeout은 재시도, Tcp는 표면화 -- 다음에 누군가 메시지를 편집할 때 변경되는 산문을 구문 분석하는 대신에. 그리고 메시지는 실제 주소를 명명하므로, 하나의 로그 줄로 조치를 취할 수 있고, 조사의 시작점이 아닙니다.

원인을 텍스트에 삽입하는 대신 #[source]에 보관하세요. 그러면 각 계층이 아래 계층을 다시 기술하지 않고 체인이 한 번, 가장자리에서 출력됩니다.

호출자가 그럴듯하게 다르게 처리할 수 있는 각 조건마다 하나의 변형. 네 가지 원인을 Other(String)으로 축소하면 열거형 내부에 문자열 오류가 재생성됩니다. 타입처럼 읽히지만 산문처럼 동작하며, 이 규칙이 잡아내기 위해 존재하는 형태입니다.

모든 clone()은 그 이유를 수반하거나, 설계가 잘못된 것입니다 [R:justify-every-clone]

부분적으로 hook no-clone-without-comment에 의해 강제됨; 주석이 호출을 다시 설명하는 것이 아니라 실제 이유를 명시하는지 여부 및 src/ 트리 외부의 모든 Rust 파일은 이 지침만으로 유지됨.

빌림 검사기 오류는 소유권에 대한 질문입니다. clone()은 이에 답하지 않습니다. 답을 피하기 위해 비용을 지불합니다. 먼저 답변을 시도하세요: 하나의 소유자가 명확하도록 재구성하고, 명명된 수명으로 빌림을 사용하고, 더 작은 필드에 걸쳐 빌림을 분할하거나, 값이 복사되는 것이 아니라 진정으로 공유되는 경우 Arc/Rc로 공유하세요.

해당 검토에서 살아남은 clone이 있는 경우, 그 이유를 옆에 이 코드베이스 전체에 사용되는 표준 형식으로 작성하세요:

let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // allow:clone: OutputFile은 자신의 출처를 소유하며, &Library 빌림보다 오래 지속됨

주석이 규칙을 리뷰 가능하게 만듭니다. 주석이 없는 clone은 서로 구별할 수 없으므로, 리뷰어는 모든 clone에 대해 소유권 인수를 다시 도출해야 하며 실제로는 어떤 것에도 다시 도출하지 않습니다. 주석이 있는 clone은 확인할 수 있고, 그 주변 설계가 변경되는 날 찾아서 제거할 수 있는 주장을 진술합니다.

이유는 소유권 또는 수명에 관한 것이어야 합니다. "컴파일되게 하기 위해"는 오류 클래스를 다시 설명한 것일 뿐, 정당화가 아닙니다. 코드가 하나의 값이라고 믿는 값의 두 복사본은 누군가가 잘못된 복사본을 변경할 때까지 기다리는 정확성 버그입니다.

리뷰에서 살아남은 대부분의 clone은 호출 사이트가 아니라 시그니처에 의해 생성되었습니다. R:borrow-in-signatures가 압력이 오는 곳이며, 매개변수를 수정하면 일반적으로 clone을 주석 처리하는 대신 삭제합니다.

모듈은 캡슐화 경계입니다. pub는 의도적인 내보내기입니다 [R:module-visibility-is-deliberate]

pub(crate) 및 pub(super)를 자유롭게 사용하세요. pub은 크레이트가 의도적으로 내보내는 것에만 사용하세요. 가시성은 문서 작업이 아닙니다. 그것은 여전히 자유롭게 변경될 수 있는 것에 대한 진술이며, 컴파일러가 확인할 수 있는 유일한 그러한 진술입니다.

기본값이 중요한 이유는 pub이 추가하기는 쉽고 제거하기는 어렵기 때문입니다. 일단 항목이 공개되면 외부 호출자가 이에 의존할 수 있으므로, 모듈을 더 이상 재구성할 수 없고, 필드 이름을 바꿀 수 없으며, 하나의 호출 사이트를 위해 작성된 도우미가 이제 지원되는 API가 됩니다. 그 어떤 것도 결정된 것이 아닙니다. 그것은 기본값으로 설정된 것입니다.

모듈은 타입과 같은 방식으로 불변 속성을 보유합니다. 검사 후에만 호출될 수 있는 도우미는 해당 검사 옆에, 모듈에 비공개로 속하므로, "이후에만"은 다른 곳에서 아무도 전혀 호출할 수 없음으로 강제됩니다. 공개로 만들면 그 보장이 문서 주석으로 변환됩니다.

의도된 표면을 크레이트 루트에서 pub use로 명시적으로 다시 내보내고, 그 뒤에 있는 모든 것은 가능한 한 비공개로 두세요. 그러면 공개 API는 코드가 우연히 있던 곳의 잔여물이 아니라 누군가가 작성한 목록이 됩니다.

R:private-fields-only는 항목 수준이 아닌 필드 수준에서 동일한 논리입니다. 둘은 분리 가능합니다. pub 타입의 비공개 필드는 구조를 누출하고, pub(crate) 타입의 공개 필드는 누출하지 않습니다. 따라서 어느 하나를 만족한다고 해서 다른 것에 대해 말하는 것은 아닙니다.

중요한 반환 값에 #[must_use]를 표시하세요 [R:must-use-on-consequential-returns]

Result를 반환하는 모든 함수와 반환 값이 호출 목적인 모든 함수에 #[must_use]를 추가하세요. 그러면 컴파일러는 현재 및 미래의 모든 호출 사이트에서 버려진 결과를 플래그 지정하며, 리뷰어가 100개의 문장 중 하나의 빈 문장을 발견하도록 맡기지 않습니다.

함수에 대한 속성보다 타입에 대한 속성을 선호하세요. #[must_use] struct Receipt;는 Receipt를 반환하는 모든 함수로 전달되며, 이 규칙을 읽은 적이 없는 사람이 나중에 작성한 함수도 포함합니다. 함수 수준 속성은 매번 기억해야 합니다. 타입이 항상 중요한 경우 타입에 첨부하고, 동일한 타입이 때때로 버려져도 괜찮은 좁은 경우에만 함수로 폴백하세요.

결과 경고를 CI에서 오류로 승격시키세요. 위반 시 아무도 읽지 않는 메모를 출력하는 #[must_use]는 추가 구문이 있는 주석에 불과하며, 주장하는 보장은 어디에도 유지되지 않습니다.

Newtype을 자유롭게 사용하세요: 별개의 개념은 별개의 타입을 얻습니다 [R:newtype-liberally]

기본 표현이 동일하더라도 모든 도메인 개념에 고유한 타입을 부여하세요. Miles(f64)와 Kilometers(f64), Host(String)와 Port(u16) -- 순서를 인간만 기억하는 두 개의 f64 또는 String과 u16이 없어야 합니다. Newtype은 일반적으로 제로 비용입니다. 기본형과 동일한 기계 코드로 컴파일되므로, 추가되는 유일한 것은 원하는 컴파일 오류입니다. 그러나 그 비용이 부담이 되는 경우, R:verify-the-abstraction-compiled-away는 반복되는 주장이 아닌 확인된 주장을 요구합니다. 작성할 수 없는 단위 혼동은 리뷰에서 잡히는 것보다 저렴하고, 잡히지 않는 것보다 훨씬 저렴합니다. R:parse-dont-validate는 newtype이 나오는 곳입니다. 경계가 증인으로서 이를 발행합니다. 이 규칙은 그 후에 기본형으로 다시 풀지 않고 모든 곳에서 이를 전달하는 것입니다.

라이브러리 반환 타입에 anyhow 금지 [R:no-anyhow-in-libraries]

또한 hook no-anyhow-in-lib에 의해 강제됨.

라이브러리 크레이트는 타입화된 오류 열거형(thiserror)을 반환하므로, Result는 정확히 무엇이 잘못될 수 있는지 말하고 호출자가 매칭할 수 있습니다. anyhow는 최외곽 바이너리 경계에만 속합니다. 이 규칙은 졸업했습니다. no-anyhow-in-lib 훅이 이제 작성 시점에 결정적으로 강제합니다.

let _ =를 사용하여 사용되지 않는 지역 변수를 무음 처리하지 마세요. 사용하거나, 보고하거나, 삭제하세요 [R:no-let-underscore-on-a-local]

부분적으로 hook no-let-underscore-local에 의해 강제됨; 밑줄 접두사 바인딩은 이 지침만으로 유지됨.

컴파일러의 사용되지 않는 바인딩 경고는 독자입니다. 그것은 계산하는 데 수고를 들인 값이 아무것도 도달하지 않는다는 것을 말합니다. let _ = x;는 그 독자를 끄기 위해 존재하며, 지역 변수에서는 거의 항상 그것이 옳았던 순간에 끕니다.

두 가지 형태를 취하며, 둘 다 -D warnings 아래에서 깨끗하게 컴파일되지만 리뷰어에게는 누군가가 내린 결정으로 읽힙니다:

  • 계산되어 버려진 구분. 플래그가 파싱되고, 모드가 디코딩되고, 설정 값이 읽힌 다음 폐기되어, 분리하려고 의도된 케이스가 하나로 조용히 처리됩니다.
  • 잡혀서 버려진 실패. if let Err(e) = op() { let _ = e; continue; }는 어디에도 이벤트를 남기지 않습니다. 실패가 발생했고, 시스템의 그 누구도 이후에 그렇게 말할 수 없습니다.

경고가 발생하면 세 가지 중 하나를 수행하세요. 값을 사용하세요. 사용되지 않은 구분은 일반적으로 누락된 분기를 의미하며, 경고가 그것을 가리키고 있습니다. 보고하거나 전파하세요. 진정으로 무시해도 안전한 오류라도 명명된 이벤트 또는 다운스트림이 알 필요가 없는 이유를 설명하는 주석을 얻습니다. 또는 값을 계산한 코드를 삭제하세요. 바인딩이 설계상 죽은 경우(예: 루프 변수에 대한 마지막 할당), 죽은 할당이 절대 발생하지 않도록 루프를 다시 작성하고 경고를 무음 처리하지 마세요.

호출에 대한 let _ = f();는 다른 경우입니다. 반환 값을 버리며, 이는 R:must-use-on-consequential-returns가 관리합니다. 이 규칙은 이미 보유한 지역 변수에 관한 것입니다. R:guarantee-needs-a-reader는 컴파일러가 읽지 않은 바인딩을 잡는 데 의존합니다. let _ =는 그것이 볼 수 없는 유일한 철자입니다.

실패 모드 확인: let _ = name;을 작성하려고 할 때마다 이 값은 무엇을 결정하기 위해 계산되었으며, 그 결정은 이제 어디에서 발생합니까? 답이 "어디에도 없다"면 경고가 옳았습니다.

프로덕션 코드에서 unwrap() 금지 [R:no-unwrap-in-production]

또한 hook no-unwrap-in-src + hook no-expect-empty-msg에 의해 강제됨.

프로덕션 코드에서는 unwrap()을 사용하지 마세요. 리소스와 불변 속성을 명명하는 의미 있는 패닉 메시지와 함께 expect()를 사용하거나, ?로 오류를 반환하고 호출자가 이를 표면화하는 방법을 결정하도록 하세요. 이 규칙은 졸업했습니다. no-unwrap-in-src 훅이 이제 작성 시점에 결정적으로 강제하므로, 명령 계층이 더 이상 할 필요가 없습니다.

파싱하고 검증하지 마세요 [R:parse-dont-validate]

부분적으로 hook no-free-validator에 의해 강제됨; 내부 호출 사이트에서 파싱된 데이터를 재확인하는 것은 이 지침만으로 유지됨.

최외곽 경계에서 원시 입력을 풍부한 도메인 타입으로 변환하고, 검사가 발생했음을 증명하는 증인 newtype을 생성하세요. 내부 코드는 증인을 받고 다시 검사하지 않습니다. 하나의 경계, 하나의 검사, 그 후 컴파일러에 의해 모든 곳에서 강제됩니다. R:parse-wide-then-range-check는 이를 더욱 날카롭게 합니다. 경계는 불법 값을 명명할 수 있도록 보기에 충분히 넓어야 합니다.

넓게 파싱한 다음 범위 검사 [R:parse-wide-then-range-check]

범위를 벗어난 값을 표현할 수 있을 만큼 넓은 타입으로 파싱한 다음, 범위 검사를 수행하세요.

...

[전체 내용을 번역하기에는 너무 깁니다. 위의 내용이 핵심입니다.]

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-java

Engineering discipline for working in java. Covers: a speculated path deoptimises when the input changes; a view is not a copy; a wrapper type is not free here; close what you open; design for inheritance or forbid it; equality is one contract; exceptions name what failed; identity is not equality for boxes; no reference to internals escapes; null is not a value; publish safely or not at all; seal the alternatives; serializable is a second constructor

george-andrikopoulos/domain-low-latency

Engineering discipline for working in low-latency. Covers: attack the design in a second pass; a measurement matches the regime it reports; a queue without a bound has no overload behaviour; a structure keeps the regime it was proved under; allocated is not resident; answer the requirement at its layer; no allocation on the hot path; no coordinated omission; no false sharing on a hot line; no retry loop on a contended path; no stall inside a publication window; no syscall on a bounded path;...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

관련 스킬