콘텐츠로 이동

Rust 에러 E0255: the name is defined multiple times (use와 내 항목의 이름 충돌)

E0255는 use 줄로 가져온 이름과 내가 직접 정의한 것의 이름이 같다는 뜻이에요. 대표적인 경우는 이래요. 파일 맨 위에서 std::io::Error를 가져오고, 아래에서 나만의 enum Error를 만드는 거예요. Rust는 이름 하나가 두 가지를 뜻하게 둘 수 없어서 컴파일을 멈춰요. 나만의 에러 타입을 처음 만들 때 아주 흔한 에러이고, 고치는 데는 짧은 한 줄이면 돼요. E0412와 달리 이 코드는 최근 버전에서도 바뀌지 않았어요. rustc 1.97도 여전히 E0255로 알려 줘요. 이 페이지는 Rust 컴파일러 에러 해설의 일부예요.

error[E0255]: the name `Error` is defined multiple times
--> src/main.rs:3:1
|
1 | use std::io::Error;
| -------------- previous import of the type `Error` here
2 |
3 | enum Error { // ❌ 1번째 줄에서 가져온 것과 같은 이름
| ^^^^^^^^^^ `Error` redefined here
|
= note: `Error` must be defined only once in the type namespace of this module
help: you can use `as` to change the binding name of the import
|
1 | use std::io::Error as OtherError;
| +++++++++++++
warning: unused import: `std::io::Error`
--> src/main.rs:1:5
|
1 | use std::io::Error;
| ^^^^^^^^^^^^^^

“the name Error is defined multiple times”는 “Error라는 이름이 여러 번 정의됐다”는 뜻이에요. 아래쪽 warning: unused import는 “가져온 std::io::Error를 쓰지 않는다”는 경고예요. 각 부분은 아래에서 풀어 볼게요.

이 에러를 내는 프로그램은 다음과 같아요.

use std::io::Error;
enum Error { // ❌ 1번째 줄에서 가져온 것과 같은 이름
EmptyName,
TooLong(usize),
}
fn check_name(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::EmptyName);
}
if name.len() > 10 {
return Err(Error::TooLong(name.len()));
}
Ok(())
}
fn main() {
match check_name("") {
Ok(()) => println!("이름이 괜찮아요"),
Err(Error::EmptyName) => println!("이름이 비어 있어요"),
Err(Error::TooLong(n)) => println!("이름이 {}글자예요. 최대 10글자까지 돼요", n),
}
}

rustc는 세 가지를 가리켜요.

  1. --- 줄(1번째 줄): “previous import of the type Error here”(타입 Error를 이미 여기서 가져왔다). use 줄이 이 모듈에 Error라는 이름을 이미 넣어 뒀어요.
  2. ^^^ 줄(3번째 줄): “Error redefined here”(Error가 여기서 다시 정의됐다). 내가 만든 enum Error가 같은 이름을 차지하려고 해요.
  3. help: 줄은 빠져나갈 길을 알려 줘요. “you can use as to change the binding name of the import”, 즉 as로 가져온 것에 다른 이름을 붙이라는 거예요.

그 아래의 warning: unused import는 쓸모 있는 힌트예요. 코드 어디에서도 std::io::Error를 실제로 쓰지 않는다는 뜻이에요(이 프로그램의 Error는 모두 내 열거형을 가리켜요). 이럴 때는 1번째 줄을 그냥 지우면 돼요. 둘 다 필요하다면 계속 읽어 주세요.

note에는 타입 네임스페이스(type namespace)라는 말이 나와요. 타입, 트레이트, 모듈은 이름 목록 하나를 같이 쓰고, 함수, 상수, static은 다른 목록을 써요. 그래서 use std::mem::swap;과 내가 만든 fn swap도 같은 에러가 나요. 다만 이때는 “값 네임스페이스”(value namespace)라고 나와요.

한 반에 이름이 “민준”인 학생이 두 명 있다고 생각해 보세요. 선생님은 이제 “민준아”라고만 부를 수 없어요. 누군가에게 “큰 민준”처럼 별명이 필요해요. as가 가져온 이름에 붙여 주는 게 바로 그 별명이에요.

