본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 2장: 소유권과 차용 체크 멘탈 모델
2026년 2월 13일·프로그래밍·

2장: 소유권과 차용 체크 멘탈 모델

Rust의 핵심 개념인 소유권 규칙 3가지, Move 시맨틱스, 불변/가변 빌림, 라이프타임 기초를 백엔드 개발자 관점에서 체계적으로 다룹니다.

17분477자11개 섹션
rust
공유
rust-backend2 / 11
1234567891011
이전1장: 백엔드 개발자가 Rust를 배워야 하는 이유다음3장: Rust 타입 시스템과 에러 처리

학습 목표

  • 소유권의 세 가지 규칙을 명확히 이해합니다
  • Move 시맨틱스와 Copy 트레이트의 차이를 파악합니다
  • 불변 빌림과 가변 빌림의 규칙을 익힙니다
  • 라이프타임의 기초 개념을 이해합니다
  • String vs &str, Vec vs Slice의 관계를 파악합니다

소유권이 필요한 이유

다른 언어에서는 메모리 관리를 개발자(C/C++)나 런타임(Java, Go, Python)에게 맡깁니다. Rust는 제3의 방법을 선택했습니다. 컴파일러가 소유권 규칙을 검증하여 메모리 안전성을 보장하는 것입니다.

이것은 단순한 기술적 선택이 아닙니다. 메모리 안전성 버그는 모든 보안 취약점의 약 70%를 차지한다는 Microsoft와 Google의 보고가 있습니다. 소유권 시스템은 이 문제를 근본적으로 해결합니다.

소유권의 세 가지 규칙

Rust의 소유권 시스템은 세 가지 규칙으로 요약됩니다.

  1. 각 값에는 소유자(owner)가 하나만 존재합니다
  2. 한 시점에 소유자는 단 하나뿐입니다
  3. 소유자가 스코프를 벗어나면 값은 자동으로 해제(drop)됩니다
소유권 기본 예제
rust
fn main() {
    let s1 = String::from("hello"); // s1이 String의 소유자
    let s2 = s1;                     // 소유권이 s1에서 s2로 이동(move)
 
    // println!("{}", s1); // 컴파일 에러! s1은 더 이상 유효하지 않음
    println!("{}", s2);    // s2가 소유자이므로 사용 가능
} // s2가 스코프를 벗어남 → 메모리 자동 해제

이 규칙들이 합쳐지면 다음을 보장합니다.

  • 이중 해제(double free) 방지: 소유자가 하나이므로 같은 메모리를 두 번 해제할 수 없습니다
  • 댕글링 포인터(dangling pointer) 방지: 해제된 메모리를 참조하는 것이 불가능합니다
  • 메모리 누수 최소화: 소유자가 스코프를 벗어나면 반드시 해제됩니다

Move 시맨틱스

위 예제에서 let s2 = s1을 실행하면 소유권이 **이동(Move)**합니다. 이것은 복사가 아닙니다.

Copy 트레이트

단, 스택에 저장되는 단순한 타입들은 Move 대신 **Copy(복사)**됩니다. 이를 **Copy 트레이트(trait)**를 구현한 타입이라 합니다.

Copy 타입 vs Move 타입
rust
fn main() {
    // Copy 타입: 정수, 부동소수점, bool, char, 고정 크기 튜플
    let x = 42;
    let y = x;      // Copy — x도 여전히 유효
    println!("x={}, y={}", x, y); // 정상 동작
 
    // Move 타입: String, Vec, Box 등 힙 데이터를 소유하는 타입
    let s1 = String::from("hello");
    let s2 = s1;    // Move — s1은 무효
    // println!("{}", s1); // 컴파일 에러
}
Info

규칙은 간단합니다. 스택에만 존재하는 고정 크기 타입은 Copy, 힙 데이터를 소유하는 타입은 Move입니다. 직접 만든 구조체도 모든 필드가 Copy이면 #[derive(Copy, Clone)]으로 Copy를 구현할 수 있습니다.

빌림(Borrowing)

모든 곳에서 소유권을 이동시키면 코드가 매우 불편해집니다. 그래서 Rust는 **빌림(Borrowing)**을 제공합니다. 소유권을 넘기지 않고 값을 참조할 수 있는 방법입니다.

불변 빌림 (Immutable Borrow)

&T 형태로 불변 참조를 만듭니다. 동시에 여러 개의 불변 참조가 존재할 수 있습니다.

불변 빌림
rust
fn calculate_length(s: &String) -> usize {
    s.len()
    // s는 빌린 것이므로 여기서 해제되지 않음
}
 
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // s1을 빌려줌
    println!("'{}'의 길이는 {}", s1, len); // s1 여전히 사용 가능
}

가변 빌림 (Mutable Borrow)

&mut T 형태로 가변 참조를 만듭니다. 한 시점에 가변 참조는 단 하나만 존재할 수 있습니다.

가변 빌림
rust
fn append_world(s: &mut String) {
    s.push_str(", world");
}
 
