문자열(String)과 &str
Rust에서 문자열은 항상 UTF-8로 인코딩되며 크게 String 타입과 &str(스트링 슬라이스) 타입으로 구분된다. String은 가변(growable)이며 힙에 할당되는 소유(owned) 타입이다. &str은 메모리에 저장된 문자열 데이터의 슬라이스를 가리키는 참조(reference) 타입이다. 이 두 타입은 사용 목적이나 메모리 모델이 다르므로 상황에 따라 적절히 선택해야 한다.
String은 확장 가능하고 소유권이 있어 독립적으로 메모리를 관리한다. 예를 들어 문자열 리터럴 "Hello"를 기반으로 String을 생성하면 새로운 힙 영역이 할당되고, 해당 메모리는 String이 스코프를 벗어날 때 자동으로 해제된다. String은 메서드를 통해 내용을 변경할 수 있다. push_str 메서드를 사용하면 다른 문자열 슬라이스를 덧붙일 수 있고, push 메서드를 사용하면 단일 문자(char)를 추가할 수 있다. 메모리 용량(capacity)이 부족해지면 자동으로 다시 할당(reallocation)이 일어나 용량을 늘린다. 이 과정에서 성능 이슈가 생길 수 있으므로, 예측 가능한 크기를 미리 할당하는 것도 고려해야 한다.
&str은 힙에 있는 String의 일부분을 가리키거나, 바이너리 내부의 문자열 리터럴을 가리킬 수 있다. 문자열 리터럴은 컴파일 시에 바이너리에 저장되는 불변 데이터 영역에 위치하며, 프로그램이 종료될 때까지 유효하다. 이런 &str은 읽기 전용 참조로 동작하고, 소유권이 없으며, 문자열 내용을 변경할 수 없다. &str은 문자열 전체를 다루는 대신 부분 슬라이스를 가리키도록 만들 수도 있다. 범위를 나타내는 인덱스를 사용해 &str을 생성하면 실제로는 새로운 문자열을 복사하지 않고, 원본 문자열의 특정 바이트 구간을 참조할 뿐이다.
문자열 슬라이스를 다룰 때는 UTF-8 문자의 경계에 유의해야 한다. Rust 문자열은 UTF-8을 통해 문자 하나가 여러 바이트로 표현될 수 있다. 만약 바이트 중간을 잘못 슬라이싱하면 유효하지 않은 UTF-8 데이터를 참조하게 될 수 있으므로 런타임에 패닉이 발생할 수 있다. 그래서 &str을 범위로 슬라이싱할 때는 반드시 유효한 문자 경계를 고려해야 한다.
Rust에서 문자열에 인덱스를 사용해 직접 접근하는 것은 허용되지 않는다. 이는 Rust가 문자열의 내부 표현을 UTF-8로 지정해 두었기 때문에, 단순히 인덱스로 접근하면 의도치 않게 바이트 단위로 분절된 문자를 가져올 수 있기 때문이다. 특정 문자를 처리하고 싶다면 chars(), bytes(), char_indices()와 같은 이터레이터 메서드를 사용해 올바른 단위로 접근해야 한다. chars()는 유니코드 스칼라 값 단위로 순회하며, bytes()는 바이트 단위로 순회한다.
String 타입은 소유권을 가지므로, 다른 스레드로 이동하거나 함수에 인자로 넘길 때 String을 그대로 사용하면 그 소유권이 이동한다. 반면 &str은 참조 타입이기 때문에 빌려온 형태로만 사용하게 된다. 이 차이는 함수 설계에서 매우 중요하다. 인자로 문자열 데이터를 받을 때, 소유권을 가져와야 한다면 String을 선택하고, 단순히 빌려만 쓰면 되면 &str을 사용한다.
예시 코드를 통해 차이를 살펴볼 수 있다.
fn main() {
let owned = String::from("Hello, ");
let slice = "World!"; // &str 타입, 문자열 리터럴
// String의 가변성을 활용해 다른 문자열을 추가
let mut greeting = owned;
greeting.push_str(slice);
println!("{}", greeting);
// &str은 원본 데이터를 빌려 쓰므로 이 자체로는 변경 불가능
let another_slice = &greeting[0..5];
println!("{}", another_slice);
}위 코드에서 owned는 힙에 할당된 String 타입이다. "Hello, " 문자열을 소유하며, 이후 greeting이라는 가변 변수에 대입되어 문자열을 더 붙일 수 있었다. slice는 바이너리에 저장된 "World!"라는 &str(문자열 리터럴)을 가리킨다. greeting에 slice를 붙여 최종적으로 "Hello, World!" 문자열이 생성되었다. &greeting[0..5]는 greeting의 부분 슬라이스를 가리키는 &str이다.
String과 &str 모두 표준 라이브러리의 다양한 메서드를 제공한다. String은 가변성 때문에 문자열을 생성하고 수정하는 메서드(push, push_str, insert, remove 등)를 제공한다. &str은 불변 참조로서 split, trim, to_lowercase, to_uppercase 같은 문자열 조작 메서드를 주로 활용한다. 또한 &str은 String으로 소유권을 옮길 수 있는 to_string 메서드나 String::from 함수를 사용할 수 있다.
타 언어와 비교했을 때 Rust의 문자열은 UTF-8 인코딩을 강제한다는 점과 엄격한 소유권 및 슬라이스 모델을 가지고 있다는 점이 큰 특징이다. 이는 런타임 오류를 줄이고 안전성을 높이는 동시에, 문자 경계를 잘못 다루는 실수를 방지할 수 있게 해준다.
String과 &str을 제대로 이해하면, Rust에서의 문자열 처리와 메모리 모델을 훨씬 직관적으로 다룰 수 있게 된다. 문자열 리터럴은 불변이고 오래 살지만, 수정이 필요하면 String을 사용해 힙에 새롭게 할당하고, 복잡한 문자열 처리 로직이 있는 상황에서 &str을 인자로 받아들여 데이터 복제를 최소화하는 설계를 할 수 있다. 이 모든 특징은 Rust가 안전하고 효율적인 시스템 언어로서 자리매김하는 데 도움을 준다.
Last updated