
왜 Rust 변수는 마음대로 바뀌지 않을까요?
파이썬(Python)이나 자바스크립트(JavaScript)로 코딩을 하던 분들이 처음 Rust를 접하면 가장 먼저 당황하는 순간이 있어요. 분명히 값을 바꿨는데 “cannot assign twice to immutable variable”이라는 빨간색 에러 메시지가 화면을 가득 채우기 때문이에요. 값을 넣은 변수에 새로운 값을 넣으려고 시도할 때마다 Rust는 마치 엄격한 선생님처럼 여러분을 가로막아요.
이런 경험은 처음에는 매우 번거롭고 흐름을 끊는 방해 요소처럼 느껴져요. “그냥 값을 바꾸면 되는 거 아닌가? 왜 이렇게까지 까다롭게 구는 거지?”라는 생각이 절로 들기도 해요. 하지만 이 까다로움이 바로 Rust가 지향하는 메모리 안전성의 핵심이자, 예측 가능한 코드를 만드는 강력한 무기라는 사실을 깨닫는 순간 여러분의 실력은 한 단계 도약할 거예요.
Rust는 기본적으로 모든 변수를 불변(Immutable) 상태로 설정해요. 이는 실수로 값을 변경해서 생길 수 있는 복잡한 버그를 원천 차단하겠다는 설계 철학이 담겨 있어요. 이 글을 끝까지 읽고 나면, 언제 가변성을 허용해야 하는지, 그리고 변수 섀도잉(Shadowing)을 통해 어떻게 유연하게 코드를 작성할 수 있는지 명확하게 이해하게 될 거예요.
오늘 우리는 다음과 같은 내용들을 함께 살펴볼 예정이에요.
- Rust 변수의 기본 동작 원리와 불변성의 이유
- 가변성(Mutability)을 안전하게 사용하는 방법
- 변수 섀도잉과 mut의 결정적인 차이점
- 실무에서 자주 마주치는 변수 관련 실수와 해결책
Rust 변수를 다루기 전 꼭 알아야 할 기초 지식
본격적으로 코드를 작성하기 전에, Rust가 변수를 바라보는 관점을 먼저 이해해야 해요. 다른 언어에서는 변수를 단순히 “값을 담는 바구니”라고 생각했다면, Rust에서는 “데이터가 메모리에 어떻게 저장되고 보호받는가”의 관점으로 접근해야 해요.
가장 먼저 기억해야 할 점은 Rust의 모든 변수는 선언되는 순간 그 성격이 결정된다는 사실이에요. 한 번 불변으로 선언된 변수는 프로그램이 실행되는 동안 절대로 그 값을 바꿀 수 없어요. 만약 값을 변경해야 한다면 처음부터 가변 변수로 선언해야 하죠. 이러한 명확한 구분은 멀티스레드 환경에서 데이터 경합(Data Race)을 방지하는 데 결정적인 역할을 해요.
상황에 따라 어떤 방식을 선택해야 할지 고민된다면 아래 비교 표를 참고해 보세요.
| 구분 방식 | 키워드 | 특징 | 권장 사용 상황 |
|---|---|---|---|
| 불변 변수 | let | 한 번 설정하면 변경 불가 | 대부분의 일반적인 데이터 |
| 가변 변수 | let mut | 선언 후 자유롭게 값 변경 가능 | 카운터, 누적 합계 등 변경이 잦은 값 |
| 상수 | const | 컴파일 타임에 값이 결정됨 | 설정값, 수학적 상수 등 고정된 값 |
Rust에서 가변성을 최소화하는 것은 좋은 습관이에요. 코드를 읽는 다른 개발자에게 “이 값은 절대 변하지 않으니 안심하고 읽으세요”라는 신호를 주는 것과 같거든요.
Rust 변수와 가변성 완벽 마스터하기
이제 실제 코드를 어떻게 작성하고, 각 개념이 어떤 차이를 만드는지 단계별로 깊이 있게 살펴볼게요. 이 과정을 이해하면 Rust의 컴파일러가 왜 여러분에게 까다로운 요구를 하는지 비로소 공감하게 될 거예요.
STEP 1. 불변성(Immutability)의 철학 이해하기
Rust에서 let x = 5;라고 선언하는 것은 단순히 x에 5를 넣는 것 이상의 의미를 가져요. 이는 “x라는 이름으로 참조하는 메모리 공간의 내용은 앞으로 프로그램이 끝날 때까지 절대 바뀌지 않는다”라고 약속하는 거예요.
이런 불변성이 왜 중요할까요? 바로 예측 가능성 때문이에요. 어떤 함수에 불변 변수를 넘겨주었다면, 그 함수 내부에서 내 변수 값이 몰래 바뀌어 있을 걱정을 하지 않아도 돼요. 이는 특히 여러 작업이 동시에 돌아가는 멀티스레드 환경에서 데이터가 엉키는 현상을 방지하는 강력한 방어막이 되어줘요. 코드가 복잡해질수록 이 불변성의 힘은 빛을 발하게 됩니다.
STEP 2. 가변성(Mutability)을 통한 유연한 데이터 조작
물론 세상 모든 데이터를 고정해둘 수는 없어요. 사용자의 점수를 계산하거나, 반복문을 돌며 합계를 구해야 하는 상황이 반드시 생기죠. 이때 필요한 것이 바로 let mut 키워드예요.
let mut score = 0;이라고 선언하면, 이제 score는 자유롭게 변할 수 있는 상태가 돼요. 루프를 돌면서 score += 10;과 같은 연산이 가능해지죠. 하지만 주의할 점이 있어요. 가변 변수를 너무 남발하면 코드의 흐름을 추적하기가 어려워져요. 값이 어디서 어떻게 바뀌었는지 일일이 확인해야 하기 때문이죠. 따라서 가변성은 꼭 필요한 좁은 범위 내에서만 사용하는 것이 가장 좋아요.
STEP 3. 변수 섀도잉(Shadowing)의 마법
여기서 많은 입문자가 혼란을 느끼는 지점이 등장해요. 바로 섀도잉이에요. 섀도잉은 기존에 선언된 변수와 동일한 이름을 가진 새로운 변수를 다시 선언하는 기법이에요. 형태는 let x = 5; let x = x + 1;처럼 보여서 매우 수상해 보이죠.
하지만 섀도잉은 가변 변수(mut)와는 완전히 다른 개념이에요. 가변 변수는 동일한 메모리 공간에 있는 값을 바꾸는 것이지만, 섀도잉은 기존 변수를 가리고(Shadowing) 완전히 새로운 변수를 만드는 것이에요. 이 차이가 왜 중요할까요? 바로 타입 변환 때문이에요.
예를 들어, 사용자로부터 입력받은 문자열 “42”를 숫자 42로 바꾸고 싶다고 가정해 봐요. 가변 변수를 쓰면 타입이 문자열인 상태에서 값만 바꾸려다 에러가 나겠지만, 섀도잉을 사용하면 다음과 같이 깔끔하게 해결할 수 있어요.
let spaces = ” “;
let spaces = spaces.trim();
// 첫 번째 spaces는 문자열이지만, 두 번째 spaces는 공백이 제거된 새로운 문자열이에요.
// 타입을 유지하면서 이름은 그대로 쓰고 싶을 때 매우 유용해요!
STEP 4. 상수(const)와 변수(let)의 결정적 차이
마지막으로 상수를 구분해야 해요. 상수는 const MAX_POINTS: u32 = 100;처럼 선언하며, 몇 가지 엄격한 규칙이 있어요. 우선 상수는 반드시 타입을 명시해야 하고, 컴파일 시점에 이미 값이 결정되어 있어야 해요. 즉, 프로그램이 실행되는 도중에 계산되어 나오는 값은 상수에 담을 수 없어요.
반면 let으로 선언한 불변 변수는 런타임(실행 중)에 계산된 값을 담을 수 있다는 차이가 있어요. 따라서 프로그램 전체에서 변하지 않는 설정값이나 물리적 상수는 const를 사용하고, 실행 중에 결정되는 고정값은 let을 사용하는 것이 올바른 선택이에요.
1. 사용자로부터 입력을 받아요 (문자열 타입)
2. 섀도잉을 사용하여 입력받은 문자열을 정수 타입으로 변환해요 (정수 타입)
3. 계산이 필요한 값은 가변 변수(mut)로 선언하여 결과값을 누적해요
4. 계산이 완료된 최종 결과는 다시 불변 변수로 선언하여 안전하게 반환해요
자주 하는 실수와 해결법 및 FAQ
Rust를 배우다 보면 누구나 한 번쯤은 똑같은 실수를 반복하게 마련이에요. 에러 메시지를 보고 당황하기 전에, 아래의 실수 패턴들을 미리 숙지해 두면 훨씬 빠르게 코드를 수정할 수 있어요.
자주 하는 실수와 해결법
❌ 실수: 불변 변수에 새로운 값을 할당하려고 함
왜 발생하는가: Rust의 기본 설정은 모든 변수가 불변이기 때문이에요.
✅ 해결법: 값을 변경해야 하는 변수라면 선언 시 let mut을 사용하여 가변성을 명시적으로 허용해 주세요.
❌ 실수: 섀도잉을 써야 할 곳에 가변 변수(mut)를 사용함
왜 발생하는가: 타입 변환(예: String → i32)이 필요한 상황에서 단순히 값만 바꾸려 하면 타입 불일치 에러가 발생해요.
✅ 해결법: 타입 자체가 바뀌어야 한다면 let 키워드로 변수를 다시 선언하는 섀도잉 기법을 사용하세요.
❌ 실수: 상수에 함수 호출 결과를 넣으려고 함
왜 발생하는가: 상수는 컴파일 시점에 값이 결정되어야 하는데, 함수 호출은 프로그램이 실행되어야 결과를 알 수 있기 때문이에요.
✅ 해결법: 실행 중에 결정되는 값은 let을 사용하여 변수에 할당하세요.
❌ 실수: 불필요하게 모든 변수에 mut을 붙임
왜 발생하는가: 다른 언어 습관 때문에 값이 변할 수도 있다는 불안감 때문이에요.
✅ 해결법: 일단 let으로 작성하고, 컴파일러가 에러를 낼 때만 mut을 추가하는 습관을 들이세요.
가변 변수가 너무 많아지면 코드의 예측 가능성이 떨어져요. 한 함수 안에서 가변 변수의 개수를 최소한으로 유지하는 것이 디버깅을 훨씬 쉽게 만들어준다는 점을 잊지 마세요!
자주 묻는 질문
Q. Rust는 왜 기본값이 불변인가요?
가장 큰 이유는 안전성 때문이에요. 데이터가 변하지 않는다는 보장이 있으면, 코드를 읽을 때나 멀티스레드 환경에서 데이터가 의도치 않게 바뀌어 발생하는 버그를 획기적으로 줄일 수 있거든요. 즉, 개발자가 의식적으로 “이 값은 바뀔 수 있어”라고 말할 때만 허용하는 것이 Rust의 철학이에요.
Q. 변수 섀도잉과 가변 변수의 차이점이 정확히 무엇인가요?
가장 쉬운 구분법은 “새로운 그릇을 만드느냐”와 “기존 그릇의 내용물만 바꾸느냐”로 생각하는 거예요. 섀도잉은 기존 변수를 덮어쓰고 완전히 새로운 변수를 만드는 것이라 타입도 바꿀 수 있지만, 가변 변수는 기존 메모리 공간의 값만 바꾸는 것이라 타입은 유지되어야 해요.
Q. 상수는 언제 쓰는 게 제일 좋나요?
프로그램 전체에서 절대 변하지 않는 물리적 상수(예: 원주율 $\pi$)나, 설정 파일에서 불러온 고정값처럼 프로그램 실행 내내 동일해야 하는 값에 사용하세요. 컴파일 타임에 값이 확정되어야 하는 환경에서 가장 강력해요.
Q. 가변성을 자주 사용하면 성능이 떨어지나요?
단순히 값을 바꾸는 행위 자체는 성능에 큰 영향을 주지 않아요. 다만, 가변 변수가 너무 많아지면 컴파일러가 최적화를 수행할 때 제약이 생길 수 있고, 무엇보다 사람이 코드를 이해하고 유지보수하는 데 드는 비용이 커지는 것이 더 큰 문제입니다.
Rust 변수 마스터를 위한 마지막 정리
오늘 우리는 Rust의 독특하면서도 강력한 변수 시스템에 대해 깊이 있게 알아봤어요. 처음에는 컴파일러의 잔소리가 귀찮게 느껴질 수 있지만, 그 잔소리가 결국 여러분의 프로그램을 안전하게 지켜주는 든든한 가드 역할을 한다는 것을 기억해 주세요.
- 모든 변수는 기본적으로 불변(Immutable) 상태예요.
- 값을 변경하려면 반드시
let mut을 사용하세요. - 타입 변환이 필요할 때는 섀도잉(Shadowing)이 최고의 도구예요.
- 상수(const)는 컴파일 시점에 결정되는 고정값에만 사용하세요.
- 가변성은 꼭 필요한 곳에만 최소한으로 사용하는 것이 좋은 습관이에요.
이제 이론은 충분해요. 직접 코드를 작성하며 컴파일러와 싸워볼 시간이에요! 오늘 배운 내용을 바탕으로 작은 계산기 프로그램을 만들거나, 사용자 입력을 처리하는 연습을 해보는 건 어떨까요?
🚀 다음 단계로 나아가기:
- 오늘 할 일: Rust Playground에서 섀도잉과 mut의 차이를 코드로 직접 구현해 보기
- 이번 주 할 일: 가변 변수를 최소화하여 간단한 반복문 로직 완성해 보기
- 실행 직전 할 일: 컴파일러 에러 메시지를 읽고 스스로 해결해 보는 연습하기
궁금했던 Rust 변수와 가변성 질문의 답을 통해 기초를 다졌다면, 이제 Rust의 가장 매력적인 주제인 소유권(Ownership)과 빌림(Borrowing) 개념으로 넘어가 보세요. 이 개념들까지 마스터하면 여러분은 진정한 Rust 개발자로 거듭날 수 있어요!
함께 읽으면 좋은 글: Rust 입문자를 위한 학습 가이드, Rust 소유권과 빌림 완벽 정리