러스트에서 방어적 프로그래밍 패턴

4 days ago 4

  • 러스트의 타입 시스템과 컴파일러를 적극 활용해 버그를 사전에 차단하는 코딩 습관을 소개
  • 벡터 인덱싱, 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 설정으로 프로젝트 전역 적용 가능

결론

  • 러스트의 타입 시스템과 컴파일러를 적극 활용해 불변식을 명시적·검증 가능하게 만드는 것이 방어적 프로그래밍의 핵심
  • 이러한 패턴은 리팩터링 시 안정성 확보, 버그 발생 가능성 최소화, 장기 유지보수성 강화에 기여
  • 컴파일되지 않는 버그가 가장 좋은 버그”라는 원칙을 실천하는 접근임

Read Entire Article