15. 크레이트 아키텍처 및 API 설계 🟡
학습 목표:
- 모듈 구조의 표준 관례와 **재내보내기(Re-export)**를 이용한 API 정제법을 익힙니다.
- 세련된 크레이트 제작을 위한 공개 API 체크리스트를 학습합니다.
impl Into,AsRef,Cow를 활용한 편리한(Ergonomic) 인자 전달 패턴을 배웁니다.- "검증하지 말고 파싱하라(Parse, don't validate)" 원칙과
TryFrom의 가치를 이해합니다.- 피처 플래그(Feature flags), 조건부 컴파일, 워크스페이스 구조를 마스터합니다.
모듈 구조와 가시성 가이드
성공적인 크레이트는 내부 구조가 복잡하더라도 외부에는 깔끔한 인터페이스를 노출합니다.
lib.rs: 크레이트의 뿌리입니다. 내부 모듈들을 선언하고, 사용자가 필요한 타입들만pub use로 재내보내어 API를 큐레이션합니다.- 가시성 제어:
pub(crate)를 활용해 크레이트 내부에서는 자유롭게 접근하되, 외부에는 노출되지 않도록 경계를 설정하세요.
공개 API 설계 체크리스트
- 빌림(Reference)으로 받고 소유권(Owned)으로 반환:
fn process(s: &str) -> String impl Trait활용: 인자 타입이 명확할 때는 제네릭보다impl Read처럼 표현하는 것이 가독성에 좋습니다.- 패닉 대신
Result: 실패 가능성이 있는 모든 API는 호출자가 처리 방식을 결정할 수 있게 하세요. - 표준 트레이트 구현:
Debug,Clone,Default등을 기본적으로 제공하세요. - 불가능한 상태를 타입으로 방지: 타입 상태(Type-state) 패턴을 적극 활용하세요.
#[must_use]사용: 반환값이 무시되면 안 되는 중요한 함수나 객체에 표시하세요.#[non_exhaustive]사용: 공개 열거형에 사용해 나중에 항목이 추가되어도 하위 호환성이 깨지지 않게 하세요.
편리한 인자 패턴: impl Into, AsRef, Cow
사용자가 호출할 때 .to_string()이나 .as_ref()를 남발하지 않도록 설계하세요.
#![allow(unused)] fn main() { // ❌ 불편한 방식: 사용자가 직접 String으로 변환해야 함 fn connect(host: String) { ... } // ✅ 편리한 방식: &str, String 모두 그대로 전달 가능 fn connect(host: impl Into<String>) { let host = host.into(); // 내부에서 변환 } }
AsRef<T>: 데이터를 읽기만 할 때 사용하세요.Cow<'a, T>: 평소에는 빌려 쓰다가 수정이 필요할 때만 비로소 복제(Clone)하고 싶을 때 사용하세요.
검증하지 말고 파싱하라 (Parse, don't validate)
데이터가 유효한지 체크만 하고 다시 원시 타입(String, i32 등)으로 들고 다니지 마세요. 유효성 검사가 완료된 정보만 담을 수 있는 **전용 타입(Newtype)**으로 파싱하세요. 일단 해당 타입의 객체가 생성되었다면, 이후 코드에서는 다시 검증할 필요 없이 안전하게 사용할 수 있습니다.
피처 플래그와 조건부 컴파일
사용자가 필요한 기능만 선택해서 빌드할 수 있게 하여 바이너리 크기와 의존성을 줄이세요. Cargo.toml의 [features] 섹션과 #[cfg(feature = "...")] 속성을 활용합니다.
📝 연습 문제: 크레이트 API 리팩토링 ★★ (~30분)
원시 타입 사용이 남발된(Stringly-typed) API를 TryFrom, 뉴타입, 빌더 패턴을 사용하여 리팩토링해 보세요. 특히 호스트 이름, 포트(1~65535) 등이 파싱 단계에서 완벽히 검증되도록 설계해 보세요.
📌 요약
- API는 유연하게 받고(Into/AsRef), 구체적으로 반환하세요.
- **
#[non_exhaustive]**는 라이브러리 유지보수성을 위한 필수 도구입니다. - 타입 시스템 자체가 보증서가 되도록 설계하세요(Parse, don't validate).
- 공급망 보안:
cargo audit과cargo deny를 정기적으로 실행하여 안전한 의존성을 관리하세요.