Published on

Rust 소유권 때문에 막히는 E0502/E0507 해결

Authors

Rust를 배우다 보면 “소유권은 이해했는데 왜 컴파일러가 계속 화를 내지?”라는 순간이 옵니다. 그 중심에 자주 등장하는 에러가 E0502E0507입니다. 둘 다 결국 “값의 수명과 별칭(aliasing)을 Rust가 안전하게 증명할 수 없어서” 발생하지만, 해결 방식은 꽤 실전적인 리팩터링 테크닉으로 정리됩니다.

이 글은 두 에러를 재현 코드로 확인하고, 컴파일이 통과하는 해결 패턴을 여러 갈래로 제시합니다. E0502는 빌림의 범위와 접근 순서를 조정하는 문제에 가깝고, E0507은 “이동(move) 대신 빌림/복제/대체” 중 무엇을 선택할지의 문제에 가깝습니다.

관련해서 E0502만 집중적으로 더 많은 패턴을 보고 싶다면 이 글도 함께 보세요: Rust 소유권·빌림 - E0502 해결 패턴 7가지

E0502: 불변으로 빌린 뒤 가변으로 빌릴 수 없음

에러 메시지의 전형은 다음과 같습니다.

  • cannot borrow ... as mutable because it is also borrowed as immutable

핵심은 같은 값에 대해 불변 빌림(&T)이 살아있는 동안 가변 빌림(&mut T)을 만들 수 없다는 규칙입니다. Rust는 “불변 참조가 존재하는 동안 값이 바뀌지 않는다”는 전제를 최적화와 안전성의 기반으로 쓰기 때문에, 이 규칙이 매우 강합니다.

1) 가장 흔한 케이스: 참조를 잡아둔 채로 수정

fn main() {
    let mut v = vec![1, 2, 3];

    let first = &v[0];
    v.push(4);

    println!("{first}");
}

위 코드는 보통 E0502가 납니다. 이유는 firstv를 불변으로 빌린 상태인데, push는 내부 버퍼 재할당 가능성이 있어 v를 가변으로 빌려야 하기 때문입니다.

해결 A: 값을 복사해서 들고 있기

원소 타입이 Copy라면 가장 간단합니다.

fn main() {
    let mut v = vec![1, 2, 3];

    let first = v[0]; // i32는 Copy
    v.push(4);

    println!("{first}");
}

해결 B: 불변 빌림의 스코프를 줄이기

참조를 “필요할 때만” 만들면 됩니다.

fn main() {
    let mut v = vec![1, 2, 3];

    v.push(4);
    println!("{}", v[0]);
}

또는 블록으로 스코프를 강제로 닫습니다.

fn main() {
    let mut v = vec![1, 2, 3];

    {
        let first = &v[0];
        println!("{first}");
    } // 여기서 first의 빌림 종료

    v.push(4);
}

해결 C: 인덱스 대신 split_at_mut로 “서로 다른 구간”임을 증명

같은 슬라이스에서 서로 다른 위치를 동시에 가변으로 만지고 싶을 때, 단순히 인덱싱으로는 Rust가 “서로 다른 원소”임을 증명하지 못해 막히는 경우가 많습니다.

fn swap_first_two(v: &mut [i32]) {
    let (a, b) = v.split_at_mut(1);
    let x = &mut a[0];
    let y = &mut b[0];
    std::mem::swap(x, y);
}

fn main() {
    let mut v = vec![10, 20, 30];
    swap_first_two(&mut v);
    assert_eq!(v, vec![20, 10, 30]);
}

split_at_mut는 “둘이 겹치지 않는 두 슬라이스”라는 것을 표준 라이브러리가 보장해주기 때문에 빌림 규칙을 만족합니다.

2) 반복 중 수정: for x in &v 하면서 v.push

fn main() {
    let mut v = vec![1, 2, 3];

    for x in &v {
        if *x == 2 {
            v.push(4);
        }
    }
}

이 코드는 대개 E0502입니다. for x in &v는 반복 내내 v를 불변으로 빌립니다.

해결 A: 인덱스로 순회하되, 길이를 미리 고정

fn main() {
    let mut v = vec![1, 2, 3];

    let len = v.len();
    for i in 0..len {
        if v[i] == 2 {
            v.push(4);
        }
    }

    assert_eq!(v, vec![1, 2, 3, 4]);
}

