콘텐츠로 이동

Rust 에러 E0277: the trait bound is not satisfied (트레이트 미구현)

E0277은 어떤 능력이 필요한 자리에, 그 능력이 없는 타입을 썼다는 뜻이에요. 출력하기, 비교하기, 더하기, 변환하기, 다른 스레드로 보내기. 이런 일에는 각각 트레이트(trait)가 필요한데, 내 타입에 그 트레이트가 없는 거예요. Rust에서 아주 흔한 에러 중 하나예요. 에러 메시지가 보통 정확한 해결책을 알려 주고, 대부분은 한 줄이면 고쳐져요. (이 페이지는 Rust 컴파일러 에러 해설 시리즈의 일부예요.)

많은 사람이 처음 만나는 건 직접 만든 구조체를 {}로 출력하려고 할 때예요.

error[E0277]: `Player` doesn't implement `std::fmt::Display`
--> src/main.rs:8:20
|
8 | println!("{}", p);
| -- ^ `Player` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Player`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

“Player가 std::fmt::Display를 구현하지 않았다”, 그래서 “기본 포매터로 출력할 수 없다”는 뜻이에요. 마지막 note는 “포맷 문자열에서 대신 {:?}(보기 좋게 출력하려면 {:#?})를 쓸 수 있을지도 모른다”고 알려 줘요.

이 에러를 내는 프로그램이에요.

struct Player {
name: String,
score: u32,
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{}", p);
}

트레이트는 “이 타입은 이런 일을 할 수 있어요”라는 약속이에요. Display는 “나는 나 자신을 보기 좋게 출력할 줄 알아요”라는 약속이고, PartialEq는 “==로 비교할 수 있어요”, Send는 “다른 스레드로 옮겨도 안전해요”라는 약속이에요.

println!, .sort(), +, thread::spawn 같은 코드는 이런 약속 중 하나를 요구해요. 컴파일러는 내가 넘긴 값(8번째 줄의 p)을 가리키면서 “이 타입은 그런 약속을 한 적이 없으니까 여기서 쓸 수 없어요”라고 말하는 거예요.

콘센트를 떠올려 보세요. 콘센트에는 정해진 모양의 플러그만 들어가요. 기기가 아무리 멀쩡해도 플러그 모양이 안 맞으면 꽂을 수 없죠. E0277은 컴파일러가 “플러그가 안 맞아요”라고 말하는 거예요. 타입에 맞는 플러그를 달아 주면 해결돼요. 보통은 #[derive(...)]로, 가끔은 짧은 impl 블록으로요.

Rust는 절대 추측하지 않아요. 내가 만든 Player 타입을 출력하고, 비교하고, 정렬하는 “정답”은 하나로 정해져 있지 않아요. 두 플레이어는 이름만 같으면 같은 걸까요, 아니면 모든 필드가 같아야 같은 걸까요? Rust는 내가 원하지 않은 일을 조용히 하는 대신, 나에게 직접 정하라고 해요.

이 검사는 컴파일할 때 일어나요. 많은 언어에서는 출력하는 법을 알려 주지 않은 객체를 출력하면 <Player object at 0x7f...>처럼 쓸모없는 결과가 나오고, 스레드에 안전하지 않은 값을 스레드로 보내면 실제 서비스에서 가끔 프로그램이 죽어요. Rust에서는 둘 다 컴파일 에러라서 한 번 고치면 끝이에요.

트레이트가 어떻게 동작하는지는 트레이트 페이지에서, 매일 쓰는 트레이트(Debug, Display, PartialEq, Ord, Hash)는 자주 쓰는 트레이트에서 더 읽어 볼 수 있어요.

E0277은 얼굴이 여러 개예요. 아래에서 내 에러 메시지와 같은 것을 찾아보세요.

방법 1: 구조체를 {}나 {:?}로 출력하기

섹션 제목: “방법 1: 구조체를 {}나 {:?}로 출력하기”

{:?}에는 Debug가 필요해요. 직접 구현할 일은 거의 없고, #[derive(Debug)]를 붙이면 돼요. 디버깅과 로그에 쓰세요. {:?} 버전의 에러는 이렇게 생겼어요.

error[E0277]: `Player` doesn't implement `Debug`
= help: the trait `Debug` is not implemented for `Player`
= note: add `#[derive(Debug)]` to `Player` or manually `impl Debug for Player`

note가 “Player에 #[derive(Debug)]를 붙이거나 impl Debug for Player를 직접 쓰라”고 알려 주고 있어요.

{}에는 Display가 필요해요. 이건 derive할 수 없어요. “보기 좋은” 모양이 어때야 하는지는 나만 알기 때문이에요. write!를 쓰는 작은 impl을 작성하세요. 사용자에게 보여 줄 텍스트에 쓰면 돼요.