모듈 안의 이름 하나는 정확히 하나의 항목을 가리켜요. Rust가 10번째 줄의 Error::EmptyName을 읽을 때는 어느 Error인지 알아야 하는데, Rust는 짐작하지 않아요. 몰래 내 열거형을 고르면 use 줄이 아무 일도 하지 않게 되고, io::Error를 고르면 내 열거형을 쓸 수 없게 돼요. 어느 쪽이든 조용히 고르면 실수가 숨어 버려요.

그래서 E0255는 E0428(내가 만든 항목 두 개가 같은 이름)과 E0252(use 줄 두 개가 같은 이름)의 형제 에러예요. 규칙은 같아요. 한 모듈 안에서는 이름 하나에 하나만. use가 어떻게 동작하는지는 모듈을 참고하세요.

방법 1: as로 가져온 이름 바꾸기

섹션 제목: “방법 1: as로 가져온 이름 바꾸기”

한 파일에서 두 타입이 모두 필요할 때 이 방법을 고르세요. 꽤 흔한 상황이에요. 앱의 규칙에는 내 Error를, 파일과 네트워크에는 io::Error를 쓰는 식이죠. 가져온 것에 어디서 왔는지 알려 주는 별명을 붙이세요. rustc의 자리 표시자 OtherError보다 IoError가 더 잘 읽혀요.

use std::io::Error as IoError;
use std::io::ErrorKind;
#[derive(Debug)]
enum Error {
EmptyName,
TooLong(usize),
}
fn check_name(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::EmptyName);
}
if name.len() > 10 {
return Err(Error::TooLong(name.len()));
}
Ok(())
}
fn save_name(name: &str) -> Result<(), IoError> {
if name == "root" {
return Err(IoError::new(ErrorKind::PermissionDenied, "예약된 이름이에요"));
}
Ok(())
}
fn main() {
println!("{:?}", check_name("Ferris"));
println!("{:?}", check_name(""));
println!("{:?}", check_name("Ferris the Crab"));
match save_name("root") {
Ok(()) => println!("저장했어요"),
Err(e) => println!("io 에러: {}", e),
}
}

실행 결과:

Ok(())
Err(EmptyName)
Err(TooLong(15))
io 에러: 예약된 이름이에요
▶ 직접 실행해 보기

check_name에 넘기는 이름은 일부러 영어로 뒀어요. len()은 바이트 수를 세는데, 한글은 한 글자가 3바이트라서 결과가 달라지거든요. as는 이 파일 안에서만 이름을 바꿔요. 다른 곳에서는 여전히 std::io::Error예요.

방법 2: 타입 대신 모듈 가져오기

섹션 제목: “방법 2: 타입 대신 모듈 가져오기”

Error를 쓸 때마다 어디서 온 것인지 보이게 하고 싶을 때 이 방법을 고르세요. 부모 모듈을 use std::io;로 가져오고 io::Error라고 쓰세요. 그러면 내 Error는 짧은 이름을 그대로 쓸 수 있어요. Error나 Result처럼 흔한 이름의 타입에는 많은 Rust 프로그래머가 이 스타일을 좋아해요.

use std::io;
enum Error {
EmptyName,
}
fn check_name(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::EmptyName);
}
Ok(())
}
fn save_name(name: &str) -> Result<(), io::Error> {
if name == "root" {
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "예약된 이름"));
}
Ok(())
}
fn main() {
if let Err(Error::EmptyName) = check_name("") {
println!("이름이 비어 있어요");
}
if let Err(e) = save_name("root") {
println!("io 에러: {}", e);
}
}

실행 결과:

이름이 비어 있어요
io 에러: 예약된 이름
▶ 직접 실행해 보기

방법 3: 내가 만든 항목의 이름 바꾸기

섹션 제목: “방법 3: 내가 만든 항목의 이름 바꾸기”

내 타입을 여러 파일에서 쓰거나 다른 사람도 쓸 때 이 방법을 고르세요. 두 번째 Error보다는 NameError나 AppError처럼 구체적인 이름이 더 분명하고, use 줄은 그대로 둘 수 있어요.