주의: 이 방식은 “초기 길이까지만” 순회합니다. push로 추가된 원소는 이번 루프에서 처리되지 않습니다.

해결 B: 변경 사항을 별도로 모아서 마지막에 반영

fn main() {
    let mut v = vec![1, 2, 3];

    let mut to_add = Vec::new();
    for x in &v {
        if *x == 2 {
            to_add.push(4);
        }
    }

    v.extend(to_add);
    assert_eq!(v, vec![1, 2, 3, 4]);
}

이 패턴은 데이터 파이프라인에서 특히 자주 씁니다. “읽기 단계”와 “쓰기 단계”를 분리하면 빌림 충돌이 대부분 사라집니다.

3) HashMap에서 getinsert를 섞을 때

use std::collections::HashMap;

fn main() {
    let mut m = HashMap::new();
    m.insert("a".to_string(), 1);

    let v = m.get("a");
    m.insert("b".to_string(), 2);

    println!("{:?}", v);
}

get이 반환한 참조가 살아있는 동안 insert는 맵을 가변으로 빌리므로 E0502가 날 수 있습니다.

해결: 필요한 값만 복사/복제하거나, get의 스코프를 줄이기

use std::collections::HashMap;

fn main() {
    let mut m = HashMap::new();
    m.insert("a".to_string(), 1);

    let v = m.get("a").copied(); // i32는 Copy

    m.insert("b".to_string(), 2);
    println!("{:?}", v);
}

값이 String처럼 Copy가 아니라면 cloned()를 고려합니다.

E0507: 빌린 값에서 이동할 수 없음

E0507은 전형적으로 다음 형태입니다.

  • cannot move out of ... which is behind a shared reference
  • 또는 cannot move out of borrowed content

즉, &T 또는 &mut T로 빌린 상태에서 그 안의 값을 move하려고 했다는 뜻입니다. Rust는 “참조는 소유권이 아니다”를 매우 엄격히 적용합니다.

1) Option에서 값을 꺼내 move하려다 실패

fn take_name(user: &User) -> String {
    user.name.unwrap()
}

struct User {
    name: Option<String>,
}

user&User이고, user.nameOption<String>입니다. unwrap()은 내부의 String을 move해서 반환하려고 하므로 E0507이 납니다.

해결 A: 소유권을 가져오려면 &mut로 받고 take()

struct User {
    name: Option<String>,
}

fn take_name(user: &mut User) -> Option<String> {
    user.name.take()
}

fn main() {
    let mut u = User { name: Some("alice".to_string()) };
    let name = take_name(&mut u);
    assert_eq!(name.as_deref(), Some("alice"));
    assert!(u.name.is_none());
}

take()OptionNone으로 대체하면서 내부 값을 move해줍니다. 이게 가장 “Rust다운” 해결책입니다.

해결 B: 소유권이 아니라 참조만 필요하면 as_deref 또는 as_ref

struct User {
    name: Option<String>,
}

fn name_ref(user: &User) -> Option<&str> {
    user.name.as_deref()
}

fn main() {
    let u = User { name: Some("alice".to_string()) };
    assert_eq!(name_ref(&u), Some("alice"));
}

해결 C: 복제가 허용된다면 clone()

struct User {
    name: Option<String>,
}

fn name_owned(user: &User) -> Option<String> {
    user.name.clone()
}

성능과 할당 비용이 늘 수 있으니, 데이터 크기와 호출 빈도를 보고 선택합니다.

2) Vec에서 원소를 move하려다 실패

fn pop_front(v: &Vec<String>) -> String {
    v[0]
}

인덱싱 v[0]String을 move하려고 하지만 &Vec<String>에서 꺼내는 것이므로 E0507입니다.

해결 A: 참조 반환

fn front(v: &Vec<String>) -> &str {
    &v[0]
}

반환 타입을 &String 또는 &str로 바꾸면 move가 아니라 borrow가 됩니다.

해결 B: 실제로 소유권을 꺼내고 싶다면 &mut Vecremove

fn pop_front(v: &mut Vec<String>) -> String {
    v.remove(0)
}

fn main() {
    let mut v = vec!["a".to_string(), "b".to_string()];
    let x = pop_front(&mut v);
    assert_eq!(x, "a");
    assert_eq!(v.len(), 1);
}

