Rust에서의 방어적 프로그래밍 패턴
(corrode.dev)- 러스트의 타입 시스템과 컴파일러를 적극 활용해 버그를 사전에 차단하는 코딩 습관을 소개
- 벡터 인덱싱,
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설정으로 프로젝트 전역 적용 가능
결론
- 러스트의 타입 시스템과 컴파일러를 적극 활용해 불변식을 명시적·검증 가능하게 만드는 것이 방어적 프로그래밍의 핵심
- 이러한 패턴은 리팩터링 시 안정성 확보, 버그 발생 가능성 최소화, 장기 유지보수성 강화에 기여
- “컴파일되지 않는 버그가 가장 좋은 버그”라는 원칙을 실천하는 접근임
Hacker News 의견
-
글이 좋았음. 다만 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로도 잡히지 않는 논리적 버그였음 - 기사 내용은 주로 프로그램을 반복적으로 개선할 때 논리적 실수를 피하는 코딩 패턴에 초점을 맞췄음
- 나도 Rust의 진짜 장점이 “신경 쓰지 않아도 되는 것들”에 있다고 생각함
-
배열이나 벡터에 직접 인덱싱하는 건 피하는 게 현명하다고 느낌
Cloudflare의 unwrap 사고가 있었던 날, 나도 슬라이스가 벡터 끝을 넘어가는 버그를 발견했음
이후 이터레이터 기반 접근으로 바꾸고 훨씬 안전하게 느껴짐- unwrap 사고를 “사고”로 볼 필요는 없다고 생각함
Rust의unwrap은 C의assert와 같음. 실패하면 문제를 알려주는 역할을 하는 것뿐임
Rust에서도 버그는 여전히 작성할 수 있음 - 결국 같은 문제임. Rust 진영에서 C를 버리자고 하지만, C에서도 인덱스 대신 핸들을 쓰는 게 일반적임
- unwrap 사고를 “사고”로 볼 필요는 없다고 생각함
-
Rust 개발자들이 방어해야 할 습관 중 하나는 불필요한 crate 의존성을 추가하는 것임
Rust는 이런 습관을 장려하는 경향이 있음. 예를 들어 Rust Book에서randcrate를 기본 예시로 쓰는 것도 그런 분위기를 만듦
물론 암호 관련 패키지를 쉽게 교체할 수 있게 한 전략적 선택이긴 하지만, 여전히 습관화되는 건 문제임- 나도 그 예시 때문에 Rust에 처음엔 거부감이 있었음
하지만 나중엔 그 의도를 이해하고 생각이 바뀌었음
- 나도 그 예시 때문에 Rust에 처음엔 거부감이 있었음
-
부분 동등성 구현이 흥미로웠음
또 하나 궁금한 건 불리언 매개변수를 피할 때 enum을 쓰는 방법임
나는 bool을 감싼 struct를 쓰는데, 이걸 일반 bool처럼 다루지 못하는 게 아쉬움
enum을 bool처럼 쓸 수 있는 방법이 있을까 궁금함- 나도 거의 항상 enum + match! 를 선호함
필요한 로직을 Trait으로 묶거나,impl <Enum>블록에 공통 메서드를 추가하는 식으로 처리함
이렇게 하면 가독성도 좋고, 각 멤버별 동작을 명확히 정의할 수 있음 -
impl Deref같은 걸 써볼 수도 있겠지만, 좋은 아이디어인지는 잘 모르겠음
- 나도 거의 항상 enum + match! 를 선호함
-
첫 번째 예시의
match구문은 너무 과한 느낌임
Vec.first()나Vec.iter().nth(0)이 더 명확하고 의도에 맞음- 나도 동의함.
match를 쓰면 오히려 문제보다 복잡한 해결책이 됨
if를 제거할 수 있다면match도 제거할 수 있으니, 안전성 측면에서 차이가 없음
first()가 훨씬 간결하고 명확함 - 같은 동작을 더 간단히 표현하려면 itertools의 exactly_one을 쓸 수도 있음
- 다만
match는 “요소가 하나 이상일 때”의 경우도 처리하도록 유도한다는 점에서 의미가 있음
즉, 검사와 의존 코드의 분리를 피하라는 원칙을 드러냄
- 나도 동의함.
-
이런 글을 읽을 때마다, 왜 코드 패턴을 모니터링하는 전담 팀이 없는지 궁금해짐
SOC나 QA처럼 코드베이스의 패턴을 장기적으로 관찰하는 팀이 있으면 좋겠음
자동화된 코드 스멜 탐지 도구로는 한계가 있음- 우리 회사(약 300명 규모)에는 이런 역할의 기술 부채 전담팀이 있음
린트 규칙 관리, 문서화, 개발자 교육, 공통 라이브러리 유지보수 등을 담당함
여러 팀이 같은 문제를 반복하면, 이를 통합할 수 있는 핵심 API를 설계함 - 대형 기술 기업에는 대부분 이런 팀이 존재함
다만 코드가 수백만 줄이면 관리가 매우 어렵다는 게 현실임
- 우리 회사(약 300명 규모)에는 이런 역할의 기술 부채 전담팀이 있음
-
이런 좋은 코딩 패턴을 팀 내에서 어떻게 장려할 수 있을지 고민임
코드 리뷰 중에는 “스타일 논쟁”으로 번져 비생산적일 때가 많음
그런데 신기하게도 린터가 경고를 띄우면 그런 논쟁이 거의 사라짐 -
TryFrom트레이트가 1.34 버전에 추가된 게 정말 유용했음
아마unwrap_or_else()를 쓰던 코드는 그 이전 시기의 잔재일 가능성이 큼
From 트레이트 문서가 이제는 언제 구현해야 하는지 아주 명확하게 설명되어 있음- Rust를 아직 배우는 중인데,
unwrap_or_else()라는 이름이 마치 “컴퓨터에게 협박하듯 명령하는” 느낌이라 재밌게 들림
- Rust를 아직 배우는 중인데,
-
이런 방어적 프로그래밍 패턴이 대규모 AI 코드 생성 품질 향상에도 도움이 될 거라 생각함
Clippy나 Rust 컴파일러가 제공하는 구체적 피드백이 AI 에이전트가 실수를 줄이고 방향을 잡는 데 큰 역할을 할 수 있음