use std::fmt;
#[derive(Debug)] // {:?}는 공짜로 얻어요
struct Player {
name: String,
score: u32,
}
// Display는 직접 써요. {}가 무엇을 출력할지 여기서 정해요.
impl fmt::Display for Player {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} ({}점)", self.name, self.score)
}
}
fn main() {
let p = Player { name: String::from("Ana"), score: 42 };
println!("{:?}", p);
println!("{}", p);
}

실행 결과:

Player { name: "Ana", score: 42 }
Ana (42점)
▶ 직접 실행해 보기

방법 2: 비교, 정렬, HashMap 키로 쓰기

섹션 제목: “방법 2: 비교, 정렬, HashMap 키로 쓰기”

.contains()와 ==에는 PartialEq가 필요해요. .sort()에는 Ord가 필요하고요. HashSet과 HashMap의 키에는 Eq와 Hash가 필요해요. 에러 메시지는 이렇게 생겼어요.

error[E0277]: can't compare `Point` with `Point`
= help: the trait `PartialEq` is not implemented for `Point`
error[E0277]: the trait bound `Priority: Ord` is not satisfied
= help: the trait `Ord` is not implemented for `Priority`
error[E0277]: the trait bound `Point: Hash` is not satisfied
= help: the trait `Hash` is not implemented for `Point`

차례대로 “Point끼리 비교할 수 없다”, “Priority에 Ord가 없다”, “Point에 Hash가 없다”는 뜻이에요.

해결책은 derive하는 거예요. 이 트레이트들은 서로를 바탕으로 쌓여 있어서, 보통은 PartialEq, Eq, Hash, PartialOrd, Ord를 한꺼번에 붙여요.

use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum Priority {
Low,
Medium,
High,
}
fn main() {
let mut tasks = vec![Priority::High, Priority::Low, Priority::Medium];
tasks.sort(); // Ord가 필요해요
println!("{:?}", tasks);
println!("High가 있나요: {}", tasks.contains(&Priority::High)); // PartialEq가 필요해요
let mut counts: HashMap<Priority, u32> = HashMap::new(); // 키에는 Eq + Hash가 필요해요
for task in &tasks {
*counts.entry(*task).or_insert(0) += 1;
}
println!("Low 작업 수: {}", counts[&Priority::Low]);
}

실행 결과:

[Low, Medium, High]
High가 있나요: true
Low 작업 수: 1
▶ 직접 실행해 보기

derive한 Ord는 열거형의 배리언트를 내가 쓴 순서대로 정렬하고, 구조체는 위에서 아래로 필드 하나씩 비교해요. 솔직히 말하면 함정이 하나 있어요. f64 필드가 있는 타입은 Eq, Hash, Ord를 derive할 수 없어요. 부동소수점에는 자기 자신과도 같지 않은 NaN이 있기 때문이에요. 부동소수점을 정렬하는 방법은 정렬을 보세요.

방법 3: 숫자 타입 섞어 쓰기 (cannot add f64 to i32)

섹션 제목: “방법 3: 숫자 타입 섞어 쓰기 (cannot add f64 to i32)”
error[E0277]: cannot add `f64` to `i32`
--> src/main.rs:4:23
|
4 | let total = items + price;
| ^ no implementation for `i32 + f64`
|
= help: the trait `Add<f64>` is not implemented for `i32`

“i32에 f64를 더할 수 없다”, “i32 + f64에 대한 구현이 없다”는 뜻이에요.

Rust는 숫자를 대신 변환해 주지 않아요. i32에서 f64로도요. +, -, *, /의 양쪽은 같은 타입이어야 해요. as로 한쪽을 직접 변환하세요.

fn main() {
let items: i32 = 3;
let price: f64 = 2.5;
let total = items as f64 * price; // 먼저 변환하고, 그다음 곱해요
println!("합계: {total}");
}

실행 결과:

합계: 7.5
▶ 직접 실행해 보기

f64::from(items)도 되고, 이쪽은 데이터를 잃는 일이 절대 없어요. 반대 방향(f64에서 i32)은 소수점 아래를 잘라 버리니까, 반올림을 먼저 생각해 보세요. 자세한 내용은 타입 변환과 변환 페이지에 있어요.

방법 4: ?가 에러를 변환하지 못할 때

