32P by GN⁺ 7일전 | ★ favorite | 댓글 1개
  • 러스트의 타입 시스템과 컴파일러를 적극 활용해 버그를 사전에 차단하는 코딩 습관을 소개
  • 벡터 인덱싱, 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을 비교에서 제외하려는 목적이라면, PizzaDetailsPizzaOrder 두 개의 struct로 분리하는 게 더 낫다고 생각함
    이렇게 하면 PartialEq 구현 시 details만 비교하도록 명확히 할 수 있음

    • 좋은 지적임. 하지만 여전히 논리적으로는 잘못된 모델링이라고 생각함
      주문 시간이 다르면 같은 주문이 아니므로, 타입 수준에서 같다고 정의하는 건 위험함
      PizzaDetailsPartialEq을 두는 건 괜찮지만, 주문 비교 로직은 별도의 비즈니스 함수로 두는 게 맞음
    • 구조를 분리하는 접근은 좋지만, PizzaDetails를 수정할 때 그 변경이 피자 중복 제거 로직에 영향을 줄 수 있다는 점이 문제임
      struct는 단순히 데이터를 묶는 용도로만 쓰는 게 이상적임
      변경이 다른 곳에 영향을 미치지 않도록, PizzaComparatorPizzaFlavor 같은 별도 타입을 두는 방법도 고려할 수 있음
      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 <Enum> 블록에 공통 메서드를 추가하는 식으로 처리함
      이렇게 하면 가독성도 좋고, 각 멤버별 동작을 명확히 정의할 수 있음
    • impl Deref 같은 걸 써볼 수도 있겠지만, 좋은 아이디어인지는 잘 모르겠음
  • 첫 번째 예시의 match 구문은 너무 과한 느낌임
    Vec.first()Vec.iter().nth(0)이 더 명확하고 의도에 맞음

    • 나도 동의함. match를 쓰면 오히려 문제보다 복잡한 해결책이 됨
      if를 제거할 수 있다면 match도 제거할 수 있으니, 안전성 측면에서 차이가 없음
      first()가 훨씬 간결하고 명확함
    • 같은 동작을 더 간단히 표현하려면 itertools의 exactly_one을 쓸 수도 있음
    • 다만 match는 “요소가 하나 이상일 때”의 경우도 처리하도록 유도한다는 점에서 의미가 있음
      즉, 검사와 의존 코드의 분리를 피하라는 원칙을 드러냄
  • 이런 글을 읽을 때마다, 왜 코드 패턴을 모니터링하는 전담 팀이 없는지 궁금해짐
    SOC나 QA처럼 코드베이스의 패턴을 장기적으로 관찰하는 팀이 있으면 좋겠음
    자동화된 코드 스멜 탐지 도구로는 한계가 있음

    • 우리 회사(약 300명 규모)에는 이런 역할의 기술 부채 전담팀이 있음
      린트 규칙 관리, 문서화, 개발자 교육, 공통 라이브러리 유지보수 등을 담당함
      여러 팀이 같은 문제를 반복하면, 이를 통합할 수 있는 핵심 API를 설계함
    • 대형 기술 기업에는 대부분 이런 팀이 존재함
      다만 코드가 수백만 줄이면 관리가 매우 어렵다는 게 현실임
  • 이런 좋은 코딩 패턴을 팀 내에서 어떻게 장려할 수 있을지 고민임
    코드 리뷰 중에는 “스타일 논쟁”으로 번져 비생산적일 때가 많음
    그런데 신기하게도 린터가 경고를 띄우면 그런 논쟁이 거의 사라짐

  • TryFrom 트레이트가 1.34 버전에 추가된 게 정말 유용했음
    아마 unwrap_or_else()를 쓰던 코드는 그 이전 시기의 잔재일 가능성이 큼
    From 트레이트 문서가 이제는 언제 구현해야 하는지 아주 명확하게 설명되어 있음

    • Rust를 아직 배우는 중인데, unwrap_or_else()라는 이름이 마치 “컴퓨터에게 협박하듯 명령하는” 느낌이라 재밌게 들림
  • 이런 방어적 프로그래밍 패턴이 대규모 AI 코드 생성 품질 향상에도 도움이 될 거라 생각함
    Clippy나 Rust 컴파일러가 제공하는 구체적 피드백이 AI 에이전트가 실수를 줄이고 방향을 잡는 데 큰 역할을 할 수 있음