트레이트 경계 문법
Coddy Rust 여정의 Object Oriented Programming 섹션에 포함된 레슨 — 61개 중 40번째.
지금까지 여러분은 트레이트(trait)를 정의하고 이를 구조체(struct)에 구현하는 방법을 배웠습니다. 또한 유연한 코드를 작성하기 위해 제네릭(generics)을 사용하는 방법도 살펴보았습니다. 이제 이 개념들을 결합할 차례입니다. 즉, 트레이트를 사용하여 제네릭 함수가 허용할 수 있는 타입을 제한(constrain)하는 방법을 알아보겠습니다.
매개변수에 대해 특정 메서드를 호출해야 하는 제네릭 함수를 생각해 봅시다. 아무런 제약 조건이 없다면, Rust는 타입 T가 해당 메서드를 가지고 있다는 것을 보장할 수 없습니다:
fn print_info<T>(item: T) {
println!("{}", item.summarize()); // 오류! T에 summarize()가 없을 수도 있습니다
}
해결책은 트레이트 바운드(trait bound)입니다. 제네릭 매개변수 뒤에 : TraitName을 추가함으로써, Rust에게 T가 해당 트레이트를 구현해야 한다고 알려줍니다:
trait Summary {
fn summarize(&self) -> String;
}
fn print_info<T: Summary>(item: T) {
println!("{}", item.summarize()); // 이제 Rust는 이 메서드가 존재한다는 것을 압니다
}
<T: Summary> 구문은 "T는 Summary를 구현하는 모든 타입"으로 읽힙니다. 이제 이 함수는 필요한 동작을 가진 타입만 허용합니다. 만약 Summary를 구현하지 않은 타입을 전달하려고 하면, 컴파일러가 명확한 오류와 함께 이를 거부할 것입니다.
이 패턴은 특정 기능에 여전히 접근하면서도 제네릭 코드를 작성할 수 있게 해주기 때문에 강력합니다. 당신의 함수는 유연하게 유지됩니다. 즉, 어떤 타입과도 작동하지만, 오직 당신이 필요로 하는 동작을 제공하는 타입들하고만 작동합니다.
챌린지
쉬움트레이트 바운드(trait bounds)를 사용하여 적절한 형식을 갖춘 메시지만 전송할 수 있도록 보장하는 알림 시스템을 만들어 봅시다! 특정 트레이트를 구현하는 모든 타입을 허용하는 제네릭 함수를 생성하여, 트레이트 바운드가 제네릭 매개변수를 어떻게 제한하는지 실습해 볼 것입니다.
코드는 두 개의 파일로 구성됩니다:
notification.rs:&self를 매개변수로 받고String을 반환하는message메서드를 가진 공개(public)Notify트레이트를 정의하세요. 그런 다음 이 트레이트를 구현하는 두 개의 공개 구조체를 만드세요:Email— 공개subject필드(String)를 가집니다.message메서드는"Email: {subject}"를 반환해야 합니다.SMS— 공개content필드(String)를 가집니다.message메서드는"SMS: {content}"를 반환해야 합니다.
Notify트레이트를 구현하는 모든 타입T를 허용하는send_notification이라는 공개 제네릭 함수를 만드세요. 이 함수는 해당 아이템의message()를 호출한 결과를 출력해야 합니다.main.rs: notification 모듈을 가져와서 제공된 입력을 사용하여Email과SMS인스턴스를 모두 생성하세요. 각 인스턴스에 대해send_notification을 호출하여 제네릭 함수가Notify를 구현하는 모든 타입과 함께 작동함을 증명하세요.
여기서 핵심은 send_notification이 수신하는 구체적인 타입을 알지 못한다는 점입니다. 단지 해당 타입이 메시지를 생성할 수 있다는 것만 알 뿐입니다. 트레이트 바운드 T: Notify가 이러한 기능을 보장합니다.
출력 결과는 두 알림이 모두 전송되는 것을 보여주어야 합니다:
Email: {subject}
SMS: {content}예를 들어, 입력값이 Meeting Tomorrow와 On my way!인 경우:
Email: Meeting Tomorrow
SMS: On my way!이메일 제목과 SMS 내용이라는 두 개의 입력을 받게 됩니다.
치트 시트
트레이트 바운드(trait bound)는 제네릭 타입 매개변수가 특정 트레이트를 구현하는 타입만 허용하도록 제한합니다.
트레이트 바운드 문법은 제네릭 매개변수 뒤에 : TraitName을 사용합니다:
fn function_name<T: TraitName>(item: T) {
// 이제 item에서 TraitName의 메서드를 호출할 수 있습니다
}
트레이트 바운드를 사용한 예시:
trait Summary {
fn summarize(&self) -> String;
}
fn print_info<T: Summary>(item: T) {
println!("{}", item.summarize());
}
<T: Summary> 표기법은 "T는 Summary를 구현하는 모든 타입"을 의미합니다. 이를 통해 함수는 제네릭을 유지하면서 트레이트 메서드를 호출할 수 있습니다.
직접 해보기
mod notification;
use notification::{Email, SMS, send_notification};
fn main() {
// 입력 읽기
let mut subject = String::new();
std::io::stdin().read_line(&mut subject).expect("Failed to read line");
let subject = subject.trim().to_string();
let mut content = String::new();
std::io::stdin().read_line(&mut content).expect("Failed to read line");
let content = content.trim().to_string();
// TODO: 제목을 포함한 Email 인스턴스 생성
// TODO: 내용을 포함한 SMS 인스턴스 생성
// TODO: email을 매개변수로 send_notification 호출
// TODO: sms를 매개변수로 send_notification 호출
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.