트레이트에는 메서드 시그니처만을 선언할 수도 있고, 구현부(본체)를 가진 메서드를 포함할 수도 있다. 메서드 구현부가 선언된 트레이트 메서드를 디폴트 메서드라고 부른다. 메서드 구현부까지 미리 정의해 두면 해당 트레이트를 구현하는 타입 입장에서는 메서드 구현을 선택적으로 재정의하거나 그대로 사용할 수 있는 편의성을 얻게 된다. 디폴트 메서드는 재사용 가능성과 유연성을 모두 확보하기 위한 수단이며, 러스트의 제너릭 프로그래밍에서 중요한 요소가 된다.
디폴트 메서드를 정의하려면 트레이트 선언부에서 메서드 몸체를 작성하면 된다. 트레이트 구현 시 이 메서드를 재정의하지 않으면 트레이트에 정의된 구현이 그대로 적용된다. 예를 들어 아래와 같이 디폴트 메서드를 가진 트레이트를 정의할 수 있다.
위 예시에서 say_hello 메서드는 본체가 제공된 디폴트 메서드이고, say_goodbye 메서드는 구현을 강제하는 필수 메서드다. 이 트레이트를 구현하는 타입은 say_hello를 재정의하지 않으면 "Hello!"를 출력하는 디폴트 구현을 그대로 사용한다. 만약 요구사항에 맞춰 동작을 바꾸고 싶다면 아래와 같이 재정의할 수 있다.
struct Person {
name: String
}
impl Greet for Person {
fn say_hello(&self) {
println!("안녕하라, 제 이름은 {}이다!", self.name);
}
fn say_goodbye(&self) {
println!("잘 가세요!");
}
}
위 구현에서는 say_hello가 디폴트 메서드 대신 Person에 맞는 방식으로 재정의되었다. say_goodbye는 트레이트에서 본체가 없는 메서드였으므로 반드시 구현해야 한다. 이렇게 디폴트 메서드가 있으면 구현자가 필요한 메서드만 오버라이딩하여 자신에게 맞는 고유 동작을 부여할 수 있다.
디폴트 메서드 내부에서는 트레이트에 정의된 다른 메서드를 호출할 수 있다. 이때 호출되는 메서드가 디폴트 메서드라면 그 메서드 역시 트레이트 구현에서 재정의될 수 있음을 주의해야 한다. 즉, 트레이트에 정의된 디폴트 메서드가 서로 의존할 수도 있으므로, 호출되는 쪽이 구현 타입에서 재정의된다면 호출하는 쪽의 동작도 자동으로 바뀔 수 있다. 이는 트레이트 설계 시 제공하고자 하는 기본 동작과 구현자가 수정해야 하는 부분을 신중히 구분해야 함을 의미한다.
트레이트 객체 사용 시에는 객체 안전성(object safety)을 고려해야 할 때가 있다. 디폴트 메서드가 있다고 해서 자동으로 객체 안전성이 보장되는 것은 아니지만, 디폴트 메서드 자체가 객체 안전성을 깨뜨리는 것은 아니다. 트레이트 객체에서 호출할 수 있는지 여부는 트레이트의 다른 요건(예: Self: Sized 제한이 없는지 등)에 좌우된다. 이를 위해서는 필요한 트레이트 메서드가 모두 객체 안전한 방식으로 정의되어야 한다.
디폴트 메서드는 트레이트가 제공하는 편의 기능으로서, 공통 동작을 쉽게 공유하고 동시에 사용자가 상황에 맞게 구현을 갈아끼울 수 있게 해준다. 이런 특성 덕분에 러스트에서 제너릭 타입과 트레이트가 결합할 때 재사용성이 크게 높아지고, 다양한 형태의 추상화를 자연스럽게 구성할 수 있게 된다. 디폴트 메서드를 통해 트레이트가 제공하는 ‘기본 동작’과 각 타입의 ‘구체적 구현’을 적절히 조합한다면, 코드 중복을 줄이고 일관된 인터페이스를 유지할 수 있다.