remove(0)O(n) 이동 비용이 있습니다. 큐처럼 앞에서 자주 빼야 한다면 VecDeque가 더 적합합니다.

3) 구조체 필드를 move하려다 실패: “부분 이동(partial move)”

#[derive(Debug)]
struct Job {
    id: u64,
    payload: String,
}

fn main() {
    let job = Job { id: 1, payload: "data".to_string() };

    let p = job.payload; // move
    println!("{job:?}");
    drop(p);
}

payload를 move하면 job은 더 이상 완전한 값이 아니어서 이후 사용이 제한됩니다. 이건 E0507 또는 관련 에러로 이어질 수 있습니다.

해결 A: 필드만 빌리기

#[derive(Debug)]
struct Job {
    id: u64,
    payload: String,
}

fn main() {
    let job = Job { id: 1, payload: "data".to_string() };

    let p = &job.payload;
    println!("payload={p}");
    println!("{job:?}");
}

해결 B: 소유권을 빼야 한다면 mem::take 또는 replace

use std::mem;

#[derive(Debug)]
struct Job {
    id: u64,
    payload: String,
}

fn main() {
    let mut job = Job { id: 1, payload: "data".to_string() };

    let payload = mem::take(&mut job.payload);
    // job.payload는 빈 String으로 대체됨
    println!("taken={payload}");
    println!("after={job:?}");
}

이 패턴은 “필드를 move하고 싶지만 구조체 자체는 계속 유효해야 하는” 상황에서 자주 씁니다.

E0502와 E0507을 한 번에 줄이는 사고방식

두 에러는 형태가 달라도, 실전에서는 아래 체크리스트로 빠르게 정리됩니다.

1) 참조를 잡아두는 시간을 최소화하라

  • let r = &x; ... x_mutate(); ... use(r); 형태면 대부분 E0502 후보입니다.
  • 해결은 use를 앞당기거나, 블록으로 스코프를 끊거나, 필요한 값만 Copy/clone해서 들고 가는 방식입니다.

2) “읽기 단계”와 “쓰기 단계”를 분리하라

  • 컬렉션을 순회하며 동시에 수정하려 하면 충돌이 납니다.
  • 먼저 조건을 스캔해서 작업 목록을 만들고, 마지막에 반영하는 방식이 안정적입니다.

3) move가 필요한지, borrow면 되는지 먼저 결정하라

  • E0507을 보면 “아, move가 발생했구나”라고 생각하면 됩니다.
  • 반환 타입이 굳이 String이어야 하는지 &str이면 되는지부터 재검토하면 해결이 빨라집니다.

4) 소유권을 꺼내야 한다면 “대체하면서 꺼내기”를 써라

  • Option이면 take()
  • 필드면 std::mem::take 또는 std::mem::replace
  • 컬렉션이면 remove, swap_remove, drain 같은 API를 검토

자주 쓰는 해결 패턴 요약

  • E0502

    • 참조 스코프 줄이기(블록, 사용 순서 변경)
    • Copy/cloned()로 값만 확보
    • 읽기/쓰기 분리(작업 목록 수집 후 반영)
    • 슬라이스는 split_at_mut로 비중첩을 증명
    • 맵은 entry API로 단일 가변 빌림 안에서 처리
  • E0507

    • move 대신 참조 반환(&T, &str)
    • clone()으로 소유 데이터 복제
    • Option::take로 소유권 꺼내기
    • mem::take/replace로 필드 대체 후 move

마무리: “컴파일러가 싫어하는 코드 모양”을 피하는 리팩터링

Rust의 소유권 규칙은 단순 암기가 아니라 “코드 구조”에 영향을 줍니다. E0502는 대개 빌림이 너무 오래 살아있거나, 읽기와 쓰기가 같은 스코프에 섞여 생깁니다. E0507은 대개 참조에서 소유권을 꺼내려는 설계 때문에 생깁니다.

해결의 공통점은 하나입니다. Rust가 안전함을 증명할 수 있도록, 스코프를 쪼개고(수명 단축) 단계를 분리하고(읽기/쓰기 분리) **move가 필요한 곳에만 명시적으로 소유권 경로를 열어주는 것(take, mem::take)**입니다.

이런 관점으로 코드를 다시 보면, 에러 메시지가 “금지”가 아니라 “더 좋은 구조로의 힌트”로 읽히기 시작합니다.