fn main() {
    let mut s = String::from("hello");
    append_world(&mut s);
    println!("{}", s); // "hello, world"
}

빌림 규칙

Rust의 빌림 규칙은 두 가지입니다.

  1. 불변 참조는 여러 개 동시에 존재할 수 있습니다 (&T 여러 개)
  2. 가변 참조는 단 하나만 존재할 수 있습니다 (&mut T 하나)
  3. 불변 참조와 가변 참조는 동시에 존재할 수 없습니다
빌림 규칙 위반 예제
rust
fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;     // 불변 참조 — OK
    let r2 = &s;     // 불변 참조 — OK (여러 개 가능)
    // let r3 = &mut s; // 컴파일 에러! 불변 참조가 있는 동안 가변 참조 불가
 
    println!("{} and {}", r1, r2);
    // r1, r2는 여기서 마지막으로 사용됨 (NLL 덕분에 여기서 빌림 종료)
 
    let r3 = &mut s;  // 이제 가변 참조 가능
    r3.push_str("!");
    println!("{}", r3);
}
Tip

**NLL(Non-Lexical Lifetimes)**은 Rust 2018 에디션부터 적용된 기능입니다. 참조의 수명이 마지막 사용 지점에서 끝나므로, 위 예제처럼 r1, r2를 마지막으로 사용한 후에는 가변 참조를 만들 수 있습니다.

이 규칙이 왜 중요할까요? 이 규칙이 **데이터 레이스(data race)**를 컴파일 타임에 방지하기 때문입니다. 데이터 레이스는 다음 세 조건이 동시에 충족될 때 발생합니다.

  • 두 개 이상의 포인터가 같은 데이터에 접근
  • 그 중 하나 이상이 데이터를 쓰고 있음
  • 접근 동기화 메커니즘이 없음

Rust의 빌림 규칙은 이 세 조건이 동시에 충족되는 것을 원천 차단합니다.

String vs &str

백엔드 개발에서 문자열은 가장 자주 다루는 타입입니다. Rust에는 두 가지 핵심 문자열 타입이 있습니다.

String과 &str
rust
fn main() {
    // String: 힙에 할당, 소유권 있음, 가변 가능
    let mut owned = String::from("hello");
    owned.push_str(", world");
 
    // &str: 문자열 슬라이스, 빌림, 불변
    let slice: &str = &owned;       // String에서 &str로 빌림
    let literal: &str = "hello";    // 문자열 리터럴은 &str
 
    greet(&owned);   // String → &str 자동 변환 (Deref coercion)
    greet(literal);  // &str 직접 전달
}
 
fn greet(name: &str) {
    println!("Hello, {}!", name);
}
특성String&str
저장 위치힙어디서든 빌림
소유권있음없음 (빌림)
가변성가변 가능불변
크기가변고정 (포인터 + 길이)
함수 매개변수 권장소유권이 필요할 때읽기만 할 때
Tip

함수의 매개변수로 문자열을 받을 때는 &str을 사용하는 것이 관례입니다. 호출자가 String이든 &str이든 모두 받을 수 있기 때문입니다. 소유권이 필요한 경우에만 String을 받으세요.

Vec과 슬라이스

동일한 패턴이 벡터에도 적용됩니다.

Vec과 슬라이스
rust
fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}
 
fn main() {
    let v: Vec<i32> = vec![1, 2, 3, 4, 5];
    let total = sum(&v);         // Vec<i32> → &[i32] 자동 변환
    println!("합계: {}", total);
 
    let partial = sum(&v[1..4]); // 슬라이스: [2, 3, 4]
    println!("부분 합계: {}", partial);
}

Vec<T>가 소유하는 타입이라면, &[T]는 빌리는 타입(슬라이스)입니다. String과 &str의 관계와 동일합니다.

라이프타임 기초

**라이프타임(Lifetime)**은 참조가 유효한 범위를 나타냅니다. 대부분의 경우 컴파일러가 자동으로 추론하지만, 때로는 명시적으로 지정해야 합니다.

라이프타임이 필요한 경우
rust
// 두 문자열 참조 중 더 긴 것을 반환
// 반환값이 어떤 입력의 참조인지 컴파일러가 판단할 수 없으므로
// 라이프타임을 명시해야 합니다
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
 
fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(&s1, &s2);
        println!("더 긴 문자열: {}", result); // s2가 아직 유효하므로 OK
    }
    // println!("{}", result); // 에러! s2가 스코프를 벗어남
}

'a라는 라이프타임 매개변수는 "반환값의 참조는 x와 y 중 더 짧은 수명을 따른다"는 것을 의미합니다. 이를 통해 댕글링 참조를 방지합니다.

Info

라이프타임이 처음에는 복잡하게 느껴질 수 있지만, 백엔드 개발에서는 대부분 구조체에 참조를 저장할 때만 명시적으로 작성합니다. 함수의 경우 컴파일러의 **라이프타임 생략 규칙(Lifetime Elision Rules)**이 대부분 자동 처리합니다.

