트레이트로 동작 공유하기

RustBeginner
지금 연습하기

소개

서로 관련이 없는 데이터 타입도 동일한 동작을 보장해야 할 때가 있습니다. 온실 센서와 배송 정보는 필드가 서로 다르지만, 둘 다 현재 상태를 설명할 수 있습니다. Rust 트레이트는 데이터 구조를 동일하게 만들지 않고도 이러한 공통 기능의 이름을 지정합니다.

이 실습에서는 간단한 트레이트 하나를 정의하고, 준비된 두 구조체에 구현한 다음, 두 값을 하나의 제네릭 보고 함수에 전달합니다. 더 긴 구조체와 샘플 데이터는 이미 작성되어 있으므로 각 수정 작업에 집중할 수 있습니다.

공통 상태 계약 정의하기

이 단계에서는 서로 관련이 없는 타입들이 공유하는 동작의 이름을 지정하는 트레이트를 정의합니다.

준비된 패키지 디렉터리로 이동한 다음 전체 기본 코드를 확인합니다. sed -n 명령은 지정한 줄을 수정하지 않고 출력합니다.

cd /home/labex/project/status-reporter
sed -n '1,160p' src/main.rs

SensorShipment는 서로 다른 필드를 가집니다. 두 구조체에 준비된 텍스트 필드는 &'static str을 사용합니다. 이는 프로그램 전체에서 유효한 문자열 리터럴을 빌려온다는 뜻입니다. 'static 수명 표기는 이 트레이트 학습의 범위가 아니므로 의도적으로 나중에 다룹니다. 여기에서는 이를 수정하거나 자세히 이해할 필요가 없습니다.

자리 표시자 아래에 있는 show_status는 이미 제네릭 함수로 작성되어 있습니다. T: Status를 세 부분으로 나누어 읽어 보세요. T는 구체적인 타입을 뜻하고, 콜론은 “만족해야 한다”는 의미이며, Status는 필요한 트레이트입니다. 쉽게 말하면 “TStatus를 구현한다는 조건에서 모든 타입 T에 대해”라는 뜻입니다. 이 약속이 있기 때문에 함수에서 item.status()를 안전하게 호출할 수 있습니다.

Rust가 바운드를 이해하려면 트레이트가 먼저 존재해야 합니다. 소스 파일을 엽니다.

nano src/main.rs

// STATUS_TRAIT를 다음 코드로 바꿉니다.

trait Status {
    fn status(&self) -> String;
}

메서드 선언이 세미콜론으로 끝나는 이유는 트레이트가 필요한 메서드를 선언할 뿐, 구현을 하나로 정하지 않기 때문입니다. &self를 사용하면 소유권을 가져오지 않고 값의 내용을 확인할 수 있으며, String을 사용하면 각 타입이 자체 상태 텍스트를 만들 수 있습니다. 이후의 각 구현에서는 이 메서드 이름, 리시버, 반환 타입을 정확히 동일하게 작성한 뒤 자체 본문을 제공해야 합니다.

Ctrl+O를 눌러 저장하고 Enter를 누른 다음 Ctrl+X로 종료합니다. 그런 다음 패키지를 확인합니다.

cargo check

아직 트레이트와 준비된 항목을 사용하지 않았다는 경고가 표시될 수 있습니다. 중요한 증거는 마지막의 Finished 줄입니다. 이 줄이 표시되면 공통 계약과 제네릭 바운드가 유효하다는 뜻입니다. 다음 두 단계에서 구현과 호출을 추가합니다.

SensorStatus 구현하기

이 단계에서는 Sensor가 트레이트 계약을 충족하도록 만들고, 센서를 공통 보고 함수에 전달합니다.

impl Trait for Type 블록은 기존 트레이트를 하나의 구체적인 타입에 연결합니다. 메서드 시그니처는 트레이트와 일치해야 하지만, 본문에서는 해당 타입의 고유한 필드를 사용할 수 있습니다.

소스 파일을 엽니다.

nano src/main.rs

// SENSOR_IMPLEMENTATION을 다음 구현 코드로 바꿉니다.

impl Status for Sensor {
    fn status(&self) -> String {
        format!("{}: {}°C", self.location, self.celsius)
    }
}

format!String을 만들어 반환하며, 그 자체로 출력하지는 않습니다. self가 빌린 Sensor이므로 이 구현에서 locationcelsius를 사용할 수 있습니다.

다음으로 main 내부의 // SENSOR_REPORT를 다음 코드로 바꿉니다.

    show_status("Sensor", &sensor);

&sensor 인수는 보고 함수의 빌린 &T 매개변수와 일치하므로 sensor의 소유권은 main에 그대로 유지됩니다. Nano에서 저장하고 종료한 다음 프로그램을 실행합니다.

cargo run --quiet
Sensor: Greenhouse: 24°C

이 줄이 출력되면 SensorStatus를 충족하고 바운드가 지정된 제네릭 보고 함수를 통해 전달되었다는 뜻입니다. 아직 배송 정보는 연결하지 않았습니다.

ShipmentStatus 구현하기

이 단계에서는 동일한 트레이트를 Shipment에 구현하고, 하나의 보고 함수가 두 구체 타입을 모두 받을 수 있음을 확인합니다.

소스 파일을 다시 엽니다.

nano src/main.rs

// SHIPMENT_IMPLEMENTATION을 다음 코드로 바꿉니다.

impl Status for Shipment {
    fn status(&self) -> String {
        format!("#{} {}", self.id, self.stage)
    }
}

이 메서드는 동일한 트레이트 계약을 따르지만 배송 정보의 고유한 idstage 필드를 사용합니다. 다음으로 // SHIPMENT_REPORT를 다음 코드로 바꿉니다.

    show_status("Shipment", &shipment);

Nano에서 저장하고 종료합니다. 완성된 패키지를 실행합니다.

cargo run --quiet
Sensor: Greenhouse: 24°C
Shipment: #204 in transit

두 줄 모두 show_status<T: Status>에서 출력됩니다. 바운드는 함수에 필요한 동작을 설명하고, 각 구현은 해당 값이 그 동작을 생성하는 방식을 결정합니다.

요약

트레이트로 공통 동작을 정의하고, 두 구조체에 독립적으로 구현했습니다. 또한 기본 트레이트 바운드를 사용해 하나의 제네릭 보고 함수가 두 타입을 모두 받을 수 있도록 했습니다.