[2026] Rust String vs &str 완벽 비교 | 문자열 타입 선택 가이드

[2026] Rust String vs &str 완벽 비교 | 문자열 타입 선택 가이드

이 글의 핵심

Rust String과 &str의 차이점을 소유권, 메모리, 성능 관점에서 비교. 실전에서 어떤 문자열 타입을 써야 하는지 선택 기준과 예제를 설명합니다.

들어가며

“String과 &str 중 무엇을 써야 할까요?” Rust를 배울 때 가장 헷갈리는 부분입니다. 이 글에서는 String과 &str의 차이를 명확히 이해하고, 상황에 맞는 타입을 선택하는 방법을 다룹니다. 비유로 말씀드리면, String내 책장에 꽂아 소유하는 책, &str도서관에서 잠시 빌려 읽는 구절에 가깝습니다. 수정이 필요하면 보통 String으로 만들고, 함수 인자로 읽기만 할 때는 &str이 자연스럽습니다.

언제 String을, 언제 &str을 쓰나요?

관점String&str
성능힙 할당·가변 버퍼참조만—가장 가벼운 읽기
사용성소유가 필요할 때리터럴·부분 문자열
적용 시나리오수집·누적·변경파싱·검색·함수 파라미터

이 글을 읽으면

  • String과 &str의 메모리 구조를 이해합니다
  • 소유권 관점에서 차이를 배웁니다
  • 성능과 메모리 사용량 차이를 익힙니다
  • 실전에서 어떤 것을 써야 하는지 판단할 수 있습니다

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

목차

  1. 빠른 비교표
  2. 메모리 구조
  3. 소유권과 빌림
  4. 성능 비교
  5. 변환 방법
  6. 실전 선택 가이드
  7. 흔한 실수
  8. 마무리

1. 빠른 비교표

특성String&str
타입소유 타입빌린 타입 (참조)
메모리힙 할당스택 또는 정적
가변성가변불변
크기동적고정
수명소유자가 제어빌림 규칙 적용
용도소유권 필요읽기만 필요
함수 인자String (소유권 이동)&str (권장)
반환값String (소유권 반환)&str (수명 주의)

2. 메모리 구조

String: 힙 할당

아래 코드는 rust를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

let s = String::from("hello");
// 메모리 구조
// 스택:
//   ptr:  0x12345678 (힙 주소)
//   len:  5
//   cap:  5
//
// 힙:
//   [h][e][l][l][o]

&str: 슬라이스 (참조)

아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

let s: &str = "hello"; // 문자열 리터럴 (정적 메모리)
// 메모리 구조
// 스택:
//   ptr:  0x87654321 (정적 메모리 주소)
//   len:  5
//
// 정적 메모리 (.rodata):
//   [h][e][l][l][o]

슬라이스 생성

다음은 rust를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

let s = String::from("hello world");
let hello: &str = &s[0..5];  // "hello"
let world: &str = &s[6..11]; // "world"
// 메모리 구조
// 스택:
//   s.ptr:     0x12345678
//   s.len:     11
//   s.cap:     11
//   hello.ptr: 0x12345678 (같은 힙 주소)
//   hello.len: 5
//   world.ptr: 0x1234567E (s.ptr + 6)
//   world.len: 5
//
// 힙:
//   [h][e][l][l][o][ ][w][o][r][l][d]
//    ^^^^^               ^^^^^
//    hello               world

일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.

3. 소유권과 빌림

String: 소유권

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

fn take_ownership(s: String) {
    println!("{}", s);
} // s가 여기서 drop됨
let s = String::from("hello");
take_ownership(s);
// println!("{}", s); // ❌ error: value borrowed after move

&str: 빌림

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

fn borrow(s: &str) {
    println!("{}", s);
} // 빌림만 하므로 drop 안 됨
let s = String::from("hello");
borrow(&s);
println!("{}", s); // ✅ 여전히 사용 가능

함수 시그니처 선택

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 패턴: 소유권 가져가기
fn process(s: String) {
    println!("{}", s);
}
let s = String::from("hello");
process(s);
// s를 더 이상 사용할 수 없음
// ✅ 좋은 패턴: 빌림
fn process(s: &str) {
    println!("{}", s);
}
let s = String::from("hello");
process(&s);
// s를 계속 사용 가능

4. 성능 비교

할당 비용

아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// String: 힙 할당 (느림)
let s1 = String::from("hello");
let s2 = s1.clone(); // 힙 메모리 복사
// &str: 포인터 복사 (빠름)
let s1: &str = "hello";
let s2 = s1; // 포인터만 복사 (8 bytes)

벤치마크

다음은 rust를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

