
컴파일러와 싸우고 있는 당신을 위한 안내서
파이썬이나 자바스크립트처럼 자유로운 언어를 쓰다가 Rust를 처음 접하면 가장 먼저 당황스러운 경험을 하게 돼요. 분명히 값을 바꿀 수 있는 변수를 만들었다고 생각했는데, 컴파일러가 “cannot assign twice to immutable variable”이라는 빨간 글씨를 내뱉으며 코드를 막아서거든요. 마치 벽에 대고 이야기하는 것 같은 답답함을 느끼셨을 거예요.
이런 현상은 Rust가 결코 여러분을 괴롭히려는 것이 아니에요. 오히려 코드가 실행되는 도중에 예상치 못한 값이 바뀌어서 발생하는 대형 사고를 미리 막아주려는 든든한 보호막에 가깝답니다. Rust 변수와 가변성의 규칙을 제대로 이해하지 못하면, 개발자는 코드를 한 줄 쓸 때마다 컴파일러와 기싸움을 해야 하는 피로한 상황에 놓이게 돼요.
오늘 이 글을 끝까지 읽고 나면, 더 이상 컴파일러의 에러 메시지를 무서워하지 않게 될 거예요. 오히려 컴파일러가 알려주는 힌트를 바탕으로 더 안전하고 탄탄한 코드를 짜는 즐거움을 느끼실 수 있어요. 단순히 문법을 외우는 게 아니라, 왜 Rust가 이런 규칙을 만들었는지 그 철학을 함께 살펴볼게요.
이번 글에서는 다음과 같은 내용을 핵심적으로 다뤄요.
- Rust 변수가 기본적으로 불변인 진짜 이유
- 가변 변수를 선언하는 올바른 방법과 주의점
- 변수 섀도잉(Shadowing)을 활용한 깔끔한 코드 작성법
- 가변 참조(&mut)를 사용할 때 빠지기 쉬운 함정
- 실제 에러 상황별 즉각적인 해결 시나리오
본격적인 학습 전 꼭 알아야 할 기본 개념
Rust 프로그래밍의 세계로 깊이 들어가기 전에, 우리가 사용할 용어와 기본적인 사고방식을 정리해둬야 해요. Rust는 다른 언어와 달리 안전성(Safety)을 최우선으로 고려해요. 그래서 변수를 다루는 방식도 매우 엄격하고 명확하게 구분되어 있어요.
가장 먼저 머릿속에 넣어야 할 개념은 ‘기본값은 불변이다’라는 원칙이에요. 대부분의 언어에서는 변수를 만들면 언제든 값을 바꿀 수 있지만, Rust는 명시적으로 허락하지 않으면 값을 바꿀 수 없도록 설계되었어요. 이는 멀티스레드 환경에서 여러 곳이 동시에 값을 수정해서 생기는 데이터 경합(Data Race) 문제를 원천 차단하기 위함이에요.
Rust에서 가변성을 다룰 때는 단순히 ‘값을 바꾼다’는 개념을 넘어, 그 값이 메모리 상에서 어떻게 관리되고 참조되는지를 함께 고민해야 해요. 이것이 바로 Rust의 핵심인 소유권(Ownership) 개념과 맞물려 돌아가기 때문이에요.
아래 표를 통해 불변 변수와 가변 변수의 차이점을 한눈에 비교해 보세요. 이 차이를 명확히 아는 것이 실수 줄이기의 첫걸음이에요.
| 구분 | 불변 변수 (Immutable) | 가변 변수 (Mutable) |
|---|---|---|
| 선언 방식 | let x = 5; |
let mut x = 5; |
| 값 수정 가능 여부 | 불가능 (컴파일 에러 발생) | 가능 |
| 주요 사용 목적 | 데이터 보호, 예측 가능한 코드 | 상태 변경이 필요한 로직 수행 |
| 안전성 수준 | 매우 높음 | 상대적으로 주의 필요 |
변수를 만들 때 무조건 mut 키워드를 붙여야 한다고 생각하면 안 돼요. 오히려 ‘이 값은 변하지 않아야 한다’는 의도를 명확히 표현하기 위해 불변을 기본으로 사용하는 습관을 들이는 것이 훨씬 전문적인 개발자로 성장하는 길이에요.
실수를 줄이는 단계별 가변성 활용법
이제 본격적으로 실전에서 어떻게 변수를 다루어야 하는지 단계별로 살펴볼게요. 단순히 문법을 아는 것을 넘어, 상황에 맞는 최적의 선택을 하는 능력을 길러야 해요.
STEP 1. 불변성을 기본값으로 설계하기
Rust 프로그래밍을 시작할 때 가장 먼저 가져야 할 태도는 모든 변수를 불변으로 선언하는 것이에요. 처음에 변수를 만들 때는 일단 let만 사용하세요. 코드를 작성하다가 정말로 값이 바뀌어야 하는 순간이 오면, 그때 컴파일러의 에러 메시지를 보고 mut를 추가하면 된답니다.
이렇게 하면 코드 전체를 훑어볼 때 어떤 데이터가 변하고 어떤 데이터가 유지되는지 한눈에 파악할 수 있어요. 예를 들어, 어떤 함수의 입력값으로 들어온 숫자가 함수 내부에서 바뀌지 않는다는 확신이 있다면, 그 숫자는 불변이어야 해요. 그래야 나중에 코드가 복잡해졌을 때 ‘이 값이 어디서 바뀌었지?’라며 추적하는 불필요한 삽질을 막을 수 있어요.
STEP 2. mut 키워드로 가변성 부여하기
상태를 업데이트해야 하는 경우, 즉 카운터를 올리거나 리스트에 요소를 추가해야 할 때는 let mut를 사용해요. 하지만 여기서 주의할 점이 있어요. 가변성을 부여했다는 것은 그 변수를 통해 메모리에 직접 접근하여 값을 수정할 권한을 얻었다는 뜻이에요.
변수의 범위(Scope)가 넓을수록 가변 변수는 위험해져요. 너무 큰 함수 안에서 하나의 가변 변수를 여기저기 넘겨주다 보면, 데이터의 상태를 추적하기가 매우 힘들어집니다. 가변성은 가능한 한 좁은 범위 내에서만 머물게 하세요.
가변 변수를 사용할 때는 항상 ‘이 값이 바뀌었을 때 내 프로그램의 다른 부분에 어떤 영향을 줄까?’를 자문해보는 습관이 필요해요.
STEP 3. 변수 섀도잉(Shadowing)의 영리한 활용
많은 입문자가 mut와 섀도잉을 헷갈려 해요. 섀도잉은 동일한 이름의 변수를 새로운 let 문으로 다시 선언하는 것을 말해요. mut와 결정적인 차이점은 바로 데이터 타입(Type)을 바꿀 수 있다는 점이에요.
예를 들어, 사용자로부터 입력받은 문자열을 숫자로 변환해야 하는 상황을 가정해볼게요. 섀도잉을 사용하면 코드가 아주 깔끔해져요.
- 시나리오: 사용자 입력 처리
- 과정:
let input = "42";(문자열) $
ightarrow$let input: u32 = input.parse().unwrap();(숫자) - 이점:
input_str,input_int처럼 서로 다른 이름을 지어줄 필요 없이,input이라는 하나의 이름으로 변수의 의미를 유지하며 타입만 바꿀 수 있어요.
하지만 섀도잉을 남발하면 변수의 생명 주기가 꼬일 수 있으니, 타입 변환이 필요한 짧은 구간에서만 사용하는 것을 권장해요.
STEP 4. 가변 참조(&mut)와 빌림(Borrowing) 규칙
이 단계가 Rust에서 가장 어려운 고비예요. 변수 자체가 가변인 것과, 그 변수를 가리키는 ‘참조자’가 가변인 것은 완전히 다른 이야기거든요. 가변 참조를 사용하려면 반드시 단 하나의 가변 참조자만 존재해야 한다는 규칙을 지켜야 해요.
만약 어떤 값을 수정하기 위해 &mut를 사용하고 있다면, 그동안 다른 곳에서 그 값을 읽기 위한 &(불변 참조)도 사용할 수 없어요. 읽는 사람(Immutable borrower)이 있는 동안에 쓰는 사람(Mutable borrower)이 들어오면 데이터가 꼬일 수 있기 때문이죠. 이 규칙은 Rust가 메모리 안전성을 보장하는 가장 강력한 메커니즘이에요.
STEP 5. 구조체와 컬렉션에서의 가변성 관리
마지막으로, 실무에서 가장 많이 쓰이는 구조체(Struct)와 벡터(Vector)에서의 가변성을 다뤄야 해요. 구조체의 필드 하나를 바꾸고 싶더라도, 그 구조체를 담고 있는 변수 자체가 mut로 선언되어 있어야만 가능해요.
벡터의 경우도 마찬가지예요. let mut v = vec![1, 2, 3];처럼 선언해야만 v.push(4); 같은 명령을 내릴 수 있어요. 데이터를 뭉치로 관리할 때는 각 요소의 가변성보다는 컨테이너 전체의 가변성을 먼저 생각하는 것이 논리적인 흐름을 잡는 데 도움이 돼요.
자주 하는 실수와 해결법
학습 과정에서 누구나 겪을 수 있는 대표적인 실수 패턴을 정리했어요. 에러 메시지를 만났을 때 이 리스트를 떠올려보세요.
- ❌ 실수: 불변 변수에 새로운 값을 할당하려고 함 $
ightarrow$ 왜 발생하는가:let으로 선언만 하고mut를 빼먹어서 발생해요. $
ightarrow$ ✅ 해결법: 선언부에let mut를 추가하세요. - ❌ 실수: 가변 참조자(&mut)를 두 개 이상 동시에 만듦 $
ightarrow$ 왜 발생하는가: Rust의 빌림 규칙(Borrowing rule)을 위반했기 때문이에요. $
ightarrow$ ✅ 해결법: 한 명의 수정자가 작업을 마친 후(스코프가 종료된 후)에 다른 수정자가 나타나도록 코드 순서를 조정하세요. - ❌ 실수: 불변 참조(&)를 사용 중인데 값을 바꾸려 함 $
ightarrow$ 왜 발생하는가: 읽기 전용 권한만 가진 상태에서 쓰기 권한을 요구했기 때문이에요. $
ightarrow$ ✅ 해결법: 참조를 만들 때&대신&mut를 사용하세요. - ❌ 실수: 섀도잉을 써야 할 곳에
mut를 씀 $
ightarrow$ 왜 발생하는가: 타입을 바꾸고 싶은데 타입 변환이 안 되어 에러가 나요. $
ightarrow$ ✅ 해결법:let을 다시 사용하여 변수 이름을 재사용(섀도잉)하세요. - ❌ 실수: 구조체 필드만 따로 가변으로 만드려 함 $
ightarrow$ 왜 발생하는가: Rust에서 가변성은 변수 단위로 결정되기 때문이에요. $
ightarrow$ ✅ 해결법: 구조체 변수 자체를let mut로 선언해야 합니다.
자주 묻는 질문
Q. Rust는 왜 이렇게까지 가변성을 까다롭게 제한하나요?
데이터 경합(Data Race)을 막기 위해서예요. 여러 스레드가 동시에 같은 값을 수정하려고 하면 프로그램이 예측 불가능하게 동작하는데, Rust는 컴파일 단계에서 이를 원천 차단하여 런타임 오류를 획기적으로 줄여준답니다.
Q. 섀도잉을 쓰면 메모리가 낭비되지 않나요?
아니요, 섀도잉은 새로운 메모리 공간을 계속 만드는 것이 아니라, 기존 변수의 이름이 가리키는 대상을 교체하는 방식이에요. 컴파일러가 최적화를 잘 해주기 때문에 성능 걱정 없이 깔끔한 코드를 위해 사용하셔도 괜찮아요.
Q. 가변 변수를 최대한 적게 쓰는 게 좋은 건가요?
네, 대체로 그렇습니다. 가변성이 적을수록 코드는 예측 가능해지고 테스트하기 쉬워져요. 정말로 상태를 바꿔야 하는 곳에만 ‘핀포인트’로 가변성을 사용하는 것이 고수의 습관이에요.
Q. 가변 참조(&mut)를 쓸 때 스코프를 어떻게 조절해야 할까요?
가변 참조자가 살아있는 동안에는 다른 참조자가 생기지 않도록, 중괄호{ }를 사용해 참조자의 생명 주기를 명시적으로 제한하는 것이 가장 좋은 방법이에요.
Q. unwrap()을 써서 강제로 값을 바꾸는 방법은 없나요?
그건 가변성을 다루는 방법이 아니라 에러를 무시하는 위험한 방법이에요. 가변성은 문법적인 규칙이지 에러 처리의 문제가 아니랍니다.
마무리하며: 안전한 Rust 개발자로 가는 길
지금까지 Rust의 변수와 가변성이 왜 중요한지, 그리고 흔히 하는 실수들을 어떻게 해결하는지 깊이 있게 살펴봤어요. 처음에는 컴파일러가 참 까다롭다고 느껴지겠지만, 이 과정이 익숙해지면 여러분의 코드는 그 어떤 언어보다 단단하고 안전해질 거예요.
- 변수는 기본적으로 불변(Immutable)으로 선언하세요.
- 값이 바뀌어야 한다면
let mut를 명시적으로 사용하세요. - 타입 변환이 필요할 때는
mut대신 섀도잉(Shadowing)을 활용하세요. - 가변 참조(&mut)는 한 번에 단 하나만 존재해야 합니다.
- 가변성은 최대한 좁은 스코프(Scope) 내에서 유지하세요.
- 컴파일러의 에러 메시지는 적이 아니라 친절한 가이드입니다.
오늘 배운 내용을 바탕으로 바로 실습을 시작해 보세요. 머리로 이해하는 것과 직접 코드를 짜며 빨간 줄을 마주하는 것은 완전히 다르니까요.
🚀 이번 주 실천 과제:
- 기존에 작성했던 코드 중
mut가 너무 많은 부분을 찾아보세요. - 불필요한
mut를 제거하고, 섀도잉으로 바꿀 수 있는 부분을 개선해 보세요. - 가변 참조를 사용할 때 중괄호
{ }를 이용해 범위를 제한하는 연습을 해보세요.
Rust의 변수와 가변성 개념을 완벽히 익히셨다면, 이제 다음 단계인 소유권(Ownership)과 빌림(Borrowing)의 심화 과정으로 넘어갈 준비가 되셨어요. 같은 실수를 반복하지 않도록 오늘 배운 규칙들을 코드 옆에 메모해 두는 것도 좋은 방법이에요!
관련해서 더 깊이 있는 내용이 궁금하다면 Rust 변수와 가변성 관련 다른 글과 입문 Rust 학습 가이드를 차례로 읽어보시는 것을 추천드려요.