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가 이렇게 하는 이유
섹션 제목: “Rust가 이렇게 하는 이유”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가 있나요: trueLow 작업 수: 1derive한 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.5f64::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 this9 | 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}");}실행 결과:
스레드에서 본 값: debugmain에 아직 있는 값: debugRefCell도 마찬가지예요. 스레드 사이에서는 Rc<RefCell<T>> 대신 Arc<Mutex<T>>를 쓰세요. 공유 상태와 동시성을 참고하세요.
자주 보게 되는 곳
섹션 제목: “자주 보게 되는 곳”- 제네릭 함수.
fn announce<T: Display>(item: T)를 내 구조체로 호출하면`Song` doesn't implement `std::fmt::Display`와 함께 “required by a bound inannounce”(announce의 바운드 때문에 필요함)가 나와요. 해결: 바운드가 요구하는 트레이트를 구현하세요. - 테스트.
assert_eq!(a, b)에는PartialEq와Debug가 둘 다 필요해요(테스트가 실패하면 값을 출력해야 하니까요). 해결:#[derive(Debug, PartialEq)]. - 엉뚱한 타입으로
.collect()하기.let nums: Vec<i32> = words.iter().collect();는 “a value of typeVec<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
더 알아보기
섹션 제목: “더 알아보기”- E0277 — Rust error codes index (영어): 이 에러의 공식 설명이에요.
- 트레이트로 공통된 동작을 정의하기 — The Rust Programming Language 한국어판: 트레이트가 무엇이고 트레이트 바운드가 어떻게 동작하는지 설명해요.
- C - 파생 가능한 트레이트 — The Rust Programming Language 한국어판:
Debug,PartialEq,Eq,Hash,PartialOrd,Ord를 derive하면 각각 무엇을 얻는지 알려 줘요. - Result로 복구 가능한 에러 처리하기 — The Rust Programming Language 한국어판:
From변환을 포함해?가 어떻게 동작하는지 설명해요. Send— Rust 표준 라이브러리 (영어): 어떤 타입이 스레드 사이를 오갈 수 있는지, 왜Rc는 안 되는지 알 수 있어요.