# String 타입과 메모리 관리

String은 러스트에서 가변적인 문자열을 표현하기 위해 사용되는 타입이며, 힙(Heap)에 데이터를 저장한다. 이는 문자열 리터럴(예: "Hello")과 달리 컴파일 타임에 고정된 불변 스트링이 아니라, 런타임에 크기가 변할 수 있는 동적 메모리 영역을 사용한다는 점에서 중요한 차이가 있다. String은 세 가지 주요 요소(힙 영역의 문자 데이터, 그 데이터의 시작 위치를 가리키는 포인터, 현재 문자열 길이와 용량 정보를 갖는 두 개의 정수)로 이루어져 있다. 이 구조가 만들어지는 순간, 러스트의 소유권(Ownership) 규칙이 적용되어 컴파일러가 메모리 안전성을 엄격히 보장한다.

String 타입이 힙 메모리를 사용하는 이유는 문자열 길이가 가변적이기 때문이다. 스택(Stack)에 데이터를 저장할 경우, 컴파일 타임에 필요한 정확한 메모리 크기를 알아야 한다. 하지만 문자열은 실행 중에 사용자 입력이나 외부 환경 등에 따라 얼마든지 길이가 달라질 수 있다. 이때 매번 스택에 재할당하는 것은 효율적이지도 않고, 안전성 문제도 야기할 수 있다. 반면, 힙 영역은 런타임에 동적으로 크기를 조절할 수 있으므로, String이 유연한 문자열 조작을 가능하게 한다.

러스트에서 String을 생성할 때는 주로 String::from 함수나 to\_string 메서드를 통해 힙 메모리에 새로운 문자열을 할당한다. 이 과정에서 소유권은 해당 String 변수로 귀속되고, 이 변수는 문자열 데이터를 단 하나만 온전히 소유한다. 예를 들어

```rust
let s1 = String::from("Hello");
```

라고 선언하면, "Hello"라는 문자열 리터럴의 내용을 참조해 힙에 새로운 공간을 마련하고 문자의 배열을 복사해 넣은 다음, s1이 그 데이터를 소유한다. 소유권은 이 시점부터 s1에 완전히 할당되어, s1이 스코프를 벗어날 때 자동으로 메모리가 해제(drop)된다.

러스트에서 중요한 규칙 중 하나는 Move semantics이다. String을 다른 변수로 이동할 때, 내부 포인터와 길이, 용량 정보만 복사되고 원래 소유권은 새로운 변수로 옮겨진다. 기존 변수를 사용하려 시도하면 컴파일 에러가 발생한다. 이는 메모리를 중복해서 해제하거나, 잘못된 참조가 발생하는 상황을 방지한다. 예를 들어

```rust
let s1 = String::from("Hello");
let s2 = s1;
println!("{}", s1); // 컴파일 에러
```

이 코드는 s2로 소유권이 이동했기 때문에 s1은 더 이상 유효한 String이 아니게 된다. 이러한 이동(Move) 규칙 덕분에 러스트는 런타임에 가비지 컬렉션 없이도 메모리 안전성을 유지한다.

String을 참조할 때는 빌림(Borrowing) 규칙이 적용된다. 빌림에는 불변 참조와 가변 참조가 있으며, 모든 참조는 동시에 유효한 범위 내에서 상호 간섭이 없도록 관리된다. 불변 참조(\&String)는 소유권을 빼앗지 않고, 데이터를 변경할 수 없지만 안전하게 문자열을 조회할 수 있다. 가변 참조(\&mut String)는 데이터를 변경할 수 있지만, 같은 시점에 오직 하나만 존재할 수 있다. 이 규칙을 통해 데이터 경합(Race Condition)이나 동시성 문제를 컴파일 타임에 방지한다.

String과 문자열 슬라이스(\&str)의 관계도 중요하다. String은 힙에 저장된 실제 데이터의 소유자이지만, 문자열 슬라이스는 그 데이터를 빌려 읽기 전용으로 참조한다. 예를 들어, \&s1\[..]는 s1의 일부나 전체를 바라보는 슬라이스이므로 실제 데이터에 대한 포인터와 길이만을 갖는다. 이 슬라이스는 소유권을 가지지 않으며, s1의 라이프타임이 유효한 동안에만 안전하게 사용할 수 있다. 이는 메모리 사용의 효율성을 높여주며, 불필요한 복사를 피할 수 있게 한다.

String을 다룰 때는 용량(Capacity)과 길이(Length)를 구분해야 한다. 길이는 현재 문자의 개수를 의미하고, 용량은 내부적으로 할당된 힙 메모리의 크기를 의미한다. 문자열에 더 많은 문자를 추가하면, 내부적으로 더 큰 메모리 공간을 할당해야 할 수 있다. 러스트는 이러한 재할당 과정을 개발자가 직접 관리하지 않아도 되도록 제공하되, 컴파일러가 할당과 해제를 자동으로 추적한다. 이때 move와 borrow 규칙이 중심이 되어, 안전성 문제 없이 동적 문자열을 다룰 수 있다.

스코프(Scope) 역시 String의 메모리 해제 시점에 깊이 관여한다. 어떤 변수가 스코프를 벗어날 때 drop 함수가 자동으로 호출되어, 소유하고 있는 힙 메모리가 해제된다. 이는 RAII(Resource Acquisition Is Initialization) 전략의 구현체로, 개발자가 매번 해제 코드를 작성하지 않아도 된다. 이러한 구조는 메모리 누수를 방지하고, 동시에 러스트 컴파일러가 이 과정을 제어해 예측 가능하고 안전한 프로그램을 만들 수 있다.

String은 내부적으로 UTF-8로 인코딩된 가변 크기 배열을 가지므로, 일부 언어에서 제공하는 인덱싱 연산을 그대로 적용하기 어렵다는 점을 이해해야 한다. UTF-8은 여러 바이트가 합쳐져 하나의 유니코드 문자(그래플림)를 형성할 수 있으므로, 단순히 바이트 인덱스로 접근하면 예상치 못한 결과가 나올 수 있다. 이 때문에 러스트는 String에서의 인덱싱 연산을 기본적으로 허용하지 않고, 대신 범위 슬라이스나 chars 메서드 등을 사용해 문자를 처리하도록 유도한다.

이처럼 String은 메모리 관리와 관련하여 소유권(Ownership)과 빌림(Borrowing)의 핵심 개념을 이해하는 좋은 예시가 된다. 힙 메모리에 안전하고 효율적으로 접근하기 위해서는 소유권이 어떻게 이전되는지, 빌림은 어떤 제약을 가지는지, 그리고 슬라이스는 어떤 상황에서 유용하게 활용되는지를 명확하게 파악해야 한다. 러스트는 이러한 메커니즘을 컴파일 타임에 엄격히 검사해, 안전하지만 빠른 실행을 가능하게 만든다. 이러한 시스템 덕분에 개발자는 런타임 오버헤드가 큰 가비지 컬렉터 없이도 동적 메모리를 자유롭게 사용할 수 있으며, 대규모 코드베이스에서도 메모리 안전성을 유지할 수 있다.
