Skip to content

3.1. Variables and Mutability


"변수로 값 저장하기" 섹션에서 언급했듯이 기본적으로 변수는 불변입니다. 이는 Rust가 제공하는 안전성과 손쉬운 동시성을 활용하는 방식으로 코드를 작성할 수 있도록 Rust가 제공하는 많은 넛지 중 하나입니다. 하지만 변수를 가변적으로 만들 수 있는 옵션도 있습니다. Rust가 어떻게 그리고 왜 불변성을 선호하도록 권장하는지, 그리고 때때로 불변성을 선택하지 않는 것이 좋은 이유를 살펴보겠습니다.

변수가 불변인 경우 값이 이름에 바인딩되면 해당 값을 변경할 수 없습니다. 이를 설명하기 위해 projects 디렉터리에 cargo new variables를 사용하여 variables라는 새 프로젝트를 생성해 보세요.

그런 다음 새 variables의 디렉터리에서 src/main.rs를 열고 해당 코드를 아직 컴파일되지 않은 다음 코드로 바꿉니다:

src/main.rs
fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

cargo run을 사용하여 프로그램을 저장하고 실행합니다. 이 출력과 같이 불변성 오류에 대한 오류 메시지가 표시될 것입니다:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error

이 예제는 컴파일러가 프로그램에서 오류를 찾는 데 어떻게 도움이 되는지 보여줍니다. 컴파일러 오류는 실망스러울 수 있지만, 실제로는 프로그램이 원하는 작업을 아직 안전하게 수행하지 못한다는 의미일 뿐, 여러분이 훌륭한 프로그래머가 아니라는 것을 의미하지는 않습니다. 숙련된 러스타시언도 여전히 컴파일러 오류가 발생시킵니다.

불변 변수 x에 두 번째 값을 할당하려고 했기 때문에 cannot assign twice to immutable variable `x`라는 오류 메시지가 표시됩니다.

불변으로 지정된 값을 변경하려고 할 때 컴파일 타임 오류가 발생하는 것은 바로 이러한 상황이 버그로 이어질 수 있기 때문입니다. 코드의 한 부분이 값이 절대 변하지 않는다는 가정 하에 작동하는데 다른 부분이 해당 값을 변경하면 코드의 첫 번째 부분이 설계된 대로 작동하지 않을 수 있습니다. 특히 두 번째 코드가 값을 가끔씩만 변경하는 경우에는 이런 종류의 버그의 원인을 사후에 추적하기 어려울 수 있습니다. Rust 컴파일러는 값이 변경되지 않는다고 명시하면 실제로 변경되지 않음을 보장하므로 사용자가 직접 추적할 필요가 없습니다. 따라서 코드를 추론하기가 더 쉬워집니다.

하지만 가변성은 매우 유용할 수 있으며 코드를 더 편리하게 작성할 수 있습니다. 변수는 기본적으로 불변이지만, 2장에서와 같이 변수 이름 앞에 mut을 추가하면 변수를 변경 가능하게 만들 수 있습니다. mut를 추가하면 코드의 다른 부분에서 이 변수의 값이 변경될 것임을 표시하여 나중에 코드를 읽는 사람에게 의도를 전달할 수도 있습니다.

예를 들어 src/main.rs를 다음과 같이 변경해 보겠습니다:

src/main.rs
fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

이제 프로그램을 실행하면 다음과 같은 결과가 나타납니다:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

mut를 사용할 때 x에 바인딩된 값을 5에서 6으로 변경할 수 있습니다. 궁극적으로 가변성을 사용할지 여부를 결정하는 것은 사용자에게 달려 있으며, 특정 상황에서 가장 명확하다고 생각되는 것이 무엇인지에 따라 달라집니다.

상수(Constants)

상수는 불변 변수와 마찬가지로 이름에 바인딩되어 변경이 허용되지 않는 값이지만 상수와 변수 간에는 몇 가지 차이점이 있습니다.

첫째, 상수에는 mut를 사용할 수 없습니다. 상수는 기본적으로 불변일 뿐만 아니라 항상 불변입니다. let 키워드 대신 const 키워드를 사용하여 상수를 선언하고 값의 타입에 주석을 달아야 합니다. 타입과 타입 어노테이션은 다음 섹션인 "데이터 타입"에서 다룰 예정이므로 지금 당장 자세한 내용은 걱정하지 마세요. 항상 타입에 주석을 달아야 한다는 점만 기억하세요.

상수는 전역 범위를 포함한 모든 범위에서 선언할 수 있으므로 코드의 많은 부분에서 알아야 하는 값에 유용합니다.

마지막 차이점은 상수는 런타임에만 계산할 수 있는 값의 결과가 아니라 상수 표현식에만 설정할 수 있다는 점입니다.