use std::time::Instant;
// String 생성
let start = Instant::now();
for _ in 0..1_000_000 {
    let _ = String::from("hello");
}
println!("String: {:?}", start.elapsed()); // 약 50ms
// &str 복사
let start = Instant::now();
let s: &str = "hello";
for _ in 0..1_000_000 {
    let _ = s;
}
println!("&str: {:?}", start.elapsed()); // 약 0.1ms (500배 빠름)

5. 변환 방법

String → &str

아래 코드는 rust를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

let s = String::from("hello");
// 방법 1: 참조
let slice: &str = &s;
// 방법 2: as_str()
let slice: &str = s.as_str();
// 방법 3: 슬라이스
let slice: &str = &s[..];

&str → String

아래 코드는 rust를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

let s: &str = "hello";
// 방법 1: to_string()
let owned: String = s.to_string();
// 방법 2: String::from()
let owned: String = String::from(s);
// 방법 3: to_owned()
let owned: String = s.to_owned();

언제 변환하나?

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ 좋은 패턴: 필요할 때만 String 생성
fn process(s: &str) -> String {
    if s.is_empty() {
        return String::from("default");
    }
    
    // 수정이 필요하면 String으로 변환
    let mut result = s.to_string();
    result.push_str(" processed");
    result
}
// ❌ 나쁜 패턴: 불필요한 변환
fn process(s: &str) -> &str {
    let owned = s.to_string(); // 불필요한 할당
    &owned // ❌ error: returns reference to local variable
}

6. 실전 선택 가이드

함수 매개변수

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ 기본: &str (유연함)
fn print_message(msg: &str) {
    println!("{}", msg);
}
print_message("hello");           // 리터럴
print_message(&String::from("hello")); // String
print_message(&my_string[..]);    // 슬라이스
// ❌ String (소유권 필요 시만)
fn consume_message(msg: String) {
    // msg를 소비하거나 저장할 때만
}

구조체 필드

아래 코드는 rust를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 소유권 필요 → String
struct User {
    name: String,  // 구조체가 소유
    email: String,
}
// 빌림 → &str (수명 매개변수 필요)
struct UserRef<'a> {
    name: &'a str,  // 다른 곳에서 빌림
    email: &'a str,
}
// 실전: 대부분 String 사용
// &str은 수명 관리가 복잡함

반환값

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ String 반환 (소유권 이동)
fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}
// ❌ &str 반환 (수명 문제)
fn create_greeting(name: &str) -> &str {
    let greeting = format!("Hello, {}!", name);
    &greeting // ❌ error: returns reference to local variable
}
// ✅ &str 반환 (입력 수명과 연결)
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

7. 흔한 실수

실수 1: String을 함수 인자로

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 패턴
fn print(s: String) {
    println!("{}", s);
}
let s = String::from("hello");
print(s);
// s를 더 이상 사용할 수 없음
// ✅ 좋은 패턴
fn print(s: &str) {
    println!("{}", s);
}
let s = String::from("hello");
print(&s);
// s를 계속 사용 가능

실수 2: 불필요한 to_string()

아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 나쁜 패턴
fn process(s: &str) {
    let owned = s.to_string(); // 불필요한 할당
    println!("{}", owned);
}
// ✅ 좋은 패턴
fn process(s: &str) {
    println!("{}", s); // 그냥 사용
}

실수 3: &str 반환 시 수명 문제

다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 컴파일 안 됨
fn get_name() -> &str {
    let name = String::from("Alice");
    &name // error: returns reference to local variable
}
// ✅ String 반환
fn get_name() -> String {
    String::from("Alice")
}
// ✅ 정적 수명
fn get_default_name() -> &'static str {
    "Guest"
}

마무리

Rust 문자열 타입 선택의 핵심:

  1. 함수 인자는 &str (유연함)
  2. 소유권 필요 시 String (구조체 필드, 반환값)
  3. 읽기만 필요하면 &str (성능 좋음)
  4. 수명 관리 복잡하면 String (안전함) 핵심: 기본은 &str, 소유권이 필요할 때만 String을 사용하세요.

FAQ

Q1. String과 &str 중 뭐가 더 빠른가요? &str이 더 빠릅니다 (힙 할당 없음). 하지만 소유권이 필요하면 String을 써야 합니다. Q2. 구조체 필드는 항상 String인가요? 대부분 String을 사용합니다. &str은 수명 매개변수가 필요하여 복잡해집니다. Q3. format! 매크로는 String을 반환하나요? 네, format!은 항상 String을 반환합니다.

관련 글


키워드

Rust, String, str, 문자열, 소유권, 빌림, 수명, 성능, 메모리, 비교, 선택 가이드

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3