Rust의 불변성 때문에 당황했던 경험이 있나요?
새로운 프로그래밍 언어를 배우다 보면 마치 벽에 부딪힌 것 같은 기분이 들 때가 있어요. 특히 Rust를 처음 접하는 분들이라면 “왜 값을 바꾸려는데 컴파일러가 화를 내는 거지?”라는 생각과 함께 머리가 아파온 경험이 분명히 있을 거예요. 파이썬이나 자바스크립트처럼 변수에 값을 넣고 마음껏 바꾸던 습관이 남아있다면, Rust의 엄격한 규칙은 마치 나를 괴롭히는 규칙처럼 느껴질 수 있어요.
하지만 이 엄격함은 사실 개발자를 괴롭히려는 것이 아니에요. 오히려 프로그램이 실행되는 도중에 예상치 못한 값이 바뀌어 발생하는 대형 사고를 미연에 방지하기 위한 강력한 보호막이죠. 우리가 작성한 코드가 의도한 대로만 움직이는지, 그리고 실수로 값을 바꿔버리는 일이 없는지를 완벽하게 보장받고 싶다면 Rust 변수와 가변성 테스트를 이해하는 것이 필수적이에요.
이 글을 끝까지 읽고 나면 단순히 문법을 아는 수준을 넘어, 내가 만든 변수가 정말로 안전한지 검증할 수 있는 능력을 갖추게 될 거예요. 단순히 코드를 짜는 것을 넘어, 그 코드가 안전하다는 것을 증명하는 방법까지 배우게 됩니다.
- 변수와 가변성의 핵심 개념 이해하기
- 가변성 오류를 방지하는 테스트 작성법
- 실무에서 바로 쓰는 단위 테스트와 CI 연동
- 자주 발생하는 실수와 해결 가이드
테스트를 시작하기 전 반드시 알아야 할 기본기
본격적으로 테스트 코드를 작성하기 전에, 우리가 무엇을 테스트할 것인지 명확히 정의해야 해요. Rust의 가장 큰 특징은 모든 변수가 기본적으로 불변(Immutable)이라는 점이에요. 즉, 한 번 값을 정하면 절대로 바꿀 수 없어요. 만약 값을 바꾸고 싶다면 반드시 가변(Mutable) 키워드를 명시해야 하죠.
이 차이를 제대로 이해하지 못하면 테스트 코드 자체를 작성할 수 없어요. 우리가 테스트해야 할 대상은 크게 두 가지예요. 첫째는 값이 변하지 않아야 할 곳에서 실제로 변하지 않는지 확인하는 것이고, 둘째는 값이 변해야 할 곳에서 의도한 대로 정확히 변하는지를 확인하는 것이에요.
| 구분 항목 | 불변 변수 (let) | 가변 변수 (let mut) |
|---|---|---|
| 값 변경 가능 여부 | 불가능함 | 가능함 |
| 메모리 안전성 | 매우 높음 | 주의가 필요함 |
| 주요 용도 | 상수, 설정값, 읽기 전용 데이터 | 카운터, 상태 변경, 누적 합계 |
테스트 환경을 준비할 때는 Cargo라는 도구를 사용하게 돼요. Rust의 빌드 도구이자 패키지 관리자인 Cargo는 테스트를 실행하는 아주 강력한 기능을 기본적으로 제공하고 있어요. 테스트를 작성하기 전에 여러분의 프로젝트가 기본적인 빌드 상태를 유지하고 있는지 먼저 확인해 보세요.
가변성을 너무 남발하면 Rust의 핵심인 소유권(Ownership) 개념이 꼬일 수 있어요. 테스트를 작성할 때도 변수를 무작정 가변으로 만들기보다는, 꼭 필요한 순간에만 mut 키워드를 사용하는 습관을 들여야 해요.
실무에 바로 적용하는 변수와 가변성 테스트 단계
이제 본격적으로 테스트를 작성해 볼까요? 단순히 코드가 돌아가는 것을 확인하는 수준을 넘어, 데이터의 흐름이 안전한지를 검증하는 구체적인 단계를 따라와 주세요.
STEP 1. 불변성 규칙이 잘 지켜지는지 확인하기
가장 먼저 해야 할 일은 우리가 의도하지 않은 값이 변경되지 않는지 확인하는 것이에요. 예를 들어, 사용자의 아이디나 고유 번호 같은 데이터는 프로그램이 돌아가는 동안 절대로 바뀌면 안 돼요. 이런 데이터를 다루는 함수를 만들었다면, 해당 함수 내부에서 값이 실수로라도 수정되지 않는지 테스트해야 해요.
테스트 시나리오는 이래요. 특정 값을 가진 불변 변수를 함수에 전달하고, 함수가 실행된 후에도 그 값이 원래와 똑같은지 확인하는 거죠. 만약 함수 내부에서 어떤 마법 같은 일이 벌어져서 값이 바뀐다면, Rust의 컴파일러가 잡아주겠지만 런타임 로직 오류는 별개의 문제예요. 논리적으로 값이 유지되는지 assert_eq! 함수를 사용하여 꼼꼼하게 체크해야 해요.
STEP 2. 가변 데이터의 상태 변화 검증하기
반대로 값이 반드시 변해야 하는 경우도 있어요. 게임의 점수나 은행 계좌의 잔액 같은 데이터가 대표적이죠. 이런 가변 변수를 테스트할 때는 상태 전이(State Transition)를 확인하는 것이 핵심이에요.
단순히 “값이 바뀌었다”에서 끝내지 마세요. “A라는 행동을 했을 때, 값이 정확히 B만큼 증가하여 C가 되었는가?”를 검증해야 해요. 예를 들어, 잔액이 1,000원일 때 500원을 입금했다면, 최종 잔액이 반드시 1,500원이어야 한다는 점을 코드로 증명해야 하죠. 이때 가변 참조(&mut)가 올바르게 전달되었는지도 함께 살펴보는 것이 좋아요.
STEP 3. 섀도잉과 가변성의 차이점 구분하기
많은 입문자가 헷갈려 하는 부분이 바로 섀도잉(Shadowing)과 가변성(Mutability)의 차이에요. 섀도잉은 새로운 변수를 같은 이름으로 다시 선언하는 것이고, 가변성은 기존 변수의 값을 바꾸는 것이에요. 이 둘은 메모리 구조와 동작 방식이 완전히 달라요.
테스트를 통해 이 차이를 명확히 해볼 수 있어요. 섀도잉을 사용하는 경우 변수의 타입이 바뀔 수 있다는 점을 테스트 케이스로 만들어 보세요. 반면 가변성을 사용하는 경우에는 타입은 유지되면서 값만 변한다는 것을 확인해야 해요. 이 두 개념을 혼동하면 나중에 소유권 문제로 큰 어려움을 겪을 수 있기 때문에 반드시 테스트로 그 차이를 몸소 익혀두는 것이 좋아요.
STEP 4. 단위 테스트(Unit Test) 구조 설계하기
이제 코드를 어떻게 배치할지 결정해야 해요. Rust에서는 관습적으로 모듈의 가장 아래쪽에 mod tests 블록을 만들어 테스트를 작성해요. 이렇게 하면 테스트 코드가 실제 코드와 밀접하게 붙어 있어 관리가 편해지죠.
테스트 함수 위에는 반드시 #[test]라는 속성을 붙여야 Cargo가 이 함수를 테스트로 인식해요. 각 테스트는 독립적이어야 해요. 하나의 테스트에서 만든 가변 변수가 다른 테스트의 결과에 영향을 주지 않도록 주의하세요. 각 테스트 함수는 자신만의 환경에서 독립적으로 실행되어야 신뢰할 수 있는 결과를 얻을 수 있어요.
STEP 5. CI 연동을 통한 테스트 자동화하기
마지막 단계는 내가 짠 테스트를 매번 수동으로 돌리지 않는 거예요. GitHub Actions 같은 도구를 사용해서 코드를 서버에 올릴 때마다 자동으로 테스트가 실행되도록 설정해 보세요. 이것이 바로 현대적인 개발 방식인 CI(지속적 통합)의 시작이에요.
자동화 설정 파일인 YAML 파일을 작성하여, 코드가 푸시될 때마다 cargo test 명령어가 실행되도록 만드세요. 이렇게 하면 내가 변수의 가변성을 수정하다가 실수로 기존의 불변 규칙을 깨뜨렸을 때, 즉시 경고를 받을 수 있어요. 실수를 실시간으로 잡아내는 가장 완벽한 방법이죠.
1. 초기값 설정: 잔액 10,000원 (불변 확인)
2. 동작 수행: 2,000원 출금 (가변 동작)
3. 결과 검증: 잔액 8,000원 (결과 확인)
4. 예외 상황: 20,000원 출금 시도 (에러 발생 여부 확인)
자주 하는 실수와 해결법
테스트를 작성하다 보면 예상치 못한 에러 메시지와 싸워야 할 때가 많아요. 가장 자주 마주치는 문제들을 정리해 드릴게요.
❌ 불변 변수에 값을 할당하려고 시도함
왜 발생하는가: Rust의 기본 원칙을 잊고 다른 언어처럼 변수를 다뤘기 때문이에요.
✅ 해결법: 값을 바꿔야 하는 변수라면 선언할 때 반드시 let mut을 사용하세요.
❌ 가변 참조(&mut)를 여러 개 동시에 사용함
왜 발생하는가: Rust의 대여 규칙(Borrowing Rules)을 위반했기 때문이에요. 한 번에 하나의 가변 참조만 허용됩니다.
✅ 해결법: 가변 참조의 수명을 짧게 유지하거나, 데이터의 소유권을 명확히 전달하는 방식으로 코드를 재설계하세요.
❌ 테스트 함수에 #[test] 속성을 빠뜨림
왜 발생하는가: 함수를 작성만 하고 테스트 대상으로 등록하지 않았기 때문이에요.
✅ 해결법: 모든 테스트 함수 바로 윗줄에 #[test]가 있는지 확인하세요.
❌ 섀도잉을 가변성으로 착각함
왜 발생하는가: 이름은 같지만 실제로는 새로운 메모리 공간을 사용하는 것이라는 점을 간과했기 때문이에요.
✅ 해결법: 데이터의 타입이 바뀌어도 되는 상황인지, 아니면 기존 데이터를 수정해야 하는 상황인지 명확히 구분하세요.
❌ 테스트 결과가 매번 다르게 나옴
왜 발생하는가: 테스트가 외부 환경이나 전역 상태에 의존하고 있기 때문이에요.
✅ 해결법: 테스트는 항상 깨끗하고 독립적인 상태에서 시작하도록 함수 내부에서 데이터를 초기화하세요.
자주 묻는 질문
Q. Rust에서 변수를 가변으로 만드는 것이 성능에 나쁜 영향을 주나요?
아니요, 그렇지 않아요. 가변 변수를 사용한다고 해서 프로그램이 느려지는 것은 아니에요. 오히려 데이터의 위치를 옮기지 않고 값을 수정할 수 있어서 효율적일 때도 많아요. 다만, 가변성을 사용하면 안전성을 위해 컴파일러가 더 꼼꼼하게 검사하므로 설계 단계에서 신중해야 해요.
Q. 테스트 코드를 어디에 작성하는 게 가장 좋나요?
규모가 작은 기능은 파일 하단의 mod tests 블록에 작성하는 것이 편리하고, 규모가 큰 통합 테스트는 프로젝트 루트의 tests/ 폴더에 별도로 작성하는 것이 정석이에요.
Q. assert_eq! 외에 다른 검증 방법도 있나요?
네, 있어요. 조건이 참인지 확인하는 assert!, 결과가 거짓인지 확인하는 assert_ne! 등이 있어요. 상황에 맞게 골라 쓰면 된답니다.
Q. 왜 Rust는 이렇게 불변성을 강조하나요?
멀티스레드 환경에서 여러 곳이 동시에 같은 값을 바꾸려고 하면 데이터 경합(Data Race)이 발생해 프로그램이 터질 수 있어요. 불변성을 기본으로 하면 이런 위험을 원천 차단할 수 있기 때문이에요.
Q. 변수 값을 바꾸지 않고 타입만 바꾸고 싶을 때는 어떻게 하나요?
그럴 때 바로 섀도잉(Shadowing)을 활용하면 돼요. 같은 이름으로 새 변수를 선언하면 기존 변수는 가려지고 새로운 타입의 변수를 사용할 수 있어요.
안전한 Rust 프로그래밍을 위한 마지막 정리
오늘 우리는 Rust 변수와 가변성의 개념부터, 이를 검증하기 위한 실전 테스트 전략까지 깊이 있게 살펴보았어요. Rust의 엄격함은 여러분의 앞길을 막는 장애물이 아니라, 여러분의 코드가 세상에 나갔을 때 발생할 수 있는 수만 가지 오류를 막아주는 든든한 지원군이라는 점을 꼭 기억해 주세요.
- 모든 변수는 기본적으로 불변(Immutable)이며, 필요할 때만 mut을 사용해요.
- 가변 데이터는 상태 전이(A → B)를 중심으로 테스트해야 해요.
- 섀도잉과 가변성의 차이를 명확히 구분하여 설계하세요.
- 단위 테스트는 파일 하단에, 통합 테스트는 별도 폴더에 작성해요.
- CI를 통해 테스트를 자동화하여 실수를 방지하세요.
이제 이론은 충분해요. 직접 코드를 입력하고, 컴파일러가 내뱉는 에러 메시지를 마주하며 직접 수정해 보는 과정이 필요해요. 에러 메시지는 여러분을 비난하는 것이 아니라, 더 나은 코드를 작성할 수 있도록 알려주는 친절한 가이드니까요.
오늘 바로 실천해 보세요:
- 오늘 할 일: 기존에 작성했던 코드 중 변수를 가변으로 바꾼 곳이 있다면, 테스트 코드를 하나라도 추가해 보세요.
- 이번 주 할 일: GitHub Actions를 설정해서 내 프로젝트의 테스트가 자동으로 돌아가는 환경을 만들어 보세요.
- 실행 직전 할 일: 변수를 선언할 때 무조건 mut을 붙이는 습관이 있다면, 이를 제거하고 컴파일러의 조언을 들어보세요.
Rust 변수와 가변성을 믿을 수 있게 만드는 테스트를 작성하여, 더욱 견고한 프로그램을 만들어 가시길 응원할게요!
관련된 더 많은 정보가 궁금하다면 Rust 변수와 가변성 관련 다른 글과 입문 Rust 학습 가이드를 참고해 보세요.