데이터 타입에 따른 변수 바인딩(Variable Binding)

Rust에서 variable binding(변수 바인딩)은 값을 변수 이름에 연결하는 과정을 의미하며, Rust 프로그래밍에서 변수 선언과 관련된 기본 개념입니다. Rust는 정적 타입 언어이고 기본적으로 변수가 불변(immutable)으로 선언되기에, 가변성을 가지려면 일부러 명시해야 합니다. 이와 함께 Rust의 바인딩은 shadowing(변수 가리기)이나 스코프와 밀접하게 연관되어 있어, 변수 활용 시 중요한 개념입니다.

Ⅰ. 기본 자료형(Primitive Type)

1. 변수 바인딩 기본

Rust에서 변수 바인딩은 let 키워드로 선언합니다. 이때 변수는 기본적으로 불변입니다. 따라서 선언 후 값을 변경하려 하면 컴파일 에러가 발생합니다.

fn main() {
let x = 5;
println!("x의 값: {}", x);
// x = 6; // 오류: 불변 변수에 재할당 불가
}

이 코드는 x를 5로 바인딩했지만, 이후 x = 6과 같이 변경하려 하면 컴파일 에러가 발생합니다. Rust 컴파일러는 이런 불변 변수 변경 시도를 막아 안정성을 보장합니다.

2. 가변 변수 (Mutable Binding)

변수를 변경 가능하게 하려면 mut 키워드를 사용해 가변 변수로 선언해야 합니다.

fn main() {
let mut x = 5;
println!("초기 x: {}", x);
x = 6;
println!("변경된 x: {}", x);
}

이렇게 하면 x에 대한 값 변경이 가능해집니다. 컴파일러가 에러 발생 시 mut를 추가하라고 친절히 안내하는 점도 Rust의 특징입니다.

3. 타입 추론 및 명시적 타입 선언

Rust 컴파일러는 대부분의 경우 변수 선언 시 타입을 자동 추론합니다.

let x = 42;      // i32로 추론
let y = true; // bool로 추론
let z: u32 = 10; // 명시적 타입 선언

필요에 따라 타입을 명시할 수도 있지만, 대부분 타입 추론에 맡깁니다.

4. 변수 가리기 (Shadowing)

Rust의 독특한 특징인 변수 가리기는 같은 이름의 변수를 같은 스코프 내에서 다시 let으로 선언하여 새 변수를 만드는 것입니다. 이때 이전 변수는 새 변수에 의해 가려집니다.

fn main() {
let x = 5;
let x = x + 1; // 이전 x를 가리고 새 x 선언
{
let x = x * 2;
println!("내부 스코프의 x: {}", x); // 12
}
println!("외부 스코프의 x: {}", x); // 6
}

위 예제에서 첫 번째 x는 5, 두 번째는 6, 내부 스코프의 x는 12입니다. 이렇게 하면 가변 변수 없이도 값 변경 효과를 낼 수 있고 타입도 변경할 수 있습니다.

5. 스코프와 변수 유효 범위

변수는 선언된 블록 {} 내에서만 유효하며, 블록을 벗어나면 사라집니다.

fn main() {
let x = 1;
{
let y = 2;
println!("내부 스코프 y: {}", y); // 2
}
// println!("외부 스코프 y: {}", y); // 오류: y는 유효하지 않음
}

또한 내부 스코프에서 같은 이름으로 변수를 다시 선언하면 외부 변수를 가립니다.

6. 실습 예제 종합

fn main() {
// 불변 변수
let a = 10;
println!("a: {}", a);
// a = 20; // 컴파일 에러!

// 가변 변수
let mut b = 10;
println!("b 초기값: {}", b);
b = 20;
println!("b 변경 후: {}", b);

// 변수 가리기 (shadowing)
let c = 5;
let c = c + 10; // 이전 c 가리기, 이제 c = 15
println!("c: {}", c);

{
let c = c * 2; // 내부 스코프 가리기, c = 30
println!("내부 블록의 c: {}", c);
}

println!("외부 블록의 c: {}", c);

// 타입 변경이 가능한 shadowing
let d = "문자열";
println!("d는 문자열: {}", d);
let d = d.len(); // 같은 이름 변수에 정수 대입 (shadowing)
println!("d는 문자열 길이: {}", d);
}

출력 결과:

a: 10
b 초기값: 10
b 변경 후: 20
c: 15
내부 블록의 c: 30
외부 블록의 c: 15
d는 문자열: 문자열
d는 문자열 길이: 6

