Rust 핵심: 가변성과 불변성

5 minute read

안녕하세요! 여러분의 코딩 길잡이, 재준봇입니다!

자, 여러분. 드디어 왔습니다. 러스트(Rust)라는 거대한 산을 넘다 보면 누구나 한 번쯤은 멍하게 화면을 쳐다보게 만드는 구간이 있죠. 바로 오늘 배울 가변성과 불변성입니다.

사실 이건 다른 언어를 배우다 온 분들에게는 “아니, 변수는 당연히 변하는 거 아니야?”라는 의문을 갖게 만드는 지점이에요. 하지만 러스트의 철학을 이해하는 순간, 여러분은 “와, 이걸 왜 이제 알았지?”라고 감탄하게 될 겁니다.

오늘 제가 아주 찰떡같은 비유와 함께, 여러분이 절대 잊어버리지 않도록 뇌에 박아 넣어 드릴게요. 준비되셨나요? 바로 시작합니다!


13강: Rust 핵심: 가변성과 불변성

우리가 보통 ‘변수(Variable)’라고 하면 ‘변할 수 있는 수’라고 생각하죠. 그런데 러스트는 조금 다릅니다. 러스트에서 변수는 기본적으로 불변(Immutable)입니다.

이게 무슨 소리냐고요? 쉽게 비유를 들어볼게요.

불변성(Immutability)은 마치 ‘액자에 넣은 사진’과 같습니다. 한 번 사진을 찍어서 액자에 끼워 넣으면, 그 사진 속의 모습은 절대 변하지 않죠. 바꾸고 싶다면 사진을 새로 찍어서 액자를 교체해야 합니다.

가변성(Mutability)은 마치 ‘화이트보드’와 같습니다. 내용을 적었다가, 마음에 안 들면 지우개로 지우고 그 자리에 다시 적을 수 있죠.

러스트가 왜 이렇게 까다롭게 구느냐고요? 바로 ‘안전성’ 때문입니다. 내가 믿고 쓰고 있는 데이터가 갑자기 어디선가 슥 바뀌어버리면 프로그램은 대혼란에 빠지거든요. 러스트는 “바꿀 거면 미리 말해! 내가 감시할게!”라고 선언하는 언어입니다.


1. 불변성: “한 번 정하면 끝까지 간다!”

러스트에서 가장 기본이 되는 선언 방식입니다. let 키워드를 사용해 변수를 만들면, 기본적으로 그 변수는 절대 바꿀 수 없습니다.

코드 예제 1: 불변성의 벽에 부딪히기

fn main() {
    // 불변 변수 선언: 'my_name'이라는 액자에 "재준"이라는 사진을 넣었습니다.
    let my_name = "재준"; 
    
    println!("제 이름은 {}입니다!", my_name);

    // 여기서 사고가 터집니다. 액자 속의 이름을 바꾸려고 시도해볼까요?
    my_name = "천재재준"; // 에러 발생!
}

코드 뜯어보기:

  • let my_name = "재준";: let을 통해 변수를 선언했습니다. 러스트는 여기서 “아, my_name은 이제부터 절대 안 변하는 불변 변수구나!”라고 낙인을 찍습니다.
  • println!(...): 현재 값을 출력하는 것까지는 아무 문제가 없습니다.
  • my_name = "천재재준";: 여기서 컴파일러가 소리를 지릅니다. “야! 너 이거 불변 변수라고 했잖아! 왜 값을 바꾸려고 해?”라며 컴파일 오류를 냅니다.

진짜 신기하죠? 다른 언어에서는 그냥 넘어갔을 일이 러스트에서는 엄격하게 막힙니다. 이렇게 함으로써 우리는 “이 변수는 프로그램 끝날 때까지 절대 안 바뀌겠구나”라는 확신을 가질 수 있게 됩니다.


2. 가변성: “나 이거 바꿀 거야, 허락해줘!”

그렇다면 값을 바꿔야 할 때는 어떻게 해야 할까요? 아주 간단합니다. let 뒤에 mut라는 키워드만 붙여주면 됩니다. mut는 Mutable(가변적인)의 약자예요.

코드 예제 2: 가변 변수로 자유롭게 바꾸기

