- Published on
Rust E0382 moved value 5분 해결 - Clone 없이
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Rust를 쓰다 보면 error[E0382]: use of moved value는 거의 필연적으로 만납니다. 많은 예제가 clone()으로 급한 불을 끄지만, 이건 종종 성능 비용(할당·복사)과 설계 부채를 남깁니다. 이 글은 Clone 없이 E0382를 5분 안에 해결하는 데 초점을 맞춰, “왜 move가 발생했는지”를 빠르게 판독하고, 그 자리에서 적용 가능한 리팩토링 패턴을 제공합니다.
관련해서 소유권/대여 충돌을 더 깊게 다룬 글도 함께 보면 좋습니다: Rust 소유권 때문에 E0502 뜰 때 리팩토링 7가지, Rust E0502 소유권 충돌, NLL로 해결하기
E0382는 “값이 사라졌다”가 아니라 “소유권이 이동했다”
E0382의 핵심은 단순합니다.
- 어떤 값
x의 소유권이 move 되었고 - 그 이후에
x를 다시 쓰려고 해서 - 컴파일러가 “이미 이동된 값인데 또 쓰네?”라고 막습니다.
move는 보통 다음 상황에서 발생합니다.
String,Vec,HashMap같은 소유권을 가진 타입을 다른 변수에 대입- 함수 인자로 값을 그대로 전달(파라미터가
T) match,if let, 패턴 바인딩에서 값을 꺼내는 패턴을 사용- 반복문에서 컬렉션을
into_iter()로 소비
5분 디버깅 루틴
- 에러 메시지에서
moved here라인을 찾습니다. - “여기서 move가 발생”한 이유가 다음 중 무엇인지 분류합니다.
- 함수 인자 타입이
T인가,&T인가 iter()/iter_mut()/into_iter()중 무엇을 썼는가- 패턴이
ref없이 값을 꺼내고 있진 않은가
- 함수 인자 타입이
- 해결책은 대부분 소유권을 유지하거나 필요한 시점에만 소유권을 넘기기입니다.
아래부터는 실전에서 가장 자주 만나는 케이스별로 Clone 없이 해결하는 방법을 보여줍니다.
패턴 1: 함수 인자를 T에서 &T로 바꾸기
가장 흔한 move 트리거는 “그냥 출력/검증하려고 넘겼는데 소유권이 넘어가 버림”입니다.
문제 코드
fn log_user(name: String) {
println!("{name}");
}
fn main() {
let name = String::from("alice");
log_user(name);
// error[E0382]: use of moved value: `name`
println!("again: {name}");
}
Clone 없이 해결
소유권이 필요 없으면 빌려오면 됩니다.
fn log_user(name: &str) {
println!("{name}");
}
fn main() {
let name = String::from("alice");
log_user(&name);
println!("again: {name}");
}
포인트
String을 받지 말고&str을 받으면 호출부가String이든 문자열 리터럴이든 유연해집니다.- “읽기 전용” 함수는 기본적으로
&T또는&str로 설계하는 습관이 E0382를 크게 줄입니다.
패턴 2: match에서 값을 “꺼내지 말고 빌려오기” (ref/참조 패턴)
Option<String>이나 Result<String, E>를 match로 분기하다가 내부 값을 move 해버리는 경우가 많습니다.
문제 코드
fn main() {
let opt = Some(String::from("hello"));
match opt {
Some(s) => println!("{s}"),
None => println!("none"),
}
// error[E0382]: use of moved value: `opt`
println!("opt is: {:?}", opt);
}
Clone 없이 해결 1: as_ref()로 참조 Option 만들기
fn main() {
let opt = Some(String::from("hello"));
match opt.as_ref() {
Some(s) => println!("{s}"),
None => println!("none"),
}
println!("opt is: {:?}", opt);
}
as_ref()는Option<T>를Option<&T>로 바꿉니다.- 내부
String은 이동하지 않습니다.
Clone 없이 해결 2: 참조 패턴 사용
fn main() {
let opt = Some(String::from("hello"));
match &opt {
Some(s) => println!("{s}"),
None => println!("none"),
}
println!("opt is: {:?}", opt);
}
포인트
match opt는 소유권을 소비할 수 있습니다.match &opt또는match opt.as_ref()는 “빌려서 보기”입니다.
패턴 3: 반복문에서 into_iter() 대신 iter()/iter_mut() 사용
컬렉션을 순회하려고 했을 뿐인데, 실수로 컬렉션 자체를 move 해버리는 케이스입니다.
문제 코드
fn main() {
let v = vec![String::from("a"), String::from("b")];
for s in v.into_iter() {
println!("{s}");
}
// error[E0382]: borrow of moved value: `v`
println!("len = {}", v.len());
}
Clone 없이 해결
fn main() {
let v = vec![String::from("a"), String::from("b")];
for s in v.iter() {
println!("{s}");
}
println!("len = {}", v.len());
}
iter()는&String을 줍니다.- 소유권은
v에 남아 있습니다.
수정해야 하는 경우: 요소를 수정해야 한다면
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
for s in v.iter_mut() {
s.push('!');
}
println!("{:?}", v);
}
포인트
into_iter()는 “소유권을 가져가며 순회”입니다.iter()/iter_mut()는 “빌려서 순회”입니다.
패턴 4: “나중에 써야 하는 값”은 미리 빌리고, 소유권 이동은 마지막에
E0382는 종종 “값을 넘긴 뒤에 로그/메트릭/추가 작업에서 다시 사용”하면서 터집니다.
문제 코드
fn send(payload: String) {
println!("sending {payload}");
}
fn main() {
let payload = String::from("data");
send(payload);
// error[E0382]
println!("sent bytes = {}", payload.len());
}
Clone 없이 해결: 필요한 정보는 move 전에 뽑기
fn send(payload: String) {
println!("sending {payload}");
}
fn main() {
let payload = String::from("data");
let bytes = payload.len();
send(payload);
println!("sent bytes = {bytes}");
}
포인트
- “이후에 필요한 건 전체 값이 아니라 일부 정보”인 경우가 많습니다.
len(),is_empty(),as_str()같은 값/참조를 move 이전에 확보하면 해결됩니다.
패턴 5: 구조체에서 필드만 빼오다가 self를 망가뜨릴 때 mem::take/Option::take
self.field를 꺼내는 순간 self의 일부가 move 되어 이후에 self를 쓰기 어려워집니다. 이때 Clone 대신 “필드를 비우고 가져오기”가 정답인 경우가 많습니다.
문제 상황(필드 move)
struct Job {
id: u64,
payload: String,
}
impl Job {
fn into_payload(self) -> String {
self.payload
}
}
위 코드는 self를 통째로 소비하므로 괜찮지만, 실제로는 &mut self 메서드에서 payload만 꺼내고 싶을 때가 많습니다.
Clone 없이 해결 1: std::mem::take
use std::mem;
struct Job {
id: u64,
payload: String,
}
impl Job {
fn take_payload(&mut self) -> String {
mem::take(&mut self.payload)
}
}
fn main() {
let mut job = Job { id: 1, payload: String::from("p") };
let p = job.take_payload();
// job.payload 는 이제 빈 문자열
println!("id={} payload='{}' taken='{}'", job.id, job.payload, p);
}
mem::take는 대상에Default::default()를 넣고 기존 값을 반환합니다.String의 기본값은 빈 문자열이라 추가 할당 없이 패턴이 깔끔합니다.
Clone 없이 해결 2: Option<T>라면 take()
struct Job {
payload: Option<String>,
}
impl Job {
fn take_payload(&mut self) -> Option<String> {
self.payload.take()
}
}
포인트
- “필드를 소유권째로 꺼내야 한다”면
take계열이 가장 흔한 정답입니다.
패턴 6: HashMap에서 값을 가져오려면 get/get_mut/remove를 의도에 맞게
맵에서 값을 읽기만 할 건데 map[key] 스타일로 접근하거나, entry 처리 중 move가 발생하는 경우가 있습니다.
읽기만 할 때: get
use std::collections::HashMap;
fn main() {
let mut m = HashMap::new();
m.insert("k".to_string(), "v".to_string());
if let Some(v) = m.get("k") {
println!("{v}");
}
// m은 그대로 유지
println!("size={}", m.len());
}
소유권을 가져와야 할 때: remove
use std::collections::HashMap;
fn main() {
let mut m = HashMap::new();
m.insert("k".to_string(), "v".to_string());
let v = m.remove("k");
println!("taken={:?} size={}", v, m.len());
}
포인트
- “읽기”는
get으로 참조를 받기 - “수정”은
get_mut - “꺼내기”는
remove또는entry기반으로 설계
언제 Clone이 정당화되나 (최소 기준)
Clone을 무조건 악으로 볼 필요는 없습니다. 다만 E0382를 만났을 때 첫 선택지가 Clone이 되면, Rust의 장점(명확한 소유권, 불필요한 복사 제거)을 스스로 포기하게 됩니다.
Clone이 정당화되기 쉬운 경우는 다음 정도입니다.
- 데이터 크기가 작고(예: 짧은
String), 코드 단순성이 압도적으로 중요한 경계 - 캐시 키처럼 “동일 값의 독립 소유권”이 명확히 필요한 설계
Arc/Rc같은 공유 소유권 모델을 쓰는 것이 더 자연스러운 경우(이때의clone()은 참조 카운트 증가)
하지만 이 글의 범위(일반적인 moved value)에서는 대부분 함수 시그니처/순회 방식/패턴 매칭/필드 take로 해결됩니다.
체크리스트: E0382를 보면 바로 확인할 것
- 함수 인자 타입이
T라서 move 되는가?&T또는&str로 바꿀 수 있는가? match opt대신match &opt또는opt.as_ref()로 해결 가능한가?- 순회에서
into_iter()를 실수로 쓰고 있지 않은가?iter()/iter_mut()가 맞지 않은가? - move 이후에 필요한 건 “값 전체”인가, “일부 정보”인가? move 전에 추출 가능하지 않은가?
- 구조체 필드를 꺼내야 한다면
std::mem::take또는Option::take가 더 적절하지 않은가?
마무리: E0382는 버그가 아니라 설계 신호
E0382는 “컴파일이 귀찮다”가 아니라, 데이터의 소유권 경계를 더 명확히 하라는 신호입니다. Clone 없이 해결하려면 결국 다음 중 하나로 귀결됩니다.
- 읽기만 하면 빌려라(
&T,&str,iter) - 소유권을 넘길 거면 마지막에 넘겨라(필요한 값은 미리 추출)
- 필드를 꺼내야 하면
take로 상태를 합법적으로 비워라
이 패턴들에 익숙해지면, E0382는 “막히는 에러”가 아니라 “코드를 더 싸고 명확하게 만드는 리팩토링 트리거”로 바뀝니다.