Ⅱ. 벡터(Vector)와 튜플(Tuple) 타입 변수 바인딩

1. 벡터(Vector) 타입 변수 바인딩

벡터는 같은 타입 요소들을 동적 크기로 저장하는 컬렉션입니다. Rust에서 벡터는 Vec<T> 타입으로 표현됩니다.

  • 벡터 바인딩 기본 예:
fn main() {
// 빈 vector 생성 (타입 명시 필요)
let mut v: Vec<i32> = Vec::new();
// 값 추가 가능하려면 mut 필요
v.push(1);
v.push(2);
println!("{:?}", v); // 출력: [1, 2]

// 초기값과 함께 벡터 생성 (타입 추론 가능)
let v2 = vec![10, 20, 30];
println!("{:?}", v2); // 출력: [10, 20, 30]
}
  • 벡터의 요소 접근:
let first = v2[0];       // 인덱스를 통한 접근 (주의: 범위 초과 시 패닉)
let maybe_first = v2.get(0); // Option 타입 반환 (안전 접근)
  • 가변 벡터 요소 수정도 mut으로 가능:
let mut v3 = vec![1, 2, 3];
v3[1] = 5; // 두 번째 요소를 5로 변경

벡터는 가변성을 가지고 가리키는 데이터가 동적으로 바뀔 수 있으므로 보통 mut 바인딩과 함께 선언합니다.

2. 3차원 벡터

1) 가변 크기: Vec<Vec<Vec<T>>>

// 2 x 3 x 4 크기의 0으로 채워진 3차원 벡터
let x = 2;
let y = 3;
let z = 4;
let mut v: Vec<Vec<Vec<i32>>> = vec![vec![vec![0; z]; y]; x];

// 읽기
let a = v[1][2][3];

// 쓰기
v[0][1][2] = 42;

헬퍼 함수를 하나 두면 더 편합니다:

fn new_3d<T: Clone>(x: usize, y: usize, z: usize, value: T) -> Vec<Vec<Vec<T>>> {
vec![vec![vec![value.clone(); z]; y]; x]
}

let mut v = new_3d(2, 3, 4, 0i32);

2) 고정 크기: 중첩 배열(스택 또는 Box로 힙에)

크기가 컴파일 타임에 고정되어 있다면 배열을 사용할 수 있습니다.

const X: usize = 2;
const Y: usize = 3;
const Z: usize = 4;

let mut v: [[[i32; Z]; Y]; X] = [[[0; Z]; Y]; X];

v[1][2][3] = 7;

크기가 커서 스택에 올리기 부담스럽다면 Box로 감싸 힙에 둘 수도있습니다.

let mut v: Box<[[[i32; Z]; Y]; X]> = Box::new([[[0; Z]; Y]; X]);

3) 평탄화(flat)한 1차원 Vec<T> + 인덱스 계산

성능/메모리 지역성을 위해 1차원 벡터에 직접 담고 인덱스를 수식으로 계산하는 패턴도 자주 씁니다.

struct Vec3D<T> {
data: Vec<T>,
x: usize,
y: usize,
z: usize,
}

impl<T: Clone> Vec3D<T> {
fn new(x: usize, y: usize, z: usize, value: T) -> Self {
Self {
data: vec![value; x * y * z],
x, y, z,
}
}

#[inline]
fn idx(&self, i: usize, j: usize, k: usize) -> usize {
// (i, j, k) -> linear index
i * self.y * self.z + j * self.z + k
}

fn get(&self, i: usize, j: usize, k: usize) -> &T {
&self.data[self.idx(i, j, k)]
}

fn get_mut(&mut self, i: usize, j: usize, k: usize) -> &mut T {
let idx = self.idx(i, j, k);
&mut self.data[idx]
}
}

let mut v = Vec3D::new(2, 3, 4, 0i32);
*v.get_mut(1, 2, 3) = 5;

2. 튜플(Tuple) 타입 변수 바인딩

튜플은 여러 개의 서로 다른 타입 값을 하나로 묶는 복합 타입입니다. 고정 길이이며 각각 요소의 타입은 다를 수 있습니다.

  • 튜플 선언과 바인딩:
let tup: (i32, f64, char) = (500, 6.4, 'z');
  • 튜플의 요소에 접근하려면 패턴 매칭 또는 점(.) 표기법 사용:
let (x, y, z) = tup;  // 구조 분해 (destructuring)
println!("x: {}, y: {}, z: {}", x, y, z);

let five_hundred = tup.0;
let six_point_four = tup.1;
let z_char = tup.2;