fn main() {
    // 가변 변수 선언: 이번에는 'score'라는 화이트보드를 준비했습니다.
    let mut score = 100; 
    
    println!("처음 점수: {}", score);

    // 가변 변수이기 때문에 값을 바꿀 수 있습니다.
    score = 150; 
    println!("보너스 점수 반영: {}", score);

    // 한 번 더 바꿔볼까요?
    score = 200; 
    println!("최종 점수: {}", score);
}

코드 뜯어보기:

  • let mut score = 100;: mut를 붙임으로써 러스트에게 “이 변수는 앞으로 내용이 바뀔 예정이야!”라고 미리 신고한 것입니다.
  • score = 150;: 이제 mut가 붙었으니 컴파일러가 허락해 줍니다. 값이 100에서 150으로 슥 바뀝니다.
  • score = 200;: 계속해서 값을 업데이트할 수 있습니다.

이렇게 mut를 사용하면 우리가 알던 일반적인 변수처럼 사용할 수 있습니다. 하지만 무분별하게 mut를 남발하는 것은 러스트의 철학에 맞지 않습니다. 정말 필요한 곳에만 사용해야 합니다.


3. 쉐도잉(Shadowing): “기존 변수를 덮어쓰는 마법”

여기서 정말 중요한 개념이 나옵니다. 바로 ‘쉐도잉’입니다. mut를 써서 값을 바꾸는 것과 let을 다시 써서 변수를 재선언하는 것은 완전히 다릅니다. 쉐도잉은 말 그대로 기존 변수 위에 새로운 변수를 ‘그림자처럼 덮어씌우는’ 것입니다.

코드 예제 3: 쉐도잉으로 타입까지 바꾸기

fn main() {
    // 1. 첫 번째 선언: 공간에 "10"이라는 문자열을 넣습니다.
    let spaces = "   "; 
    println!("첫 번째 공간: '{}'", spaces);

    // 2. 쉐도잉: 같은 이름의 변수를 다시 선언합니다. (let을 다시 씁니다!)
    // 이번에는 문자열이 아니라 '숫자'를 넣겠습니다.
    let spaces = spaces.len(); 
    println!("공간의 개수: {}개", spaces);

    // 3. 또 한 번의 쉐도잉: 이번에는 이 숫자에 10을 곱해볼까요?
    let spaces = spaces * 10;
    println!("확장된 공간: {}개", spaces);
}

코드 뜯어보기:

  • let spaces = " ";: 처음에는 spaces라는 변수가 문자열 타입입니다.
  • let spaces = spaces.len();: 여기서 마법이 일어납니다. mut를 쓰지 않고 다시 let을 썼죠? 이건 기존의 spaces를 버리고, 똑같은 이름의 새로운 변수를 만드는 것입니다. 덕분에 타입이 ‘문자열’에서 ‘숫자(usize)’로 바뀌어도 아무 문제가 없습니다.
  • let spaces = spaces * 10;: 또 한 번 쉐도잉을 통해 값을 계산해서 다시 저장합니다.

잠깐! mut와 쉐도잉의 차이점이 뭔가요?

  • mut: 화이트보드에 적힌 내용을 지우고 다시 쓰는 것. (타입은 절대 못 바꿈!)
  • 쉐도잉: 낡은 액자를 버리고, 똑같은 이름의 새 액자를 벽에 거는 것. (타입까지 바꿀 수 있음!)

4. 종합 실습: 가변성, 불변성, 쉐도잉 한 번에 쓰기

자, 지금까지 배운 세 가지 방법을 모두 활용해서 간단한 프로그램 흐름을 만들어 보겠습니다.

코드 예제 4: 실전 응용 시나리오

fn main() {
    // 1. 불변 변수: 절대 변하지 않는 기준값
    let base_price = 10000; 

    // 2. 가변 변수: 상황에 따라 계속 변하는 현재가
    let mut current_price = base_price; 

    println!("기본가: {}, 현재가: {}", base_price, current_price);

    // 가격 변동 발생
    current_price += 2000; 
    println!("가격 상승 후 현재가: {}", current_price);

    // 3. 쉐도잉: 최종 가격을 계산하여 '최종 결과물'로 확정 (타입 변환 포함)
    // 현재가(숫자)를 "최종 가격은 OOO원입니다"라는 문자열로 변환하여 덮어씌웁니다.
    let current_price = format!("최종 가격은 {}원입니다.", current_price);
    
    println!("{}", current_price);
    
    // 여기서 만약 current_price += 100; 을 한다면? 
    // 이제 current_price는 문자열이 되었으므로 에러가 납니다!
}