섹션 제목: “방법 4: ?가 에러를 변환하지 못할 때”
error[E0277]: `?` couldn't convert the error to `ConfigError`
|
8 | fn parse_port(text: &str) -> Result<u16, ConfigError> {
| ------------------------ expected `ConfigError` because of this
9 | let port = text.parse::<u16>()?;
| --------------^ the trait `From<ParseIntError>` is not implemented for `ConfigError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

“?가 에러를 ConfigError로 변환하지 못했다”는 뜻이에요. note는 “? 연산은 From 트레이트를 써서 에러 값을 암묵적으로 변환한다”고 설명해요.

?는 에러를 만나면 그 에러를 함수 밖으로 반환해요. 에러 타입이 함수가 약속한 타입과 다르면, ?는 From으로 변환을 시도해요. 내가 할 일은 그 From을 작성하는 거예요.

use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
BadPort(ParseIntError),
}
// ParseIntError를 ConfigError로 바꾸는 법을 `?`에게 알려 줘요.
impl From<ParseIntError> for ConfigError {
fn from(err: ParseIntError) -> Self {
ConfigError::BadPort(err)
}
}
fn parse_port(text: &str) -> Result<u16, ConfigError> {
let port = text.parse::<u16>()?;
Ok(port)
}
fn main() {
println!("{:?}", parse_port("8080"));
println!("{:?}", parse_port("eighty"));
}

실행 결과:

Ok(8080)
Err(BadPort(ParseIntError { kind: InvalidDigit }))
▶ 직접 실행해 보기

다른 좋은 방법도 있어요. 호출 하나만 .map_err(ConfigError::BadPort)?로 변환하거나, thiserror 크레이트의 #[from]을 쓰거나, 작은 프로그램이라면 Result<T, Box<dyn std::error::Error>>를 반환해도 돼요. 사용자 정의 에러와 anyhow와 thiserror를 참고하세요.

방법 5: Rc는 스레드 사이로 안전하게 보낼 수 없어요

섹션 제목: “방법 5: Rc는 스레드 사이로 안전하게 보낼 수 없어요”
error[E0277]: `Rc<String>` cannot be sent between threads safely
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(move || {
| ------------- required by a bound introduced by this call
...
= help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<String>`

“Rc<String>은 스레드 사이로 안전하게 보낼 수 없다”, 즉 Rc<String>에는 Send가 구현되어 있지 않다는 뜻이에요.

Rc는 주인의 수를 평범한 숫자로 세요. 두 스레드가 이 숫자를 동시에 바꾸면 값이 망가질 수 있어요. 그래서 Rc는 Send가 아니에요. 대신 Arc(“atomic”, 원자적 버전)를 쓰세요. 쓰는 법은 똑같고, 스레드 사이에서도 안전해요.

use std::sync::Arc;
use std::thread;
fn main() {
let config = Arc::new(String::from("debug"));
let for_thread = Arc::clone(&config); // 스레드를 위한 두 번째 주인
let handle = thread::spawn(move || {
println!("스레드에서 본 값: {for_thread}");
});
handle.join().unwrap();
println!("main에 아직 있는 값: {config}");
}

실행 결과:

스레드에서 본 값: debug
main에 아직 있는 값: debug
▶ 직접 실행해 보기

RefCell도 마찬가지예요. 스레드 사이에서는 Rc<RefCell<T>> 대신 Arc<Mutex<T>>를 쓰세요. 공유 상태와 동시성을 참고하세요.

  • 제네릭 함수. fn announce<T: Display>(item: T)를 내 구조체로 호출하면 `Song` doesn't implement `std::fmt::Display`와 함께 “required by a bound in announce”(announce의 바운드 때문에 필요함)가 나와요. 해결: 바운드가 요구하는 트레이트를 구현하세요.
  • 테스트. assert_eq!(a, b)에는 PartialEq와 Debug가 둘 다 필요해요(테스트가 실패하면 값을 출력해야 하니까요). 해결: #[derive(Debug, PartialEq)].
  • 엉뚱한 타입으로 .collect()하기. let nums: Vec<i32> = words.iter().collect();는 “a value of type Vec<i32> cannot be built from an iterator over elements of type &String”(&String 요소의 반복자로는 Vec<i32>를 만들 수 없다)를 내요. 해결: 각 아이템을 먼저 변환하세요. 예를 들어 .map(|w| w.parse().unwrap())처럼요. 반복자를 참고하세요.
  • E0599: no method found (메서드 없음): 키가 Hash가 아닌 HashMap에서 .insert()를 호출하면 E0599(“trait bounds were not satisfied”, 트레이트 바운드 불충족)로 나올 때도 있어요. 해결책은 같아요. 트레이트를 derive하세요.
  • E0308: mismatched types (타입 불일치): f64가 와야 할 자리에 i32를 넣은 것처럼, 다른 타입이 기대되는 곳에 엉뚱한 타입을 넣었어요.
  • E0382: borrow of moved value (이동된 값 사용): 트레이트를 생각하며 Clone이나 Copy를 붙인 직후에 자주 나와요.
  • E0369: binary operation == cannot be applied (이항 연산 적용 불가): PartialEq가 없을 때 == 연산자에서 나오는 버전이에요. 해결책도 같아요.
  • 전체 컴파일러 에러 해설

이해도 점검

1. {:?}를 쓰다가 "Order doesn't implement Debug" 에러가 났어요. 가장 빠른 해결책은 무엇일까요?

2. let total = 3_i32 + 2.5_f64;는 왜 실패할까요?

3. 스레드 코드가 "Rc<String> cannot be sent between threads safely"로 실패해요. 무엇을 써야 할까요?

점수: 0 / 3