튜플은 기본적으로 불변이며, 가변 선언 시 요소 변경 가능:

let mut tup2 = (1, 2);
tup2.0 = 5; // 첫 번째 요소 변경 가능

4. 스코프와 바인딩의 변수 가리기

벡터, 튜플 등도 기본 자료형과 마찬가지로 스코프 내에서 변수 가리기(shadowing)가 가능합니다:

let v = vec![1];
let v = vec![2, 3]; // 이전 v 가려짐
println!("{:?}", v); // [2, 3]

Ⅲ. 배열

1. 1차원 배열 정의 방법

let arr = [1, 2, 3];  // 길이 3의 1차원 배열

2. 2차원 배열

let arr = [[1, 2, 3], [4, 5, 6]];  // 2x3 배열

3. 3차원 배열

let arr = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
];

Ⅳ. 해시맵

Rust에서 해시맵(HashMap)의 변수 바인딩도 기본적으로 let 키워드를 사용해 이름을 해시맵 값에 바인딩하는 방식으로 이루어집니다. 즉, 해시맵도 다른 변수처럼 데이터 구조 전체를 하나의 변수 이름에 연결하는 행위입니다.

  • 해시맵을 선언할 때는 use std::collections::HashMap;를 임포트하고,
  • 보통 HashMap::new()를 호출하여 빈 해시맵을 만들고,
  • let mut를 붙여 가변 바인딩을 해야 해시맵에 키-값 쌍을 추가하거나 수정할 수 있습니다.

예를 들어:

use std::collections::HashMap;

fn main() {
// 가변 해시맵 바인딩
let mut scores = HashMap::new();

// 값 삽입
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// 값 접근
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

println!("Blue 팀 점수: {}", score);

// 해시맵의 모든 키-값 쌍 출력
for (key, value) in &scores {
println!("{key}: {value}");
}
}
  • 여기서 scores 변수에 해시맵 데이터 구조가 바인딩되었고,
  • mut 키워드 때문에 값 삽입과 변경이 가능합니다.
  • .insert() 메서드로 키-값 쌍을 추가하거나 갱신하며,
  • .get() 메서드로 해당 키에 연관된 값을 읽을 수 있습니다.

요약하면, 해시맵도 배열, 벡터, 튜플, 구조체와 같이 Rust에서 변수 바인딩 개념이 동일하게 적용됩니다. let으로 이름을 데이터에 바인딩하고, 변경하려면 mut가 필수입니다. 해시맵은 키-값 쌍 컬렉션이라는 점에서 특수하지만, 바인딩이라는 개념 면에서는 일반 변수와 차이가 없습니다.

추가적으로 해시맵 내부에 구조체 같은 복합 데이터 타입을 저장해도, 변수 바인딩과 관련된 기본 원칙은 같습니다.

Ⅴ. 사용자 정의 타입

Rust에서 사용자 정의 타입(예: 구조체와 열거형)에 대한 변수 바인딩은 기본 타입이나 벡터, 튜플 등과 동일하게 let 키워드를 사용하여 값을 변수 이름에 연결하는 것을 의미합니다. 하지만 구조체와 열거형은 내부 필드나 variant에 데이터를 포함할 수 있어서, 변수 바인딩과 활용이 더 다양한 형태로 나타납니다.

아래에서 구조체와 열거형을 사용자 정의 타입의 변수 바인딩 예제로 구체적으로 설명합니다.

1. 구조체(Struct) 변수 바인딩 예제

// 구조체 정의
struct Point {
x: i32,
y: i32,
}

fn main() {
// 구조체 인스턴스를 생성하고 변수 p에 바인딩
let p = Point { x: 10, y: 20 };

// 구조체 필드에 접근
println!("x: {}, y: {}", p.x, p.y);

// 가변 바인딩 시 필드 값 변경 가능
let mut p_mut = Point { x: 5, y: 5 };
p_mut.x = 15;
println!("변경된 x: {}", p_mut.x);

// 구조체를 분해하여 필드 값을 각각 변수에 바인딩
let Point { x: a, y: b } = p;
println!("분해된 x: {}, y: {}", a, b);
}
  • let p = Point { … };에서 p는 Point 타입 값에 변수 바인딩입니다.
  • 패턴 매칭처럼 let Point { x: a, y: b } = p; 구문으로 구조체 필드를 변수 a, b에 바인딩할 수도 있습니다.
  • mut 키워드로 가변 바인딩을 선언하면 구조체 필드를 수정할 수 있습니다.

2. 열거형(Enum) 변수 바인딩 예제

