Published on

Rust E0382 moved value 5분 해결 - Clone 없이

Authors

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분 디버깅 루틴

  1. 에러 메시지에서 moved here 라인을 찾습니다.
  2. “여기서 move가 발생”한 이유가 다음 중 무엇인지 분류합니다.
    • 함수 인자 타입이 T인가, &T인가
    • iter()/iter_mut()/into_iter() 중 무엇을 썼는가
    • 패턴이 ref 없이 값을 꺼내고 있진 않은가
  3. 해결책은 대부분 소유권을 유지하거나 필요한 시점에만 소유권을 넘기기입니다.

아래부터는 실전에서 가장 자주 만나는 케이스별로 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는 “막히는 에러”가 아니라 “코드를 더 싸고 명확하게 만드는 리팩토링 트리거”로 바뀝니다.