C - 파생 가능한 트레이트
derive로 Debug·비교·정렬·복제·해시·기본값 트레이트를 자동 구현할 때 타입이 충족해야 할 조건을 정리합니다.
derive는 러스트에서 코드를 줄여 주는 편의 기능으로 자주 소개되지만, 실제로는 타입 설계 의도를 드러내는 중요한 장치입니다.
어떤 트레이트를 자동 구현할지 결정하는 순간, 해당 타입의 비교 가능성·정렬 가능성·디버깅 전략이 함께 정해집니다.
그래서 이 부록은 무엇을 자동화할 수 있는가를 넘어서 어떤 의미를 타입에 부여하는가를 확인하는 참고서로 읽는 것이 좋습니다.
본문에서 트레이트 선택이 애매할 때 이 절을 참조하면 설계 결정을 더 일관되게 만들 수 있습니다.
자동 파생이 편리하더라도, 도메인 규칙과 충돌하는 경우에는 수동 구현이 더 정확한 선택이 될 수 있습니다.
특히 비교/정렬 트레이트는 컬렉션 동작과 직접 연결되므로 파생 조건을 명확히 이해하고 사용해야 합니다.
이 절을 기준으로 파생 가능성과 설계 의도를 함께 검토하면 타입 모델의 예측 가능성이 높아집니다.
결과적으로 자동화 편의성과 도메인 정확성 사이의 균형을 더 잘 맞출 수 있습니다.
Rust · derive · dependency graph
내장 파생 9개의 선행 계약을 연결한다
Copy는 Clone을, Ord는 Eq와 PartialOrd를 요구합니다. Eq와 PartialOrd는 각각 PartialEq에 모이며, 나머지 세 트레이트는 내장 트레이트 선행 조건이 없습니다.
Copy → Clone
Copy는 Clone의 하위 트레이트입니다. 모든 필드도 Copy여야 하고 타입은 Drop을 구현할 수 없습니다.
Ord → Eq + PartialOrd → PartialEq
Eq와 PartialOrd가 각각 PartialEq를 요구하므로 비교 계약은 한 지점으로 모입니다.
Debug
{:?} 프로그래머 출력을 만듭니다. 사용자 표시용 Display는 내장 파생 대상이 아닙니다.
Default
구조체는 모든 필드가 Default여야 하며, 열거형은 유닛 배리언트 하나를 #[default]로 고릅니다.
Hash
내장 트레이트 상속선은 없지만 해시 키에서는 Eq와 함께 동등한 값의 해시가 같다는 계약을 지킵니다.
- 화살표는 직접 요구하는 트레이트를 가리킴
PartialEq는 fan-in 2인 공통 의존점
복제 계약
Clone은 타입별 명시 복제이고 깊은 복사를 보장하지 않습니다. Copy는 저장 위치와 무관한 암시적 비트 복사입니다.
정렬 계약
구조체는 필드 순서, 열거형은 판별값을 우선 비교합니다. 명시적 판별값은 기본 선언 순서를 바꿀 수 있습니다.
목록의 경계
내장 파생은 정확히 아홉 개입니다. 라이브러리는 절차적 매크로로 사용자 정의 파생을 추가할 수 있습니다.
선이 없다는 것은 트레이트가 쓸모없다는 뜻이 아니라 다른 내장 트레이트를 선행 조건으로 요구하지 않는다는 뜻입니다.
부록 C: 파생 가능한 트레이트
이 책의 여러 곳에서 구조체나 열거형 정의에 적용할 수 있는 derive 속성에 대해 설명했습니다.
derive 속성은 derive 문법으로 명시한 타입에 대해 자체적인 기본 구현이 있는 트레이트를 구현하는 코드를 생성합니다.
이 부록에서는 derive와 함께 사용할 수 있는 표준 라이브러리의 모든 트레이트에 대한 참고 자료를 제공합니다.
각 절이 다룰 내용은 다음과 같습니다.
- 이 트레이트를 파생하면 어떤 연산자와 메서드가 활성화되는지
derive가 제공하는 트레이트의 구현체가 하는 일- 타입에 대해 트레이트를 구현한다는 것의 의미
- 트레이트를 구현할 수 있는 조건 혹은 허용되지 않는 조건
- 트레이트가 필요한 연산들의 예
derive 속성이 제공하는 것과는 다른 동작을 원한다면, 각 트레이트에 대한
표준 라이브러리 문서를 참고하여
수동으로 구현하는 방법에 대한 자세한 내용을 확인하세요.
러스트의 내장 파생 트레이트는 Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd 아홉 가지입니다.
표준 라이브러리에 정의된 다른 트레이트에는 적절한 기본 동작이 없으므로, 달성하려는 목적에 적합한 방식으로 구현하는 것은 여러분의 몫입니다.
파생될 수 없는 트레이트의 예로는 Display가 있는데, 이는 최종 사용자를 위한 서식을 처리합니다.
여러분은 최종 사용자에게 타입을 표시할 적절한 방법을 항상 고려해야 합니다.
최종 사용자에게 어떤 부분을 표시해야 할까요?
어떤 부분이 관련성이 있을까요?
어떤 형식의 데이터가 가장 관련성이 높을까요?
러스트 컴파일러에는 이러한 인사이트가 없으므로 적절한 기본 동작을 제공할 수 없습니다.
이 부록에 나열된 트레이트가 파생 가능한 트레이트를 전부 포괄하지는 않습니다.
라이브러리는 자신의 트레이트에 대한 derive를 구현할 수 있으므로, derive를 사용할 수 있는 트레이트 목록은 정말 개방적입니다.
derive를 구현하려면 절차적 매크로를 사용해야 하며, 이는 18장의 ‘매크로’절에서 다룹니다.
프로그래머 출력을 위한 Debug
Debug 트레이트는 형식 문자열에서 디버그 서식을 활성화하는데, 이는 {}
자리표시자 내에 :?를 추가하여 표시합니다.
Debug 트레이트는 디버깅 목적으로 어떤 타입의 인스턴스를 출력할 수 있게
해주므로, 여러분과 여러분의 타입을 사용하는 다른 프로그래머들은 프로그램의
실행 중 특정 지점에서 인스턴스를 검사할 수 있습니다.
Debug 트레이트는 이를테면 assert_eq! 매크로를 사용할 때 필요합니다.
이 매크로는 동등 단언이 실패했을 경우 인스턴스의 값을 출력하여 프로그래머가 두 인스턴스가 같지 않은 이유를 확인할 수 있도록 해 줍니다.
동등 비교를 위한 PartialEq 및 Eq
PartialEq 트레이트는 타입의 인스턴스를 비교하여 동등 여부를 확인하고
==와 != 연산자를 사용할 수 있게 해 줍니다.
PartialEq를 파생시키면 eq 메서드가 구현됩니다.
PartialEq가 구조체에 파생되면, 두 인스턴스는 모든 필드가 동일할 때만 동일하고, 필드 중 어느 하나라도 동일하지 않다면 두 인스턴스는 동일하지 않습니다.
열거형에 파생되면, 두 값이 같은 배리언트이고 포함된 모든 필드가 같을 때만 동일하며 다른 배리언트끼리는 동일하지 않습니다.
예를 들면 PartialEq 트레이트는 assert_eq! 매크로를 사용할 때 필요한데,
이 매크로는 두 인스턴스를 비교하여 동등 여부를 확인할 수 있어야 하기
때문입니다.
Eq 트레이트에는 메서드가 없습니다.
이 트레이트의 목적은 어노테이션된 타입의 모든 값에 대해 해당 값이 자기 자신과 동일하다는 것을 나타내는 것입니다.
Eq 트레이트는 PartialEq를 구현한 타입에만 적용할 수 있지만, PartialEq를 구현한 모든 타입이 Eq를 구현할 수 있는 것은 아닙니다.
Eq를 파생하려면 구조체의 모든 필드와 열거형 배리언트의 모든 필드도 Eq를 구현해야 합니다.
이에 대한 한 가지 예는 부동 소수점 숫자 타입입니다.
부동 소수점 숫자 구현은 not-a-number(NaN) 값의 두 인스턴스가 서로 동일하지 않다고 명시합니다.
Eq가 필요한 예시로는 HashMap<K, V>의 키가 있습니다. 해시 키는 Eq + Hash를 구현해야 하며, HashMap<K, V>는 동등 비교와 해시 결과를 함께 사용해 키를 찾습니다.
순서 비교를 위한 PartialOrd 및 Ord
PartialOrd 트레이트는 정렬 목적을 위하여 타입의 인스턴스를 비교할 수 있게 해줍니다.
PartialOrd를 구현한 타입은 <, >, <=, >= 연산자를 사용할 수 있습니다.
PartialOrd 트레이트는 PartialEq를 구현한 타입에만 적용할 수 있습니다.
PartialOrd 트레이트를 파생시키면 partial_cmp 메서드가 구현되는데, 이는 Option<Ordering>을 반환하며, 반환 값은 주어진 값이 순서를 정의하지 않을 때 None이 됩니다.
이 트레이트를 구현한 타입의 대부분의 값은 비교할 수 있지만, 순서를 정의하지 않는 값의 예가 있다면 not-a-number(NaN) 부동 소수점 값입니다.
NaN 부동 소수점 숫자와 어떤 부동 소수점 숫자를 사용하여 partial_cmp를 호출하면 None이 반환될 것입니다.
구조체에 대해 파생되면, PartialOrd는 구조체 정의에서 필드가 나타나는 순서대로 각 필드의 값을 비교하는 방식으로 두 인스턴스를 비교합니다.
열거형에 대해 파생되면 배리언트는 우선 판별값(discriminant)으로 비교되고, 같은 배리언트라면 필드 순서로 비교됩니다. 기본 판별값은 선언 순서를 따르지만, 명시적 판별값을 지정할 수 있는 열거형에서는 그 값이 기본 순서를 바꿀 수 있습니다.
Ord 트레이트는 어떤 두 값에 대해 항상 유효한 순서가 존재한다는 것을 알려줍니다.
Ord 트레이트는 cmp 메서드를 구현하는데, 이는 Option<Ordering> 이 아닌 Ordering을 반환합니다.
이는 항상 유효한 순서가 존재하기 때문입니다.
Ord 트레이트는 PartialOrd와 Eq를 구현한 타입에만 적용할 수 있습니다. (Eq는 PartialEq를 필요로 합니다.)
구조체와 열거형에 대해 파생되면, cmp는 PartialOrd의 partial_cmp에 대한 파생 구현체가 동작하는 방식과 동일하게 동작합니다.
Ord가 필요한 한 가지 예는 BTreeSet<T>에 값을 저장할 때인데, 이는
값의 정렬 순서에 따라 데이터를 저장하는 데이터 구조입니다.
값을 복제하기 위한 Clone과 Copy
Clone 트레이트는 값을 명시적으로 복제할 수 있게 하며, 타입별 구현은 임의의 코드를 실행할 수 있습니다. String처럼 힙 데이터를 복제하는 타입도 있지만, 참조나 스마트 포인터처럼 같은 기반 데이터를 가리킬 수도 있으므로 Clone이 항상 깊은 복사를 보장하는 것은 아닙니다.
Clone에 대한 더 자세한 내용은 3장의 ‘변수와 데이터 간 상호작용 방식. 클론(clone)’ 절을 참고하세요.
Clone을 파생하면 clone 메서드가 구현되는데, 이는 타입 전체에 대해 구현될 때 타입의 각 부분에 대해 clone을 호출합니다.
이는 Clone을 파생하기 위해서는 타입의 모든 필드나 값 또한 Clone을 구현해야 한다는 것을 의미합니다.
Clone이 필요한 경우의 한 예에는 슬라이스에 to_vec 메서드를 호출할 때가 있습니다.
슬라이스는 자신이 가진 타입 인스턴스를 소유하지 않지만, to_vec에서 반환된 벡터는 자신의 인스턴스를 소유해야 하므로, to_vec은 각 아이템에 대해 clone을 호출합니다.
따라서 슬라이스에 저장된 타입은 Clone을 구현해야 합니다.
Copy 트레이트는 값의 저장 위치와 무관하게 임의의 코드 없이 비트를 복사하는 의미를 부여합니다.
Copy에 대한 더 자세한 내용은 3장의 ‘값의 복사(copy) 의미’절을 참고하세요.
Copy 트레이트에는 아무 메서드도 정의되어 있지 않은데, 이는 프로그래머가 메서드를 오버로딩하고 임의의 코드를 실행하지 않는다는 가정을 위반하는 것을 방지하기 위해서입니다.
따라서 모든 프로그래머는 복사가 매우 빠르게 수행될 것이라고 가정할 수 있습니다.
어떤 타입에 대해 Copy를 파생하려면 그 타입의 모든 부분이 Copy를 구현해야 하며, Drop을 구현한 타입에는 Copy를 구현할 수 없습니다.
Copy를 구현하는 타입은 또한 Clone을 구현해야 하는데, 왜냐하면 Copy를 구현하는 타입은 Copy와 동일한 작업을 수행하는 Clone의 단순한 구현체를 가지고 있기 때문입니다.
Copy 트레이트는 드물게만 요구됩니다; Copy를 구현하는 타입은
최적화가 가능하므로 clone을 호출할 필요가 없으며, 이는 코드를 더
간결하게 만듭니다.
Copy를 사용하여 가능한 모든 것은 Clone을 사용하여 수행할 수 있지만, 코드가
느려지거나 clone을 사용해야 하는 경우가 있을 수 있습니다.
또한 Copy 구현 여부는 공개 API의 약속입니다. 나중에 Copy가 아닌 필드를 추가해 구현을 제거하면 사용자 코드가 깨질 수 있으므로 처음부터 장기적인 타입 설계를 고려합니다.
어떤 값을 고정 크기의 값으로 매핑하기 위한 Hash
Hash 트레이트는 해시 함수를 사용하여 임의의 크기를 가진 타입의 인스턴스를 고정 크기의 값으로 매핑할 수 있게 해 줍니다.
Hash를 파생하면 hash 메서드가 구현됩니다.
파생된 hash 메서드의 구현체는 타입의 각 부분에 대해 hash를 호출한 결과를 조합하는데, 이는 Hash를 파생하기 위해서는 타입의 모든 필드 혹은 값 또한 Hash를 구현해야 한다는 것을 의미합니다.
Hash를 필요로 하는 한 가지 예는 효율적으로 데이터를 저장하기 위하여
HashMap<K, V>에 키를 저장할 때입니다.
Hash와 Eq 사이에는 상속 관계가 없지만, 해시 키는 k1 == k2이면 두 값의 해시도 같아야 한다는 계약을 지켜야 합니다.
기본값을 위한 Default
Default 트레이트는 타입에 대한 기본값을 생성할 수 있게 해 줍니다.
Default를 파생하면 default 함수가 구현됩니다.
구조체에 파생된 default 함수의 구현체는 각 필드에 대해 default를 호출하므로, 구조체의 모든 필드가 Default를 구현해야 합니다.
Default::default 함수는 일반적으로 4장의 ‘기존 인스턴스를 이용해 새 인스턴스를 만들 때 구조체 업데이트 문법 사용하기’절에서 다룬 구조체 업데이트 구문과 함께 사용됩니다.
구조체의 몇 개의 필드만 커스터마이징한 다음 나머지 필드에 대해 기본값을 설정하고 사용하려면 ..Default::default()를 사용할 수 있습니다.
Default 트레이트는 예를 들면 Option<T> 인스턴스에서 unwrap_or_default 메서드를 사용할 때 필요합니다.
Option<T>가 None이면, unwrap_or_default 메서드는 Option<T>에 저장되는 T 타입에 대한 Default::default의 결과를 반환합니다.
열거형에 Default를 파생할 때는 기본으로 선택할 유닛 배리언트 하나에 #[default]를 붙여야 하며, 그 배리언트에는 #[non_exhaustive]를 붙일 수 없습니다. 데이터가 있는 배리언트를 기본값으로 삼으려면 직접 구현합니다.
파생 가능한 트레이트는 구조체와 열거형에 어떤 기본 동작을 자동으로 부여할지 고르는 기준표로 쓰면 좋습니다.
마지막으로 파생 트레이트를 빠르게 다시 찾을 수 있도록 의미, 조건, 탈락 기준을 한 장으로 묶었습니다.
Rust · derive · semantic comparison
derive는 반복 구현보다 타입의 약속을 만든다
필드 조건이 컴파일되더라도 자동 구현의 의미가 도메인 규칙과 맞는지 따로 확인합니다. 비교·복제·해시·기본값은 컬렉션 동작과 공개 API에 이어지는 계약입니다.
| 트레이트 | 자동 구현의 의미 | 파생 조건 | 설계 약속과 주의 |
|---|---|---|---|
Debug |
{:?}와 {:#?}의 프로그래머용 출력을 만듭니다. |
모든 필드가 Debug를 구현합니다. |
최종 사용자 서식인 Display는 내장 파생되지 않습니다. |
Clone |
각 부분의 clone을 호출해 값을 명시적으로 복제합니다. |
모든 필드가 Clone을 구현합니다. |
복제 방식은 타입별이며 같은 기반 데이터를 가리킬 수도 있어 깊은 복사를 보장하지 않습니다. |
Copy |
대입과 인수 전달에서 저장 위치와 무관한 암시적 비트 복사 의미를 줍니다. | 모든 필드가 Copy, 타입이 Clone, Drop 미구현이어야 합니다. |
구현 여부가 공개 API 약속이므로 나중에 제거하면 호환성을 깰 수 있습니다. |
PartialEq |
==, !=와 필드별 동등 비교를 만듭니다. |
모든 필드가 PartialEq를 구현합니다. |
어떤 필드를 같은 값의 기준으로 볼지 도메인 의미와 맞아야 합니다. |
Eq |
모든 값이 자기 자신과 같다는 반사적 동등성을 표시합니다. | 파생하려면 PartialEq와 모든 필드의 Eq가 필요하며, 모든 값에 반사성이 성립해야 합니다. |
NaN처럼 자기 자신과 같지 않은 값이 있으면 적용할 수 없습니다. |
PartialOrd |
<, <=, >, >=와 Option<Ordering> 비교를 만듭니다. |
PartialEq와 모든 필드의 PartialOrd가 필요합니다. |
구조체는 필드 순서, 열거형은 판별값을 우선 비교하며 일부 값은 순서가 없을 수 있습니다. |
Ord |
모든 두 값에 유효한 Ordering을 반환하는 전체 순서를 만듭니다. |
Eq, PartialOrd와 모든 필드의 Ord가 필요합니다. |
열거형의 명시적 판별값은 기본 선언 순서보다 우선할 수 있습니다. |
Hash |
각 부분을 해셔에 공급하는 hash 구현을 만듭니다. |
모든 필드가 Hash를 구현합니다. |
Eq를 상속하지 않지만 해시 키는 k1 == k2이면 같은 해시를 내야 합니다. |
Default |
각 부분의 기본값으로 Default::default()를 만듭니다. |
구조체의 모든 필드가 Default여야 합니다. 열거형은 #[non_exhaustive]가 아닌 유닛 배리언트 하나에 #[default]를 붙입니다. |
빈 값이 아니라 도메인에서 실제로 유효하고 유용한 시작 상태인지 확인합니다. |
Debug
{:?} 프로그래머 출력 · 모든 필드 Debug · 사용자용 Display는 별도 구현
Clone
각 필드를 명시 복제 · 모든 필드 Clone · 타입별 복제이며 깊은 복사 보장 없음
Copy
암시적 비트 복사 · 모든 필드 Copy + Clone + Drop 불가 · 공개 API 약속
PartialEq
==와 != · 모든 필드 PartialEq · 동등성 기준이 도메인 의미와 맞아야 함
Eq
반사적 동등성 · 파생 시 PartialEq와 모든 필드의 Eq 필요 · NaN처럼 자기 자신과 다른 값은 부적합
PartialOrd
부분 순서와 Option<Ordering> · PartialEq 필요 · 필드 순서와 열거형 판별값 확인
Ord
전체 순서 · Eq + PartialOrd 필요 · 명시적 판별값이 열거형 순서를 바꿀 수 있음
Hash
필드별 해시 · 모든 필드 Hash · Eq 상속은 아니지만 동등한 키의 해시는 같아야 함
Default
기본 상태 · 구조체의 모든 필드 Default · 열거형은 #[non_exhaustive]가 아닌 유닛 #[default] 선택
자동 구현을 버릴 때
식별자 일부를 비교에서 빼거나 사용자에게 별도 형식으로 보여 주는 등 도메인 규칙이 필드별 기본 동작과 다르면 수동 구현을 선택합니다.
컬렉션 계약을 함께 보기
HashMap 키는 Eq + Hash, BTreeSet 값은 Ord를 요구합니다. 트레이트 조합은 저장 구조의 정확성과 연결됩니다.
내장 파생 목록은 아홉 개지만 라이브러리는 사용자 정의 derive 매크로를 추가할 수 있습니다. 이 표는 내장 구현의 의미만 다룹니다.