// 열거형 정의
enum Message {
Quit, // 데이터 없는 variant
Move { x: i32, y: i32 }, // 필드가 있는 variant (struct-like)
Write(String), // 튜플 형태 variant
ChangeColor(i32, i32, i32), // 여러 필드를 가진 튜플 variant
}

fn main() {
// enum 값에 변수 바인딩
let msg = Message::Move { x: 10, y: 20 };

// match로 variant별 데이터 바인딩과 처리
match msg {
Message::Quit => println!("Quit variant"),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to {}, {}, {}", r, g, b),
}
}
  • let msg = Message::Move { x: 10, y: 20 };에서 msg가 Message enum의 Move variant 값에 바인딩되었습니다.
  • match 문에서는 각 variant의 내부 데이터를 별도의 변수(x, y, text, r, g, b)에 패턴 매칭을 사용한 변수 바인딩으로 추출합니다.
  • 열거형 값 하나가 여러 variant 중 하나로만 존재할 수 있기에, match를 통해안전하게 처리합니다.

요약

구분변수 바인딩 방식설명
기본 타입let x = 5;변수 이름이 단순 값과 바인딩됨
구조체let p = Point { … };변수 이름이 구조체 인스턴스에 바인딩
let Point { x: a, y: b } = p;구조체 필드를 변수에 분해하여 바인딩
열거형let msg = Message::Write(“Hi”.to_string());변수에 enum variant 값 바인딩
match msg { Message::Write(text) => { … } }match 내 패턴 매칭으로 variant 내 데이터에 변수 바인딩

이처럼 사용자 정의 타입에서도 변수 바인딩은 let을 사용해 값을 변수 이름에 연결하는 동일한 개념이며, 구조체는 필드별 분해 바인딩이 가능하고, 열거형은 variant별 패턴 매칭을 통해 내부 데이터를 변수에 바인딩하는 방식으로 동작합니다.

해쉬맵 (HashMap)

HashMap은 컬렉션의 일종으로, HashMap<K, V> 타입은 K 타입의 키에 V 타입의 값을 매핑한 것을 저장합니다. 해쉬맵은 벡터를 이용하듯 인덱스를 이용하는 것이 아니라 임의의 타입으로 된 키를 이용하여 데이터를 찾기를 원할때 유용합니다.


1. Struct와 HashMap

항목StructHashMap
정의 방식컴파일 타임에 고정된 필드와 타입 정의런타임에 키-값 쌍으로 유동적으로 데이터 저장
고정된 필드 이름임의의 키 (보통 String 등 Eq + Hash 트레잇 필요)
타입 안정성필드 타입이 컴파일 시점에 결정됨모든 값이 동일 타입이거나 제네릭으로 지정됨
사용 목적고정된 구조의 데이터 표현동적으로 키/값 데이터를 저장하고 조회

※ 제네릭(Generic) : 어떤 자료형이든 사용할 수 있도록 유연하고 재사용 가능한 코드를 만드는 기능

2. HashMap 생성과 삽입

HashMap을 사용하려면 먼저 std::collections에서 가져와야 합니다.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Blue", 10);
    scores.insert("Red", 50);
}

HashMap::new()로 HashMap을 생성합니다.

let mut scores = HashMap::new();
는 가변형 scores란 변수에 빈 HashMap을 대입하는 것입니다. 다시 말하면 빈 HashMap을 가변형 scroes란 이름으로 만드는 것입니다.

insert(key, value)로 값을 추가할 수 있습니다.

scores.insert(“Blue”, 10);
는 scores는 HashMap의 키로 Blue(&str type), 값으로 10을 대입하는 것이고,

scores.insert(“Red”, 50);
는 scores는 HashMap의 키로 Red(&str type), 값으로 50을 대입하는 것입니다.

이때 HashMap은 키와 값의 타입을 모두 제네릭으로 받으며, 보통 자동 추론됩니다.
Blue와 Red는 &str 형식이고, 10과 50은 정수이므로 i32로 해쉬맵의 형식이 추론된 것입니다.

“Blue”와 “Red”를 String::from(“Blue”)와 String::from(“Red”)로 수정하면, Key의 type이 String으로 자동 변경됩니다.

첫번째는 String type이고, 두번째는 &str 타입이면 두번째 key 아래에 빨간색 물결 줄이 표시되는데, 커서를 갖다대면 mismatched types 에러가 표시됩니다.


3. 값 접근 및 소유권

값을 읽을 때는 get() 메서드를 사용합니다.

