변수와 가변성

2 장에서 언급했듯이, 기본 변수는 불변성 입니다. 이것은 Rust가 제공하는 안전성과 손쉬운 동시성이라는 장점을 취할 수 있도록 코드를 작성하게끔 강제하는 요소 중 하나 입니다. 하지만 여전히 당신은 가변 변수를 사용하고 싶을테죠. 어떻게 그리고 왜 Rust에서 불변성을 애호해주길 권장하는지 알아보면 그런 생각을 포기할 수 있을지도 모르겠습니다.

변수가 불변성인 경우, 일단 값이 이름에 bound되면 해당 값을 변경할 수 없습니다. 시험 삼아 cargo new --bin variables을 실행해서 * projects * 디렉토리에 * variables *라는 새 프로젝트를 생성 해 봅시다. 그런 다음 새 * variables * 디렉토리에서 * src / main.rs *를 열고 코드를 다음과 같이 바꿉니다.

Filename: 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 명령을 통해 실행시켜 봅시다. 당신은 다음과 같이 출력되는 에러를 확인하게 될 겁니다.

error[E0384]: re-assignment of immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ re-assignment of immutable variable

위의 예제는 컴파일러가 당신이 만든 프로그램에서 당신을 도와 에러를 찾아주는 방법에 대해 보여주고 있습니다. 컴파일러 에러가 힘빠지게 만들 수도 있지만, 단지 당신의 프로그램이 아직 안전하게 수행되긴 미흡하다는 뜻이지, 당신의 소양이 부족함을 의미하는건 아닙니다. 숙련된 Rustacean들도 여전히 에러를 발생시키니까요. 에러가 나타내는 것은 불변성 변수에 재할당이고, 원인은 우리가 불변성 변수 x에 두 번째로 값을 할당했기 때문입니다.

우리가 이전에 불변성으로 선언한 것의 값을 변경하고자 하는 시도를 하면 컴파일 타임의 에러를 얻게 되고 이로 인해 버그가 발생할 수 있기 때문에 중요합니다. 만약 우리 코드의 일부는 값이 변경되지 않는다는 것을 가정하는데 다른 코드는 이와 다르게 값을 변경한다면, 전자에 해당하는 코드는 우리가 의도한 대로 수행되지 않을 수 있습니다. 특히 후자에 해당되는 코드가 항상 그렇지 않고 가끔 값을 변경하는 경우 나중에 버그의 원인을 추적하기가 매우 어렵습니다.

Rust에서는 컴파일러가 변경되지 않은 값에 대한 보증을 해주고, 실제로 이는 바뀌지 않습니다. 이것이 의미하는 바는 당신이 코드를 작성하거나 분석할 시에 변수의 값이 어떻게 변경되는지 추적할 필요가 없기 때문에 코드를 더 합리적으로 만들어줍니다.

하지만 가변성은 매우 유용하게 사용될 수 있습니다. 변수는 기본적으로 불변성이지만 우리는 변수명의 접두어로 mut을 추가하는 것을 통해 가변성 변수를 선언할 수 있습니다. 이 변수의 값이 변경을 허용하는 것에 추가로 향후 코드를 보는 사람에게 코드의 다른 부분에서 해당 변수의 값을 변경할 것이라는 의도를 주지시킵니다.

예를 들어, src/main.rs를 다음과 같이 변경해보도록 합니다.

Filename: 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.30 secs
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

mut를 사용하여, x에 bind된 값을 5에서 6으로 변경할 수 있습니다. 불변성 변수만을 사용하는 것보다 가변성 변수를 사용하여 보다 쉽게 구현할 수 있을 경우 가변성 변수를 만들어 사용할 수도 있습니다.

이런 의사 결정에 있어서 버그를 예방하는 것 외에도 고려해야 할 요소들이 있습니다. 예를 들어, 대규모 데이터 구조체를 다루는 경우 가변한 인스턴스를 사용하는 것이 새로 인스턴스를 할당하고 반환하는 것보다 빠를 수 있습니다. 데이터 규모가 작을수록 새 인스턴스를 생성하고 함수적 프로그래밍 스타일로 작성하는 것이 더 합리적이고, 그렇기에 약간의 성능 하락을 통해 가독성을 확보할 수 있다면 더 가치있는 선택입니다.

변수와 상수 간의 차이점들

변수의 값을 변경할 수 없다는 사항이 아마 당신에게 다른 언어가 가진 프로그래밍 개념을 떠오르게 하지 않나요: 상수 불변성 변수와 마찬가지로 상수 또한 이름으로 bound된 후에는 값의 변경이 허용되지 않지만, 상수와 변수는 조금 다릅니다.

첫 째로, 상수에 대해서는 mut을 사용하는 것이 허용되지 않습니다: 상수는 기본 설정이 불변성인 것이 아니고 불변성 그 자체 입니다.

