A Type Stronger than the Sum of its Components
구성 요소의 합보다 더 강한 타입
Rust의 `std::path::Component` 열거형을 각 변형을 나타내는 별도 타입으로 나눠 경로 처리 함수의 입력 조건을 타입으로 표현한 사례입니다. `NormalComponent`와 `ParentDirComponent`를 쓰면 경로 결합과 디렉터리 탐색에서 잘못된 구성 요소를 넘기는 실수를 줄일 수 있습니다.
- 주제
AI 요약
Rust에서 경로를 다루는 라이브러리를 만들던 글쓴이는 std::path::Component 열거형의 각 변형을 별도 타입으로 표현했습니다. 열거형 하나로 여러 종류의 경로 구성 요소를 전달하면 함수가 실제로 필요한 값보다 넓은 입력을 받게 됩니다. 글쓴이는 소유형 NormalComponent(OsString)을 만들어 Component::Normal에 해당하는 값만 함수에 전달하도록 했습니다.
입력 타입으로 경로 조건 표현하기
기존 함수는 join_normal(&self, path: &OsStr) -> AbsPath처럼 OsStr을 받았습니다. 하지만 OsStr에는 ..이나 절대 경로도 들어갈 수 있습니다. 절대 경로를 결합하면 대상 경로가 대체될 수 있어, 함수 이름이 약속하는 동작과 실제 입력이 어긋납니다. 반면 NormalComponent를 받으면 절대 경로에 정상 구성 요소를 더한다는 조건을 타입으로 드러낼 수 있습니다.
글쓴이는 이미 절대 경로인 AbsPath, 상대 경로인 RelativePath, 정규화된 CanonicalPath도 사용하고 있었습니다. 다만 경로는 문자열처럼 보이는 어휘적 값과 디스크상의 실제 위치가 서로 다를 수 있습니다. 상대 경로가 심볼릭 링크를 거쳐 절대 위치를 가리킬 수 있고, 경로 중간의 ..을 처리하려면 부모 디렉터리와 심볼릭 링크를 고려해야 합니다. 예를 들어 /path/to/location/skipped/..에서 skipped가 존재하지 않거나 디렉터리가 아니면 메타데이터 조회가 실패할 수 있습니다. 경로를 먼저 canonicalize하면 /path/to/location으로 바뀌어 조회 결과가 달라집니다.
경로 정규화와 플랫폼별 동작
절대 경로라고 해서 정규화된 것은 아닙니다. .과 ..이 남아 있을 수 있습니다. 실제 경로에서 심볼릭 링크의 부모 위치를 확인하지 않은 채 이를 단순히 접어 정규화하면 결과가 달라질 수 있습니다. Windows에는 \\?\로 시작하는 경로처럼 .과 ..을 일반 구성 요소로 취급하는 형식도 있습니다. canonicalize가 이런 경로를 반환할 수 있으므로, 정규화된 경로에 다시 ..을 결합하는 방식은 안전하지 않을 수 있습니다. Rust의 Component 파싱은 이 경우에도 ..을 Component::ParentDir로 분류합니다.
이 타입 설계는 디렉터리를 한 구성 요소씩 탐색하는 함수에도 쓰입니다. up(position: Reached, name: ParentDirComponent) -> (Step, Reached)는 호출자가 부모 디렉터리 구성 요소를 갖고 있음을 타입으로 요구합니다. 이전처럼 OsString을 받으면 호출자가 값을 직접 점검해야 하지만, 별도 타입을 쓰면 컴파일러가 함수 입력 조건을 확인합니다. 글쓴이는 디렉터리 항목을 DirEntry에서만 구성 요소로 만들 경우, 해당 값이 디스크에 존재하거나 존재했던 항목이라는 사실도 타입으로 표현할 수 있다고 설명합니다. 다만 파일 시스템 상태는 바뀔 수 있으므로, 경로와 메타데이터를 보관한다고 해서 이후에도 존재를 보장하지는 않는다는 고민도 덧붙입니다.
열거형 변형을 독립 타입으로
글쓴이는 이 방식을 기존 열거형의 각 변형에 맞는 타입을 만드는 설계 패턴으로 설명합니다. 여러 선택지 중 하나를 표현하는 합 타입(sum type)이 유용하다면, 그중 특정한 한 종류만 허용하는 타입도 유용할 수 있다는 생각입니다. 댓글에서는 Component에서 NormalComponent로 바꾸는 함수가 모든 변형을 처리할 수 없으므로 Option<NormalComponent> 같은 반환값이 필요하다는 논의가 이어집니다. 글쓴이는 OsString을 할당해 소유 타입으로 변환하며, 먼저 별도 열거형을 만든 뒤 구조체를 추출했다고 답합니다.
Reddit 반응
- @u/Comun4 — 좋은 글입니다! 요즘 파일 시스템 작업을 많이 하는데 운영체제 경로를 다루는 일은 정말 까다롭습니다.
- @u/schneems — 감사합니다! 매일 쓰는 것에도 예상하지 못한 깊이와 예외 사례가 있다는 점이 놀랍습니다. Rust를 몇 년 썼고 타입 시스템을 최대한 활용하고 싶지만, 디스크 내용처럼 관찰하지 않는 사이 바뀔 수 있는 대상을 표현하는 데 어려움을 겪었습니다. 경로와 메타데이터를 함께 둔 구조체로 파일이 존재한다고 증명하려 했지만, 실제 상태를 확인하기가 쉽지 않았습니다. 경로가 존재하고 상위 디렉터리를 나열할 수 있어도 접근 권한이 없어 메타데이터를 읽지 못할 수 있습니다.
NormalComponent를 찾은 뒤에는 디렉터리 경로와 정상 구성 요소를 조합해 디렉터리 항목을 표현할 수 있었습니다.DirEntry에서만 만들면 메타데이터를 보관하지 않아도 해당 항목이 디스크에 존재하거나 존재했던 값이라는 속성을 표현합니다. - @u/SmartAsFart — Rust로 외부 장치 인터페이스를 많이 작성합니다. 장치마다 자체 상태가 있습니다. 바뀔 수 있는 대상이 관찰하지 않는 동안에도 그대로라고 가정하는 방식은 매우 좋지 않다는 점을 알게 됐습니다. 그 사실을 받아들이고 Rust 객체가 외부 대상 자체가 아니라 인터페이스임을 분명히 해야 합니다. 파일 그 자체가 아니라 파일 핸들인 셈입니다. 겉모습만 씌우는 추상화를 없애는 편이 훨씬 말이 됩니다.
- @u/schneems — 감사합니다! 매일 쓰는 것에도 예상하지 못한 깊이와 예외 사례가 있다는 점이 놀랍습니다. Rust를 몇 년 썼고 타입 시스템을 최대한 활용하고 싶지만, 디스크 내용처럼 관찰하지 않는 사이 바뀔 수 있는 대상을 표현하는 데 어려움을 겪었습니다. 경로와 메타데이터를 함께 둔 구조체로 파일이 존재한다고 증명하려 했지만, 실제 상태를 확인하기가 쉽지 않았습니다. 경로가 존재하고 상위 디렉터리를 나열할 수 있어도 접근 권한이 없어 메타데이터를 읽지 못할 수 있습니다.
- @u/JoshTriplett — 잘하셨습니다. Rust 표준 라이브러리의 경로 지원 대부분을 다시 작성할 사람이 필요합니다. 참여할 생각이 있으신가요? :)
- @u/schneems — 감사합니다! 더 알아보고 참여하고 싶습니다. 다음 단계로 무엇을 하면 좋을까요? Zulip에서는 Richard Schneeman이라는 이름을 쓰고 있습니다. 아직 플랫폼을 익히는 중입니다. GitHub 계정은 Reddit과 같은 핸들입니다.
- @u/teerre —
Component::Normal에서NormalComponent로 가는 변환을 어떻게 전체 함수(total function)로 만드나요? 분명 오류가 있을 것 같습니다.- @u/schneems — 여기서 ‘전체 함수’가 무슨 뜻인지 잘 모르겠습니다.
NormalComponent는 소유 타입입니다.&OsStr에서 변환할 때OsString을 할당합니다. 제 코드에서는 먼저 자체 열거형을 만든 뒤 구조체를 추출합니다. - @u/ZeroXbot — 참고로 ‘전체 함수’는 일부 정의역 입력에 대해 값을 내놓지 않을 수 있는 부분 함수와 대비되는 수학 용어입니다.
- @u/teerre —
fn convert(a: Component) -> NormalComponent같은 함수를 생각했습니다.Component::Root는Component::Normal로 만들 수 없으니 panic 없이 구현할 수 없다고 봤습니다. 코드를 보니 그런 함수가 아니라Component -> Option<NormalComponent>이군요.
- @u/schneems — 여기서 ‘전체 함수’가 무슨 뜻인지 잘 모르겠습니다.
- @u/thicket — 공유해 주셔서 감사합니다. 신중하게 타입을 설계하면 어떤 보장을 얻는지 잘 보여주는 사례입니다.
- @u/simon_o — 이해가 됩니다. 8년 전에 비슷한 방식을 시도했습니다.
AbsolutePath,RelativePath와 자리표시자 방식을 두면/나C:에서 시작하는 경로뿐 아니라config/cache/...처럼 역직렬화하는 시스템에 맞춰 해석할 경로도 표현할 수 있습니다. 여러 운영체제에서 쓸 수 있는 경로를 설정 파일에 담는 것이 목적이었습니다. - @u/BoltActionPiano — 열거형 값의 부분집합도 익명 타입으로 표현할 수 있으면 좋겠습니다. 함수가 열거형의 일부 변형만 받거나
fn foo(x: Component::Normal) -> Component::RootDir처럼 쓸 수 있으면 합니다. 열거형을 정의하면 각 변형의 타입도 자동으로 생기는 방식도 좋겠습니다.
원문: Schneems.com / 번역·요약: Trawling