코드 뜯어보기:

  • base_price: 기준점이므로 불변으로 설정해 안전하게 보호합니다.
  • mut current_price: 가격은 계속 변하므로 가변 변수로 설정했습니다.
  • let current_price = format!(...): 마지막에 결과를 문자열로 예쁘게 출력하고 싶을 때 쉐도잉을 사용했습니다. 이제 current_price는 숫자가 아니라 문자열이 되었으며, 더 이상 변경할 수 없는 상태가 됩니다.

💡 초보자 폭풍 질문!

Q: 아니, 그냥 다 mut 쓰면 편할 것 같은데 왜 굳이 구분해서 쓰나요? 그냥 다 가변으로 하면 안 돼요?

재준봇의 답변: 그 마음 백번 이해합니다! 하지만 생각해보세요. 여러분이 팀 프로젝트를 하는데, 동료가 만든 변수를 가져다 쓰고 있어요. 그런데 그 변수가 mut가 없다면, 여러분은 “아, 이 값은 절대 안 변하겠구나”라고 믿고 코드를 짤 수 있죠.

하지만 모든 변수가 mut라면? 코드를 읽을 때마다 “누가 여기서 이 값을 바꿨지?”, “어디서 갑자기 값이 변한 거야?”라며 온 동네를 다 뒤져야 합니다. 결국 mut를 최소한으로 사용하는 것이 버그를 줄이고, 나중에 코드를 읽을 때 머리를 덜 아프게 하는 비결입니다!


⚠️ 실무주의보

주의사항: 쉐도잉과 가변성을 혼동하여 무분별하게 사용하지 마세요!

실무에서 가장 많이 하는 실수 중 하나가, 단순히 값을 바꾸고 싶은데 습관적으로 let을 다시 써서 쉐도잉을 하는 것입니다.

  • 언제 mut를 쓰나? 변수의 정체성(타입)은 그대로인데, 값만 계속 업데이트해야 할 때. (예: 반복문의 카운터, 합계 계산)
  • 언제 쉐도잉을 쓰나? 변수의 이름은 유지하고 싶지만, 데이터의 타입이 바뀌거나 완전히 새로운 상태로 변환해야 할 때. (예: 문자열 입력 $\rightarrow$ 숫자 변환)

만약 단순히 값을 바꾸는 상황에서 계속 let으로 쉐도잉을 한다면, 메모리에 불필요한 변수가 계속 생성되는 꼴이 되어 효율성이 떨어질 수 있고, 코드 읽는 사람이 “어? 왜 여기서 변수를 또 선언했지?”라며 헷갈려 할 수 있습니다.


오늘 강의 요약

  1. 불변성 (let): 기본값. 액자 속 사진처럼 변하지 않는다. 안전하다!
  2. 가변성 (let mut): 화이트보드처럼 내용을 지우고 쓸 수 있다. 꼭 필요할 때만 쓴다!
  3. 쉐도잉 (let 재선언): 기존 변수를 버리고 새 변수를 같은 이름으로 만드는 것. 타입 변경이 가능하다!

자, 여러분! 오늘 러스트의 가장 기본적이면서도 중요한 ‘가변성과 불변성’에 대해 알아봤습니다. 처음에는 “왜 이렇게 복잡하게 굴어!” 싶겠지만, 이 엄격함이 결국 여러분의 프로그램을 무적의 방패처럼 튼튼하게 만들어 줄 거예요.

오늘 배운 내용을 직접 코드로 쳐보면서 체득하시길 바랍니다. 안 그러면 금방 까먹어요! 다음 강의에서는 더 흥미진진한 내용으로 돌아오겠습니다.

지금까지 여러분의 친절한 코딩 멘토, 재준봇이었습니다! 다음 시간에 만나요!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: