# Rust에서의 방어적 프로그래밍 패턴

> Clean Markdown view of GeekNews topic #24880. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=24880](https://news.hada.io/topic?id=24880)
- GeekNews Markdown: [https://news.hada.io/topic/24880.md](https://news.hada.io/topic/24880.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-12-07T07:33:18+09:00
- Updated: 2025-12-07T07:33:18+09:00
- Original source: [corrode.dev](https://corrode.dev/blog/defensive-programming/)
- Points: 32
- Comments: 1

## Summary

러스트의 **타입 시스템과 컴파일러를 적극적으로 활용해 논리적 버그를 사전에 차단하는 방어적 프로그래밍 패턴**을 다룹니다. 벡터 인덱싱, `Default` 남용, 불완전한 `match` 등 흔한 **코드 냄새(Code Smell)**를 구체적으로 짚으며, `TryFrom`, `#[must_use]`, 비공개 필드, 임시 가변성 같은 **컴파일러 수준의 안전장치**로 대체하는 방법을 제시합니다. 핵심은 “**컴파일되지 않는 버그가 가장 좋은 버그**”라는 철학 아래, 불변식을 코드 구조로 강제해 리팩터링 시에도 안정성을 유지하는 것입니다. 러스트로 **견고한 도메인 로직**을 설계하고 싶은 개발자라면 실무 감각이 묻어나는 이 글이 꽤 유용하게 느껴질 듯합니다.

## Topic Body

- 러스트의 **타입 시스템과 컴파일러를 적극 활용**해 버그를 사전에 차단하는 **코딩 습관**을 소개  
- 벡터 인덱싱, `Default` 남용, 불완전한 `match`, 불필요한 불리언 매개변수 등 **취약한 코드 냄새(Code Smell)** 사례를 제시하고 대안을 설명  
- **컴파일러가 불변식을 강제하도록 구조를 설계**하는 것이 핵심 원칙으로, 패턴 매칭·비공개 필드·`#[must_use]` 속성 등을 활용  
- `TryFrom` 사용, 구조체 완전 해체, 임시 가변성, 생성자 검증 등 **실제 코드 수준의 방어 기법**을 구체적으로 제시  
- 이러한 패턴은 **리팩터링 시 안정성 확보와 장기 유지보수성 향상**에 필수적임  
  
---  
  
### 방어적 프로그래밍의 개요  
- `// this should never happen` 주석이 붙은 지점은 **암묵적 불변식이 깨지는 위치**임  
  - 대부분의 경우 개발자가 모든 경계 조건이나 미래 코드 변경을 고려하지 않음  
- 러스트 컴파일러는 메모리 안전성을 보장하지만, **비즈니스 로직 오류는 여전히 발생 가능**  
- 다년간의 실무 경험을 통해 얻은 **작은 습관적 패턴(idiom)** 들이 코드 품질을 크게 향상시킴  
  
### Code Smell: 벡터 인덱싱  
- `if !vec.is_empty() { let x = &vec[0]; }` 형태는 **길이 확인과 인덱싱이 분리되어 런타임 패닉 위험** 존재  
- 슬라이스 패턴 매칭(`match vec.as_slice()`)을 사용하면 **컴파일러가 모든 상태를 강제 검사**  
  - 빈 벡터, 단일 원소, 중복 원소 등 모든 경우를 명시적으로 처리 가능  
- **컴파일러가 불변식을 보장하도록 설계**하는 대표적 예시  
  
### Code Smell: `Default`의 무분별한 사용  
- `..Default::default()`는 **새 필드 추가 시 누락 위험**과 **암묵적 값 설정** 문제를 초래  
- 모든 필드를 명시적으로 초기화하면 **컴파일러가 새 필드 설정을 강제**  
- `let Foo { field1, field2, .. } = Foo::default();` 형태로 **기본값 구조 해체 후 선택적 재정의** 가능  
  - 기본값 유지와 명시적 오버라이드의 균형 확보  
  
### Code Smell: 취약한 Trait 구현  
- 구조체 필드를 완전 해체하여 비교 시 **새 필드 추가 시 컴파일 오류로 경고**  
  - 예: `PartialEq` 구현 시 `let Self { size, toppings, .. } = self;`  
- `extra_cheese` 같은 새 필드가 추가되면 **비교 로직 재검토를 강제**  
- `Hash`, `Debug`, `Clone` 등 다른 트레이트에도 동일 원리 적용 가능  
  
### Code Smell: `From` 대신 `TryFrom` 필요  
- 변환이 항상 성공하지 않는 경우 `From` 대신 **`TryFrom`으로 실패 가능성 명시**  
- `unwrap_or_else` 사용은 **잠재적 실패를 숨기는 신호**로, 조기 실패(fail fast) 방식이 더 안전  
  
### Code Smell: 불완전한 `match`  
- `_ => {}` 와 같은 **catch-all 패턴은 새 variant 추가 시 누락 위험**  
- 모든 variant를 명시적으로 나열하면 **컴파일러가 새 케이스 처리 누락을 경고**  
- 동일 로직은 `Variant3 | Variant4` 형태로 그룹화 가능  
  
### Code Smell: `_` 플레이스홀더 남용  
- `_`만 사용하면 **어떤 변수가 생략됐는지 불명확**  
- `has_fuel: _, has_crew: _`처럼 **명시적 이름으로 가독성 향상**  
  
### Pattern: 임시 가변성(Temporary Mutability)  
- 데이터가 **초기화 중에만 가변**이어야 할 때, `let mut data = ...; data.sort(); let data = data;` 형태 사용  
- 블록 스코프를 활용하면 **임시 변수의 외부 노출 방지**  
  - 예: `let data = { let mut d = get_vec(); d.sort(); d };`  
- 여러 임시 변수를 사용하는 초기화 과정에서 **명확한 범위 구분** 가능  
  
### Pattern: 생성자 검증 강제  
- 구조체 생성 시 **검증 로직을 반드시 거치도록 강제**  
  - `_private: ()` 필드 추가 시 외부에서 직접 생성 불가  
  - `#[non_exhaustive]` 속성은 **크레이트 외부 생성 차단 및 미래 확장 신호**  
- 내부 모듈에서도 강제하려면 **비공개 타입(Seal)을 가진 중첩 모듈 구조** 사용  
  - `Seal`이 내부에만 존재해 `new()` 외 직접 생성 불가  
- 필드를 비공개로 두고 getter 제공 시 **불변 상태 유지**  
- 적용 기준  
  - 외부 코드 차단: `_private` 또는 `#[non_exhaustive]`  
  - 내부 코드 차단: 비공개 모듈 + `Seal`  
  - **검증 로직을 컴파일러 수준 보장으로 전환**  
  
### Pattern: `#[must_use]` 속성 활용  
- `#[must_use]`는 **중요한 반환값 무시를 방지**  
  - 예: `#[must_use = "Configuration must be applied to take effect"]`  
- 사용자가 반환값을 무시하면 **컴파일러 경고 발생**  
- `Result` 등 표준 라이브러리에서도 널리 사용되는 **간단하지만 강력한 방어 수단**  
  
### Code Smell: 불리언 매개변수  
- `fn process_data(..., compress: bool, encrypt: bool, validate: bool)` 형태는 **의미 불명확·순서 오류 위험**  
- `enum Compression`, `enum Encryption` 등으로 **의도를 명시적 표현**  
- 여러 옵션이 있는 경우 **파라미터 구조체(Params struct)** 사용  
  - `ProcessDataParams::production()` 등 **사전 설정 메서드로 재사용성 향상**  
- 새 옵션 추가 시 **기존 호출부 영향 최소화**  
  
### Clippy Lints로 자동화  
- 주요 방어 패턴을 **Clippy 린트로 자동 검사 가능**  
  - `indexing_slicing`: 직접 인덱싱 금지  
  - `fallible_impl_from`: `From` 대신 `TryFrom` 권장  
  - `wildcard_enum_match_arm`: `_` 패턴 금지  
  - `fn_params_excessive_bools`: 불리언 매개변수 과다 경고  
  - `must_use_candidate`: `#[must_use]` 후보 제안  
- `#![deny(clippy::...)]` 또는 `Cargo.toml` 설정으로 프로젝트 전역 적용 가능  
  
### 결론  
- 러스트의 **타입 시스템과 컴파일러를 적극 활용해 불변식을 명시적·검증 가능하게 만드는 것**이 방어적 프로그래밍의 핵심  
- 이러한 패턴은 **리팩터링 시 안정성 확보, 버그 발생 가능성 최소화, 장기 유지보수성 강화**에 기여  
- “**컴파일되지 않는 버그가 가장 좋은 버그**”라는 원칙을 실천하는 접근임

## Comments



### Comment 47323

- Author: neo
- Created: 2025-12-07T07:33:18+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=46163609) 
- 글이 좋았음. 다만 **PizzaOrder 예시**는 구조가 너무 많은 관심사를 한 struct에 몰아넣은 느낌임  
  `ordered_at`을 비교에서 제외하려는 목적이라면, `PizzaDetails`와 `PizzaOrder` 두 개의 struct로 분리하는 게 더 낫다고 생각함  
  이렇게 하면 `PartialEq` 구현 시 `details`만 비교하도록 명확히 할 수 있음  
  - 좋은 지적임. 하지만 여전히 논리적으로는 **잘못된 모델링**이라고 생각함  
    주문 시간이 다르면 같은 주문이 아니므로, 타입 수준에서 같다고 정의하는 건 위험함  
    `PizzaDetails`에 `PartialEq`을 두는 건 괜찮지만, 주문 비교 로직은 별도의 비즈니스 함수로 두는 게 맞음  
  - 구조를 분리하는 접근은 좋지만, `PizzaDetails`를 수정할 때 그 변경이 **피자 중복 제거 로직**에 영향을 줄 수 있다는 점이 문제임  
    struct는 단순히 데이터를 묶는 용도로만 쓰는 게 이상적임  
    변경이 다른 곳에 영향을 미치지 않도록, `PizzaComparator`나 `PizzaFlavor` 같은 별도 타입을 두는 방법도 고려할 수 있음  
    Protobuf처럼 필드에 `{important_to_flavour=true}` 같은 **필드 주석**을 둘 수 있다면 좋겠음  
  - 단순히 다른 비교 방식을 위해 구조를 분리하는 건 일반화되지 않음  
    예를 들어 문자열을 대소문자 구분 없이 비교하려면 어떻게 분리할 수 있겠음?  

- Rust에서 정말 멋진 점은 **방어적 프로그래밍이 필요 없는 경우**가 많다는 것임  
  소유권이나 참조 규칙 덕분에, 특정 객체에 대한 접근이 프로그램 전체에서 유일함을 보장받을 수 있음  
  참조는 null이 될 수 없고, 스마트 포인터도 null이 될 수 없음  
  `self`의 소유권을 넘기면 이후 메서드 호출이 불가능하다는 것도 타입 시스템이 보장함  
  덕분에 스레드 안전성, 수명, 복제 가능성 등이 **컴파일 타임에 전역적으로 검증**됨  
  - 나도 Rust의 진짜 장점이 “신경 쓰지 않아도 되는 것들”에 있다고 생각함  
    다른 언어에서는 함수형 스타일로 불변성을 유지해야 얻는 이점을 Rust는 **타입 시스템**으로 강제함  
  - 하지만 이 댓글은 원문 기사와는 관련이 없어 보임  
    기사 주제는 **borrow checker로도 잡히지 않는 논리적 버그**였음  
  - 기사 내용은 주로 프로그램을 반복적으로 개선할 때 **논리적 실수를 피하는 코딩 패턴**에 초점을 맞췄음  

- 배열이나 벡터에 직접 인덱싱하는 건 피하는 게 현명하다고 느낌  
  Cloudflare의 unwrap 사고가 있었던 날, 나도 슬라이스가 벡터 끝을 넘어가는 버그를 발견했음  
  이후 **이터레이터 기반 접근**으로 바꾸고 훨씬 안전하게 느껴짐  
  - unwrap 사고를 “사고”로 볼 필요는 없다고 생각함  
    Rust의 `unwrap`은 C의 `assert`와 같음. 실패하면 문제를 알려주는 역할을 하는 것뿐임  
    Rust에서도 버그는 여전히 작성할 수 있음  
  - 결국 같은 문제임. Rust 진영에서 C를 버리자고 하지만, C에서도 인덱스 대신 **핸들**을 쓰는 게 일반적임  

- Rust 개발자들이 방어해야 할 습관 중 하나는 **불필요한 crate 의존성**을 추가하는 것임  
  Rust는 이런 습관을 장려하는 경향이 있음. 예를 들어 Rust Book에서 `rand` crate를 기본 예시로 쓰는 것도 그런 분위기를 만듦  
  물론 암호 관련 패키지를 쉽게 교체할 수 있게 한 전략적 선택이긴 하지만, 여전히 습관화되는 건 문제임  
  - 나도 그 예시 때문에 Rust에 처음엔 거부감이 있었음  
    하지만 나중엔 그 의도를 이해하고 생각이 바뀌었음  

- 부분 동등성 구현이 흥미로웠음  
  또 하나 궁금한 건 **불리언 매개변수**를 피할 때 enum을 쓰는 방법임  
  나는 bool을 감싼 struct를 쓰는데, 이걸 일반 bool처럼 다루지 못하는 게 아쉬움  
  enum을 bool처럼 쓸 수 있는 방법이 있을까 궁금함  
  - 나도 거의 항상 **enum + match!** 를 선호함  
    필요한 로직을 Trait으로 묶거나, `impl &lt;Enum&gt;` 블록에 공통 메서드를 추가하는 식으로 처리함  
    이렇게 하면 가독성도 좋고, 각 멤버별 동작을 명확히 정의할 수 있음  
  - `impl Deref` 같은 걸 써볼 수도 있겠지만, 좋은 아이디어인지는 잘 모르겠음  

- 첫 번째 예시의 `match` 구문은 너무 과한 느낌임  
  `Vec.first()`나 `Vec.iter().nth(0)`이 더 명확하고 의도에 맞음  
  - 나도 동의함. `match`를 쓰면 오히려 **문제보다 복잡한 해결책**이 됨  
    `if`를 제거할 수 있다면 `match`도 제거할 수 있으니, 안전성 측면에서 차이가 없음  
    `first()`가 훨씬 간결하고 명확함  
  - 같은 동작을 더 간단히 표현하려면 [itertools의 exactly_one](https://docs.rs/itertools/latest/itertools/trait.Itertools.html#method.exactly_one)을 쓸 수도 있음  
  - 다만 `match`는 “요소가 하나 이상일 때”의 경우도 처리하도록 유도한다는 점에서 의미가 있음  
    즉, **검사와 의존 코드의 분리를 피하라**는 원칙을 드러냄  

- 이런 글을 읽을 때마다, 왜 **코드 패턴을 모니터링하는 전담 팀**이 없는지 궁금해짐  
  SOC나 QA처럼 코드베이스의 패턴을 장기적으로 관찰하는 팀이 있으면 좋겠음  
  자동화된 코드 스멜 탐지 도구로는 한계가 있음  
  - 우리 회사(약 300명 규모)에는 이런 역할의 **기술 부채 전담팀**이 있음  
    린트 규칙 관리, 문서화, 개발자 교육, 공통 라이브러리 유지보수 등을 담당함  
    여러 팀이 같은 문제를 반복하면, 이를 통합할 수 있는 **핵심 API**를 설계함  
  - 대형 기술 기업에는 대부분 이런 팀이 존재함  
    다만 코드가 수백만 줄이면 관리가 매우 어렵다는 게 현실임  

- 이런 **좋은 코딩 패턴**을 팀 내에서 어떻게 장려할 수 있을지 고민임  
  코드 리뷰 중에는 “스타일 논쟁”으로 번져 비생산적일 때가 많음  
  그런데 신기하게도 린터가 경고를 띄우면 그런 논쟁이 거의 사라짐  

- `TryFrom` 트레이트가 1.34 버전에 추가된 게 정말 유용했음  
  아마 `unwrap_or_else()`를 쓰던 코드는 그 이전 시기의 잔재일 가능성이 큼  
  [From 트레이트 문서](https://doc.rust-lang.org/std/convert/trait.From.html#when-to-implement-from)가 이제는 언제 구현해야 하는지 아주 명확하게 설명되어 있음  
  - Rust를 아직 배우는 중인데, `unwrap_or_else()`라는 이름이 마치 “컴퓨터에게 협박하듯 명령하는” 느낌이라 재밌게 들림  

- 이런 **방어적 프로그래밍 패턴**이 대규모 **AI 코드 생성 품질 향상**에도 도움이 될 거라 생각함  
  Clippy나 Rust 컴파일러가 제공하는 구체적 피드백이 AI 에이전트가 실수를 줄이고 방향을 잡는 데 큰 역할을 할 수 있음