다른 언어와의 비교

소유권 개념을 다른 언어의 경험에 빗대어 이해해 봅시다.

Java/Go 경험자를 위한 비유
rust
// Java에서 이렇게 작성하면:
// String s1 = new String("hello");
// String s2 = s1;  // 같은 객체를 참조 (GC가 관리)
 
// Rust에서는:
let s1 = String::from("hello");
let s2 = s1.clone();  // 명시적 복제 — 깊은 복사
println!("{} {}", s1, s2); // 둘 다 사용 가능 (각자 소유)
 
// 또는 빌림으로:
let s3 = String::from("world");
let s4 = &s3;  // 빌림 — 복사 없이 참조
println!("{} {}", s3, s4); // 둘 다 사용 가능
개념Java/GoRust
메모리 관리GC가 자동 회수소유권 + 스코프 기반 자동 해제
값 전달참조 복사 (얕은 복사)Move (소유권 이동)
깊은 복사clone() 명시clone() 명시
참조 공유자유롭게 공유빌림 규칙 준수 필요
데이터 레이스런타임에 발견컴파일 타임에 방지

실전 패턴: 백엔드 핸들러에서의 소유권

백엔드 코드에서 소유권이 실제로 어떻게 적용되는지 미리 살펴봅시다.

Axum 핸들러에서의 소유권 패턴
rust
use axum::{Json, extract::State};
use std::sync::Arc;
 
// AppState는 Arc로 감싸서 여러 핸들러가 공유
struct AppState {
    db_pool: String, // 실제로는 sqlx::PgPool
}
 
// Json<CreateUser>: 요청 본문의 소유권을 핸들러가 가져감
// State<Arc<AppState>>: 공유 상태를 빌림
async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<CreateUser>,
) -> Json<User> {
    // payload는 이 핸들러가 소유 — 자유롭게 사용
    // state는 Arc로 공유 — 여러 핸들러가 동시 접근 가능
    let user = User {
        name: payload.name, // Move: payload.name의 소유권이 user.name으로 이동
    };
    Json(user)
}
 
struct CreateUser { name: String }
struct User { name: String }

이 패턴은 5장에서 자세히 다루겠지만, 소유권이 실제 백엔드 코드에서 어떻게 자연스럽게 녹아드는지 보여줍니다.

정리

이 장에서는 Rust의 핵심 개념인 소유권과 빌림을 다루었습니다.

  • 소유권 규칙 3가지: 값에는 소유자가 하나, 소유자는 단 하나, 스코프 벗어나면 해제
  • Move 시맨틱스: 힙 데이터를 소유하는 타입은 대입 시 소유권이 이동합니다
  • 빌림 규칙: 불변 참조 여러 개 또는 가변 참조 하나, 둘은 동시에 불가
  • String vs &str: 소유 vs 빌림의 대표적인 쌍입니다
  • 라이프타임: 참조의 유효 범위를 컴파일러에게 알려주는 메커니즘입니다

다음 장에서는 Rust의 타입 시스템과 에러 처리를 다룹니다. 구조체, 열거형, 트레이트를 배우고, Result와 Option을 활용한 안전한 에러 처리 패턴을 익힙니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#rust

관련 글

프로그래밍

3장: Rust 타입 시스템과 에러 처리

구조체, 열거형, 트레이트부터 Result/Option, thiserror/anyhow를 활용한 에러 처리 패턴까지 Rust 타입 시스템의 핵심을 다룹니다.

2026년 2월 15일·14분
프로그래밍

1장: 백엔드 개발자가 Rust를 배워야 하는 이유

Rust의 고유 가치인 안전성과 성능, 백엔드 생태계 현황, GC 없는 메모리 관리, 2026년 채택 현황까지 백엔드 개발자 관점에서 Rust를 배워야 하는 이유를 살펴봅니다.

2026년 2월 11일·15분
프로그래밍

4장: async/await와 Tokio 런타임

Rust의 비동기 프로그래밍 기초부터 Future 트레이트, Tokio 런타임, spawn/select/join, 채널 기반 동시성 패턴까지 체계적으로 다룹니다.

2026년 2월 17일·15분
이전 글1장: 백엔드 개발자가 Rust를 배워야 하는 이유
다음 글3장: Rust 타입 시스템과 에러 처리

댓글

목차

약 17분 남음
  • 학습 목표
  • 소유권이 필요한 이유
  • 소유권의 세 가지 규칙
  • Move 시맨틱스
    • Copy 트레이트
  • 빌림(Borrowing)
    • 불변 빌림 (Immutable Borrow)
    • 가변 빌림 (Mutable Borrow)
    • 빌림 규칙
  • String vs &str
  • Vec과 슬라이스
  • 라이프타임 기초
  • 다른 언어와의 비교
  • 실전 패턴: 백엔드 핸들러에서의 소유권
  • 정리