# 스칼라 타입과 복합 타입

러스트에서 데이터 타입은 크게 스칼라 타입과 복합 타입으로 나눌 수 있다. 스칼라 타입은 하나의 값만 담는 단일 단위의 타입이며, 복합 타입은 여러 값을 한꺼번에 담는 타입이다. 러스트는 정적 타입 언어이기 때문에 모든 변수와 상수에는 컴파일 시점에 명확한 타입이 할당되어야 한다. 정확하고 풍부한 타입 시스템은 메모리를 효율적으로 사용하게 하면서도 안전성을 높여준다.

스칼라 타입에는 정수, 부동소수점, 불(boolean), 문자 타입이 포함된다. 정수 타입은 부호가 있는 정수(i 계열)와 부호가 없는 정수(u 계열)로 나뉘며, 각각 8비트(i8, u8), 16비트(i16, u16), 32비트(i32, u32), 64비트(i64, u64), 128비트(i128, u128), 그리고 플랫폼 종속 정수(isize, usize)를 제공한다. 예를 들어, 64비트 정수 i64의 경우 -9,223,372,036,854,775,808부터 9,223,372,036,854,775,807까지 표현할 수 있다. 부호가 없는 정수 u64는 0부터 18,446,744,073,709,551,615까지 표현한다. 러스트는 기본 정수 리터럴을 i32로 간주하지만, 변수 선언 시 타입 어노테이션을 통해 다른 정수 타입을 명시할 수 있다. 또한 10진수(1000), 16진수(0xff), 8진수(0o77), 2진수(0b1111\_0000) 등 다양한 형태로 정수 리터럴을 표기할 수 있으며, 가독성을 높이기 위해 밑줄로 자릿수를 구분할 수도 있다.

부동소수점 타입은 f32와 f64를 제공한다. f32는 32비트 부동소수점이고 f64는 64비트 부동소수점이며, 기본 부동소수점 타입은 f64로 가정된다. 예를 들어 3.14와 같은 실수 리터럴은 컴파일러가 기본적으로 f64로 간주한다. 부동소수점 연산은 부정확성이 발생할 수 있으므로, 금전 계산처럼 정밀도를 엄격히 요구하는 경우에는 부동소수점 대신 정수 연산을 사용하거나 별도의 라이브러리를 고려해야 한다.

불 타입은 참(true) 또는 거짓(false)의 두 가지 값만 가질 수 있다. 불 타입은 조건 분기에 자주 사용되며, 길이가 고정되어 있지 않기 때문에 실제로는 1바이트만 차지하는 식으로 최적화될 수 있다. 그러나 러스트 수준에서 불 타입은 오직 참과 거짓만을 표현하는 용도로 사용된다.

문자 타입 char는 유니코드 스칼라 값(Unicode Scalar Value)을 표현하며, 4바이트 크기를 가진다. ASCII 문자부터 이모지까지 유니코드로 표현 가능한 다양한 문자를 단일 char 값으로 처리할 수 있다. 예를 들어 'a', '한', '語' 같은 문자를 모두 char 타입 하나로 저장할 수 있다는 점이 러스트의 특징 중 하나다.

복합 타입에는 튜플과 배열이 대표적이다. 튜플(tuple)은 여러 서로 다른 타입의 데이터를 하나로 묶어 처리할 수 있는 타입이며, 괄호 안에 각 요소를 ,로 구분해 나열한다. 튜플의 길이는 한 번 정의되면 고정되며, 전체 구조가 하나의 타입으로 인식된다. 예를 들어 (i32, f64, char) 형태의 튜플에는 정수, 부동소수점, 문자를 한꺼번에 담을 수 있다. 튜플의 각 요소에 접근하려면 .연산자와 인덱스 번호를 사용한다.

```rust
fn main() {
    let person: (&str, i32, f64) = ("Alice", 30, 1.68);
    println!("이름: {}", person.0);
    println!("나이: {}", person.1);
    println!("키: {}", person.2);
}
```

배열(array)은 동일한 타입의 데이터를 연속된 메모리 구역에 저장한다. 배열은 길이가 고정되며, 배열의 타입 표기는 \[T; N] 형태를 사용한다. 예를 들어 \[i32; 5]는 i32 타입의 값을 다섯 개 담는 배열을 의미한다. 배열도 요소에 인덱스로 접근할 수 있으며 0부터 시작하는 정수 인덱스를 사용한다. 배열의 길이는 컴파일 시점에 고정되어 있어야 하므로, 동적인 크기를 갖고 싶다면 벡터(vector) 등의 다른 자료구조를 사용해야 한다.

```rust
fn main() {
    let numbers: [i32; 3] = [10, 20, 30];
    println!("첫 번째 값: {}", numbers[0]);
    println!("두 번째 값: {}", numbers[1]);
    println!("세 번째 값: {}", numbers[2]);
}
```

배열은 스택에 할당되며, 컴파일러가 정확한 크기와 범위를 알 수 있기 때문에 효율적이다. 다만 길이가 커다란 배열을 선언하면 스택 공간을 많이 차지할 수 있으므로 적절히 사용해야 한다. 또한 러스트에서 배열의 인덱스를 벗어나는 접근은 컴파일 타임에는 잡히지 않을 수 있지만 런타임 시 체크되어 잘못된 접근이 있으면 즉시 패닉을 발생시킨다. 이 덕분에 안전한 메모리 관리를 보장한다.

배열과 달리 슬라이스(slice)는 컴파일 시점에 길이가 고정되지 않으며, 배열이나 벡터 등에 대해 부분적으로 참조할 수 있는 뷰(view)를 제공한다. 슬라이스는 러스트의 참조 타입 중 하나이며, 실제 데이터는 다른 곳에 저장하고 슬라이스는 해당 데이터의 포인터와 길이 정보를 갖는다. 슬라이스는 &\[T] 형태로 표기하며, 종종 문자열 슬라이스 \&str 형태로 많이 사용된다. 슬라이스를 통해 전체 컬렉션을 복제할 필요 없이 원하는 구간만 안전하게 접근할 수 있다.

```rust
fn main() {
    let nums = [1, 2, 3, 4, 5];
    let slice_of_nums = &nums[1..4];
    println!("슬라이스 길이: {}", slice_of_nums.len());
    println!("슬라이스 첫 번째 값: {}", slice_of_nums[0]);
}
```

복합 타입 중 하나인 문자열 타입 String도 내부적으로는 UTF-8 인코딩을 사용하는 가변 길이 버퍼로, 힙 메모리에 저장된다. 이와는 별도로 문자열 슬라이스인 \&str 타입은 변경 불가능한 문자열 뷰로서 주로 리터럴이나 String의 일부를 참조할 때 사용된다.

스칼라 타입과 복합 타입에 대한 정확한 이해는 러스트를 안전하고 효율적으로 사용하는 데 큰 도움이 된다. 각 타입별로 메모리를 어떻게 관리하고, 런타임 비용이 어떻게 발생하는지 파악한다면, 러스트가 제공하는 강력한 안전성 덕분에 오류 발생을 줄이면서도 높은 성능을 유지할 수 있을 것이다. 이를 기반으로 더 복잡한 자료구조나 사용자 정의 타입을 다룰 때에도 안정적이고 확장성 있는 코드를 작성할 수 있다.
