- 러스트의 타입 시스템과 컴파일러를 적극 활용해 버그를 사전에 차단하는 코딩 습관을 소개
- 벡터 인덱싱, 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 설정으로 프로젝트 전역 적용 가능
결론
- 러스트의 타입 시스템과 컴파일러를 적극 활용해 불변식을 명시적·검증 가능하게 만드는 것이 방어적 프로그래밍의 핵심
- 이러한 패턴은 리팩터링 시 안정성 확보, 버그 발생 가능성 최소화, 장기 유지보수성 강화에 기여
- “컴파일되지 않는 버그가 가장 좋은 버그”라는 원칙을 실천하는 접근임