# 문자열(String)과 문자열 슬라이스(\&str)

러스트에서 문자열을 다룰 때 가장 자주 마주치는 두 가지 타입은 String과 \&str이다. 둘 다 UTF-8로 인코딩된 텍스트를 표현하지만, 소유권과 가변성, 그리고 메모리 사용 측면에서 차이가 있다. 러스트는 메모리 안전성과 성능을 모두 만족하기 위해 이 두 타입을 명확히 구분하며, 각 타입을 적절히 선택해야 한다.

String은 힙(Heap)에 할당되는 가변(growable) 문자열이다. String은 소유권을 갖는 타입이기 때문에, String 인스턴스가 스코프를 벗어나면 할당된 힙 메모리는 자동으로 해제된다. 또한 String은 필요에 따라 용량(capacity)을 조절하면서 크기를 늘리거나 줄일 수 있다. 예를 들어 push\_str 메서드를 사용하면 기존 문자열의 끝에 새로운 문자열을 추가할 수 있다.

```rust
fn main() {
    let mut s = String::from("Hello");
    s.push_str(", world!");
    println!("{}", s);
}
```

위 예제에서 String::from("Hello")를 통해 String 값을 생성한다. 문자열 리터럴 "Hello"는 불변 참조 형태의 \&str이지만, from 함수를 통해 힙에 새로운 String을 생성하여 s가 그 소유권을 가지게 된다. s가 가변(mutable)이므로 push\_str을 사용해 내용을 변경할 수 있다.

\&str은 문자열 슬라이스로서, 주로 불변 참조 타입이다. 문자열 리터럴(예: "Hello")은 런타임 중에 변경될 수 없는 메모리에 고정되어 있으며, \&str은 이를 가리키는 형태를 가진다. \&str은 어디까지나 “슬라이스”일 뿐이므로 자체적으로 메모리를 소유하지 않고, 그 대신 다른 문자열 데이터(문자열 리터럴 혹은 String) 위에 얹혀져 있는 관점에서 바라보면 된다. 따라서 \&str은 힙을 직접 관리하지 않으며, 그 길이와 시작 지점에 대한 참조 정보를 갖는다.

```rust
fn main() {
    let s_slice: &str = "Hello, world!";
    println!("{}", s_slice);
}
```

위 예제처럼 "Hello, world!"라는 문자열 리터럴은 빌드 시점부터 프로그램 내부에 저장되고, s\_slice는 그 메모리 주소와 길이를 가리키기만 한다. 그러므로 s\_slice 자체가 소유권을 행사하는 것이 아니므로 별도의 할당 해제 과정이 필요하지 않다.

String과 \&str의 가장 중요한 차이 중 하나는 수정 가능 여부다. String은 가변적이므로 필요에 따라 문자열을 추가로 결합하거나 변경할 수 있다. \&str은 불변이므로, 내용 자체를 변경할 수 없다. 만약 \&str을 변경하려면 새로운 String을 생성하거나, 이미 가변 String을 참조하고 있는 \&mut str를 사용해야 한다. 그러나 후자의 경우는 직접 다룰 일이 비교적 드물고, 보통은 String을 소유한 뒤에 그 참조를 얻어 쓰는 방식이 주를 이룬다.

러스트에서 문자열은 내부적으로 UTF-8로 인코딩된다. 따라서 개별 인덱스로 문자를 접근하는 행위는 권장되지 않는다. UTF-8은 가변 길이 인코딩을 사용하므로, 단일 바이트 인덱스로 특정 문자를 보장할 수 없기 때문이다. 인덱스로 접근하는 대신, 문자열을 슬라이스로 잘라내거나 chars 메서드 같은 반복자를 사용하는 방식이 안정적이다.

```rust
fn main() {
    let s = String::from("안녕하라");
    // 올바른 부분 슬라이스
    let slice = &s[0..3]; // UTF-8 인코딩 상 "안"만 추출
    println!("{}", slice);

    for c in s.chars() {
        println!("{}", c);
    }
}
```

위 예제에서 s\[0..3]는 "안"에 해당하는 바이트 범위만 골라서 슬라이스로 만든다. 만약 UTF-8 경계를 벗어난 임의의 바이트 범위를 슬라이스로 만들면 런타임에 오류가 발생한다. 정확한 경계를 결정하기 어렵다면 chars 메서드를 사용하면 각 유니코드 문자를 순회할 수 있다.

문자열 슬라이스는 주로 함수의 파라미터로 유용하다. 함수가 문자열 데이터를 인자로 받아야 하지만, 그 문자열을 직접 수정하거나 소유할 필요가 없다면 \&str을 사용하는 것이 좋다. 불필요한 복사를 방지해 성능상의 이점을 얻을 수 있고, 함수 내부에서도 불변 참조라는 점이 더욱 안전하다.

```rust
fn print_message(msg: &str) {
    println!("메시지: {}", msg);
}

fn main() {
    let owned_string = String::from("소유된 문자열");
    let borrowed_str = "문자열 리터럴";

    print_message(&owned_string);
    print_message(borrowed_str);
}
```

이 예제에서 print\_message 함수는 \&str 타입을 받는다. owned\_string이 String임에도 불구하고 &를 붙여 \&str로 빌려올 수 있다. borrowed\_str은 이미 문자열 리터럴이므로 곧바로 \&str이다. 이렇게 함으로써 함수가 어떤 종류의 문자열이든 다룰 수 있고, 함수가 문자열을 소유하지 않기 때문에 불필요한 힙 할당 없이 높은 효율을 유지한다.

정리하자면 String은 힙에 저장되는 가변적이고 소유권을 갖는 문자열 타입이고, \&str은 다른 문자열 리소스를 불변 참조하는 슬라이스 타입이다. 문자열을 만들고 변경해야 한다면 String을, 이미 존재하는 문자열을 단순히 읽기만 한다면 \&str을 사용한다. 러스트의 문자열은 내부적으로 UTF-8 인코딩을 따르므로 인덱스 기반 접근은 주의가 필요하며, chars나 잘 정의된 슬라이스 범위를 통해 안전하게 문자에 접근할 수 있다. 이러한 방식을 숙지하면 러스트에서 효율적이고 안전하게 문자열을 다룰 수 있다.