우리가 상수를 사용하고자 하면 let키워드 대신 const키워드를 사용해야 하고, 값의 유형을 선언해야 합니다. 우리가 사용할 수 있는 유형들과 유형의 선언을 챕터 “Data Types,”에서 다루게 될 것이므로 자세한 사항은 지금 걱정하지 말고, 우리는 반드시 값의 유형을 선언해야 한다는 것을 알고 지나갑시다.

상수는 전체 영역을 포함하여 어떤 영역에서도 선언될 수 있습니다. 이는 코드의 많은 부분에서 사용될 필요가 있는 값을 다루는데 유용합니다.

마지막 차이점은 상수는 오직 상수 표현식만 설정될 수 있지, 함수 호출의 결과값이나 그 외에 실행 시간에 결정되는 값이 설정될 수는 없다는 점 입니다.

아래의 MAX_POINTS라는 이름을 갖는 상수를 선언하는 예제에서는 값을 100,000으로 설정합니다. (Rust의 상수 명명 규칙에 따라 모든 단어를 대문자로 사용합니다.)


# #![allow(unused_variables)]
#fn main() {
const MAX_POINTS: u32 = 100_000;
#}

상수는 자신이 선언되어 있는 영역 내에서 프로그램이 실행되는 시간 동안 항상 유효하기에, 당신의 어플리케이션 도메인 전체에 걸쳐 프로그램의 다양한 곳에서 사용되는 값을 상수로 하면 유용합니다. 사용자가 한 게임에서 획득할 수 있는 최대 포인트, 빛의 속도 같은 값 등등...

당신의 프로그램 전체에 걸쳐 하드코드 해야 하는 값을 이름지어 상수로 사용하면 향후 코드를 유지보수 하게 될 사람에게 그 의미를 전달할 수 있으므로 유용합니다. 또한 향후 해당 값을 변경해야 하는 경우에 상수로 선언된 값 한 곳만 변경하면 되므로 도움이 될 겁니다.

Shadowing

앞서 우리가 2장에서 추측 게임 예제를 통해 봤듯이, 이전에 선언한 변수와 같은 이름의 새 변수를 선언할 수 있고, 새 변수는 이전 변수를 shadows하게 됩니다. Rustaceans들은 이를 첫 변수가 두 번째에 의해 shadowed 됐다고 표현하게 됩니다. 해당 변수명은 두 번째 변수의 값을 갖게 된다는 뜻이죠. let키워드를 사용해서 다음처럼 반복하여 같은 변수 명으로 변수를 shadow 할 수 있습니다.

Filename: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

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

이 프로그램은 처음 x에 값 5를 bind 합니다. 이후 반복된 let x = 구문으로 x를 shadow하고 원본 값에 1을 더해서 x의 값은 6이 됩니다. 세 번째 let 문으로 또 x를 shadow하고, 이전 값에 2를 곱하여 x의 최종값은 12가 됩니다. 이 프로그램을 실행하면 다음과 같은 결과를 볼 수 있습니다.

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

이와 같은 사용은 변수를 mut으로 선언하는 것과는 차이가 있게 됩니다. 왜냐면 let키워드를 사용하지 않고 변수에 새로 값을 대입하려고 하면 컴파일-시에 에러를 얻게 되기 때문이죠. 우리가 몇 번 값을 변경할 수는 있지만 그 이후에 변수는 불변성을 갖게 됩니다.

또 다른 mut과 shadowing의 차이는 let키워드를 다시 사용하여 효과적으로 새 변수를 선언하고, 값의 유형을 변경할 수 있으면서도 동일 이름을 사용할 수 있다는 점 입니다. 예를 들어, 공백 문자들을 입력받아 얼마나 많은 공백 문자가 있는지 보여주고자 할 때, 실제로는 저장하고자 하는 것은 공백의 갯수일테죠.


# #![allow(unused_variables)]
#fn main() {
let spaces = "   ";
let spaces = spaces.len();
#}

이와 같은 구조가 허용되는 이유는 첫 spaces 변수가 문자열 유형이고 두 번째 spaces 변수는 첫 번째 것과 동일한 이름을 가진 새롭게 정의된 숫자 유형의 변수이기 때문입니다. Shadowing은 space_str이나 space_num 과 같이 대체된 이름을 사용는 대신 간단히 spaces 이름을 사용할 수 있게 해줍니다. 그러나 우리가 mut을 사용하려고 했다면:

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

우리는 다음처럼 변수의 유형을 변경할 수 없다는 컴파일-시의 에러를 얻게 될 겁니다:

error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected &str, found usize
  |
  = note: expected type `&str`
             found type `usize`

변수가 어떻게 동작하는지 탐구했으니, 더 많은 데이터 유형을 사용 살펴보도록 합시다.