use std::io::Error;
#[derive(Debug)]
enum NameError {
EmptyName,
TooLong(usize),
}
fn check_name(name: &str) -> Result<(), NameError> {
if name.is_empty() {
return Err(NameError::EmptyName);
}
if name.len() > 10 {
return Err(NameError::TooLong(name.len()));
}
Ok(())
}
fn main() {
println!("{:?}", check_name("Ferris the Crab"));
let disk = Error::other("디스크가 가득 찼어요");
println!("{}", disk);
}

실행 결과:

Err(TooLong(15))
디스크가 가득 찼어요
▶ 직접 실행해 보기

프로젝트가 커지면 thiserror 크레이트로 이런 에러 열거형을 훨씬 짧게 쓸 수 있어요. 사용자 정의 에러를 참고하세요.

위 에러처럼 rustc가 warning: unused import도 함께 출력할 때 이 방법을 고르세요. 예전 코드에서 남았거나, 에디터가 자동으로 넣은 use예요. 그 줄을 지우면 이 페이지 맨 위의 프로그램이 그대로 동작해요.

enum Error {
EmptyName,
TooLong(usize),
}
fn check_name(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::EmptyName);
}
if name.len() > 10 {
return Err(Error::TooLong(name.len()));
}
Ok(())
}
fn main() {
match check_name("") {
Ok(()) => println!("이름이 괜찮아요"),
Err(Error::EmptyName) => println!("이름이 비어 있어요"),
Err(Error::TooLong(n)) => println!("이름이 {}글자예요. 최대 10글자까지 돼요", n),
}
}

실행 결과:

이름이 비어 있어요
▶ 직접 실행해 보기

표준 라이브러리와 이름이 같은 내 함수. use std::mem::swap;과 fn swap(a: &mut i32, b: &mut i32)를 함께 쓰면 E0255가 나요. 라벨은 “previous import of the value swap here”(값 swap을 이미 여기서 가져왔다)이고, note에는 “value namespace”(값 네임스페이스)라고 나와요. 해결: 내 함수를 swap_values라고 부르거나, 직접 만들 생각이었다면 use를 지우세요.

모듈 사이에서 복사해 붙여넣기. struct User를 mod models로 옮기고 맨 위에 use models::User;를 추가했는데, main.rs에 있던 예전 struct User를 지우는 걸 깜빡한 경우예요. 해결: 예전 복사본을 지워서 User가 models에 있는 것만 가리키게 하세요.

표준 라이브러리와 이름이 같은 트레이트. use std::fmt::Display;와 내가 만든 trait Display도 똑같이 충돌해요(“previous import of the trait Display”, 트레이트 Display를 이미 가져왔다). 해결: 내 트레이트의 이름을 바꾸거나(Describe, Render), use std::fmt;라고 쓰고 fmt::Display로 쓰세요.

알아 두면 좋은 점: use std::io::*; 같은 글롭(glob) 가져오기는 E0255를 절대 내지 않아요. *로 들어온 이름보다 내가 만든 항목이 조용히 이겨요. 편하긴 하지만 어느 Error를 쓰는지 가려질 수도 있어서, 보통은 이름을 하나씩 가져오는 편이 더 분명해요.

  • E0252: fmt::Result와 io::Result처럼 use 줄 두 개가 같은 이름을 가져와요.
  • E0428: 내가 만든 항목 두 개가 같은 이름을 써요. 함수를 오버로딩하려고 할 때 자주 나요.
  • E0592: impl 블록 안에 이름이 같은 메서드가 두 개 있어요.
  • E0432: use 줄 자체가 존재하지 않는 것을 가리켜요.
  • E0425: 반대 문제예요. 쓰는 곳에서 그 이름이 아예 정의되지 않았어요.
  • 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.

이해도 점검

1. 파일이 use std::fmt::Result;로 시작하고, 아래에서 enum Result { Pass, Fail }을 정의해요. 어떤 에러가 날까요?

2. 한 파일에서 std::io::Error와 내가 만든 enum Error가 모두 필요해요. 깔끔한 해결 방법은 무엇일까요?

3. rustc가 E0255와 함께 warning: unused import: std::io::Error도 보여 줘요. 이 경고는 무엇을 알려 줄까요?

점수: 0 / 3