다음은 상수 선언의 예입니다:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

상수의 이름은 THREE_HOURS_IN_SECONDS이며, 그 값은 60(1분의 초 수)에 60(1시간의 분 수)을 곱하고 3(이 프로그램에서 계산하려는 시간 수)을 곱한 결과로 설정됩니다. Rust의 상수 명명 규칙은 단어 사이에 밑줄과 함께 모두 대문자를 사용하는 것입니다. 컴파일러는 컴파일 시 제한된 연산 집합을 평가할 수 있으므로 이 상수를 10,800으로 설정하는 대신 이 값을 이해하고 검증하기 쉬운 방식으로 작성하도록 선택할 수 있습니다. 상수를 선언할 때 사용할 수 있는 연산에 대한 자세한 내용은 Rust 레퍼런스의 상수 평가 섹션을 참조하세요.

상수는 선언된 범위 내에서 프로그램이 실행되는 전체 시간 동안 유효합니다. 이 속성을 사용하면 상수는 게임의 플레이어가 획득할 수 있는 최대 점수나 빛의 속도와 같이 프로그램의 여러 부분에서 알아야 할 수 있는 애플리케이션 도메인의 값에 유용하게 사용할 수 있습니다.

프로그램 전체에서 사용되는 하드코딩된 값을 상수로 명명하면 향후 코드 유지 관리자에게 해당 값의 의미를 전달할 때 유용합니다. 또한 나중에 하드코딩된 값을 업데이트해야 하는 경우 코드에서 변경해야 할 곳이 한 곳만 있으면 도움이 됩니다.

섀도잉(Shadowing)

2장의 추측 게임 튜토리얼에서 보았듯이, 이전 변수와 같은 이름으로 새 변수를 선언할 수 있습니다. 러스타시언은 첫 번째 변수가 두 번째 변수에 의해 가려진다고(shadowed) 말하는데, 이는 사용자가 변수 이름을 사용할 때 컴파일러가 두 번째 변수를 보게 된다는 것을 의미합니다. 사실상 두 번째 변수는 첫 번째 변수를 오버셰도우하여, 변수 이름이 섀도우 처리되거나 범위가 종료될 때까지 변수 이름을 사용하는 모든 것을 자신에게 가져갑니다. 다음과 같이 동일한 변수 이름을 사용하고 let 키워드를 반복해서 사용하면 변수를 섀도잉할 수 있습니다:

src/main.rs
fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}

이 프로그램은 먼저 x5라는 값에 바인딩합니다. 그런 다음 let x =를 반복하여 원래 값을 취하고 1을 더하여 새 변수 x를 생성하므로 x의 값은 6이 됩니다. 그런 다음 중괄호로 생성된 내부 범위 내에서 세 번째 let 문도 x를 섀도우 처리하고 새 변수를 생성하여 이전 값에 2를 곱하여 x의 값을 12로 만듭니다. 해당 범위가 끝나면 내부 섀도잉이 종료되고 x6으로 돌아갑니다. 이 프로그램을 실행하면 다음과 같이 출력됩니다:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6

섀도잉은 변수를 mut로 표시하는 것과는 다른데, 실수로 let 키워드를 사용하지 않고 이 변수에 재할당을 시도하면 컴파일 타임 오류가 발생하기 때문입니다. let을 사용하면 값에 대해 몇 가지 변형을 수행하지만 해당 변형을 완료한 후에는 변수를 변경할 수 없게 만들 수 있습니다.

mut와 섀도잉의 또 다른 차이점은 let 키워드를 다시 사용하면 사실상 새 변수를 생성하는 것이므로 값의 타입을 변경하되 동일한 이름을 재사용할 수 있다는 점입니다. 예를 들어, 프로그램에서 사용자가 공백 문자를 입력하여 텍스트 사이에 원하는 공백 개수를 표시하도록 요청한 다음 해당 입력을 숫자로 저장하고 싶다고 가정해 보겠습니다:

    let spaces = "   ";
    let spaces = spaces.len();

첫 번째 spaces 변수는 문자열 타입이고 두 번째 spaces 변수는 숫자 타입입니다. 따라서 섀도잉을 사용하면 spaces_str, spaces_num과 같은 다른 이름을 만들지 않아도 되며, 대신 더 간단한 spaces 이름을 재사용할 수 있습니다. 하지만 여기에 아래와 같이 mut를 사용하려고 하면 컴파일 타임 오류가 발생합니다:

    let mut spaces = "   ";
    spaces = spaces.len();

변수의 타입을 변경할 수 없다는 오류 메시지가 표시됩니다:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error

변수의 작동 원리를 살펴보았으니 이제 변수가 가질 수 있는 더 많은 데이터 타입을 살펴보겠습니다.


References