let team_name = "Blue";
let score = scores.get(team_name);
println!("{:?}", score); // Some(10)

get()은 Option<&V>를 반환하므로, Some(v) 또는 None으로 패턴 매칭하거나 unwrap_or 등을 활용해야 합니다.

    match score {
        Some(score) => println!("{team_name} 팀의 점수: {score}"),
        None => println!("{team_name} 팀의 점수를 찾을 수 없습니다."),
    }

println!(“{:?}”, score); 의 결과는 Some(10)인데, 위와 같이 match 흐름 제어 연산자를 사용하면 점수가 있으면 점수, 여기서는 10이 반환되고,

팀 이름을 이름이 없는 Green으로 변경하면 Option 값은 None이 출력되고, match 연산자의 결과는 “Green 팀의 점수를 찾을 수 없습니다.”란 메시지가 나오게 됩니다.

소유권 이슈

Rust의 HashMap은 키와 값의 소유권을 가져갑니다.

let field = String::from("Favorite color");
let value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field, value);
// println!("{}", field); // 오류! 소유권이 이동됨

따라서, 위 코드에서 println! 앞의 주석을 제거하고 실행하면
value borrowed here after move 에러가 발생합니다.

위 오류를 없애려면 map.insert안 의 field를 &field로 수정하면 됩니다.


4. for 루프로 순회하기

HashMap의 모든 값을 순회하려면 for 루프를 사용합니다.

for (key, value) in &scores {
    println!("{}: {}", key, value);
}

참조로 순회하므로 키나 값을 수정하진 않지만, 읽을 수 있습니다.


5. 조건부 삽입: entry()와 or_insert()

entry()는 키의 존재여부에 따라 다른 동작을 하게 해주는 API입니다. entry 함수의 리턴값은 열거형 Entry인데, 해당 키가 있는지 혹은 없는지를 나타냅니다.

scores.entry("Yellow").or_insert(30);
scores.entry("Blue").or_insert(50);
  • “Yellow”라는 key가 없기 때문에 value 30이 추가되고,
  • “Blue”는 이미 존재하므로 기존 값(10)이 유지됩니다.

or_insert()는 존재하지 않을 때만 값을 삽입하는 매우 유용한 메소드입니다.


6. 값을 업데이트하기

HashMap의 값을 직접 변경하려면 get_mut() 또는 entry()를 활용할 수 있습니다.

기존 값 수정 (1) :

if let Some(score) = scores.get_mut("Red") {
    *score += 10;
}

if let Some(score) = scores.get_mut(“Red”) {
: score가 Red의 score라면 score를 가변 참조로 반환하는데,

*score += 10;
: 참조를 반환하므로 값을 수정하기 위해서는 dereference(역참조, 포인터가 가리키는 메모리 주소에 저장된 값을 읽거나 쓰는 것)하는 연산자 *를 앞에 붙여야 하며, 이 구문은 score에 10을 더하는 것입니다.

기존 값 수정 (2) :

또는 entry() 기반으로도 가능:

scores.entry("Red").and_modify(|v| *v += 1);

Red라는 키가 있으면, value를 v로 받아서, 1을 더하는 것입니다.


7. 예제: 단어 개수 세기

HashMap은 문자열 데이터를 처리할 때 특히 유용합니다. 예를 들어, 문장에서 각 단어의 개수를 세어보겠습니다.

이 코드는 split_whitespace()로 공백 기준으로 단어를 나누고, 각 단어가 등장한 횟수를 계산합니다.

use std::collections::HashMap;

fn main() {
    let text = "hello world hello rust";

    let mut counts = HashMap::new();

    for word in text.split_whitespace() {
        *counts.entry(word).or_insert(0) += 1;
    }

    println!("{:?}", counts);
}

*counts.entry(word).or_insert(0) += 1;
: word란 키가 있으면 value에 1을 더하고, 없으면 value에 0을 추가하는 것입니다.

출력 결과는 아래와 같은데
{“hello”: 2, “world”: 1, “rust”: 1}

key 순으로 정렬하지 않아 표시되는 순서가 매번 달라집니다.

9. HashMap 요약

  • HashMap는 키-값 쌍을 저장하는 자료구조
  • insert()로 값 추가, get()으로 값 조회
  • 키는 고유해야 하며, 같은 키로 다시 삽입하면 덮어씀
  • entry()와 or_insert()로 조건부 삽입 가능
  • 순회는 for (k, v) in &map
  • 소유권은 HashMap으로 이동됨
  • 문자열 처리에 매우 유용함