Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

변수와 가변성

“변수로 값 저장하기” 절에서 언급했듯이, 변수는 기본적으로 불변입니다. 이것은 러스트가 제공하는 안전성과 쉬운 동시성을 최대한 활용하는 방식으로 코드를 작성하도록 살짝 밀어 주는 여러 장치 중 하나입니다. 물론 여전히 변수를 가변으로 만들 수도 있습니다. 러스트가 왜 불변성을 선호하도록 유도하는지, 그리고 어떤 경우에는 왜 그 기본 선택에서 벗어나고 싶을 수 있는지를 살펴봅시다.

변수가 불변이라면, 어떤 값이 이름에 한 번 바인딩되고 나면 그 값을 바꿀 수 없습니다. 이를 확인해 보기 위해 projects 디렉터리 안에 cargo new variables 를 사용해 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 으로 프로그램을 실행해 보세요. 다음 출력처럼 불변성과 관련된 오류 메시지를 받게 될 것입니다.

$ 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`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

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

이 예제는 컴파일러가 프로그램의 오류를 어떻게 찾아주는지를 보여 줍니다. 컴파일러 오류는 답답할 수 있지만, 사실 그것은 아직 프로그램이 여러분이 원하는 일을 안전하게 하지 못하고 있다는 뜻일 뿐입니다. 여러분이 형편없는 프로그래머라는 뜻은 절대로 아닙니다! 경험 많은 Rustacean도 여전히 컴파일러 오류를 만납니다.

여러분이 cannot assign twice to immutable variable `x` 라는 오류 메시지를 받은 이유는, 불변 변수 x 에 두 번째 값을 대입하려 했기 때문입니다.

불변으로 지정된 값을 바꾸려 할 때 컴파일 시점 오류가 나는 것은 중요합니다. 바로 이 상황이 버그로 이어질 수 있기 때문입니다. 코드의 한 부분은 어떤 값이 절대 바뀌지 않는다고 가정하고 동작하는데, 다른 부분이 그 값을 바꿔 버리면 첫 번째 부분의 코드는 설계한 대로 동작하지 않을 수 있습니다. 이런 종류의 버그는 나중에 원인을 추적하기가 어렵고, 특히 두 번째 코드 조각이 값을 가끔만 바꿀 때 더 그렇습니다. 러스트 컴파일러는 여러분이 어떤 값이 바뀌지 않는다고 선언하면 정말로 바뀌지 않도록 보장해 주므로, 그 상태를 머릿속으로 따로 추적할 필요가 없습니다. 덕분에 코드에 대해 추론하기가 더 쉬워집니다.

하지만 가변성은 매우 유용할 수 있고, 코드를 더 편하게 작성하게 해 주기도 합니다. 변수는 기본적으로 불변이지만, 2장 에서 했듯이 변수 이름 앞에 mut 를 붙이면 가변으로 만들 수 있습니다. 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` profile [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 키워드로 선언하며, 값의 타입을 반드시 명시해야 합니다. 타입과 타입 주석은 다음 절인 “데이터 타입”에서 다루므로 지금은 세부를 걱정하지 않아도 됩니다. 다만 타입을 항상 명시해야 한다는 점만 기억하세요.

상수는 전역 스코프를 포함한 어떤 스코프에서도 선언할 수 있으므로, 코드의 여러 부분이 알아야 하는 값에 유용합니다.

마지막 차이는, 상수는 상수식으로만 값을 지정할 수 있고 런타임에 계산되어야만 하는 결과값으로는 지정할 수 없다는 점입니다.

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

#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}

이 상수의 이름은 THREE_HOURS_IN_SECONDS 이고, 값은 60(1분의 초 수) × 60(1시간의 분 수) × 3(이 프로그램에서 계산하고 싶은 시간 수)의 결과로 설정됩니다. 러스트에서 상수 이름은 모두 대문자로 쓰고 단어 사이는 밑줄로 구분하는 것이 관례입니다. 컴파일러는 컴파일 시점에 제한된 연산 집합을 평가할 수 있으므로, 이 상수를 그냥 10,800 으로 쓰는 대신 더 이해하고 검증하기 쉬운 형태로 적을 수 있습니다. 상수를 선언할 때 어떤 연산을 사용할 수 있는지는 Rust Reference의 상수 평가 절을 참고하세요.

상수는 선언된 스코프 안에서 프로그램이 실행되는 전체 시간 동안 유효합니다. 이런 성질 때문에 상수는 애플리케이션 도메인에서 프로그램의 여러 부분이 알아야 하는 값, 예를 들면 게임 플레이어가 얻을 수 있는 최대 점수나 빛의 속도 같은 값을 표현할 때 유용합니다.

프로그램 전반에서 쓰이는 하드코딩된 값을 상수로 이름 붙여 두면, 미래의 유지보수자가 그 값의 의미를 이해하는 데 도움이 됩니다. 또한 나중에 그 값을 바꿔야 할 때 코드의 단 한 곳만 수정하면 된다는 점도 장점입니다.

섀도잉

2장의 숫자 맞히기 게임 튜토리얼에서 본 것처럼, 이전 변수와 같은 이름으로 새 변수를 선언할 수 있습니다. Rustacean은 이때 첫 번째 변수가 두 번째 변수에 의해 shadowed 되었다고 말합니다. 즉, 변수 이름을 사용할 때 컴파일러가 보게 되는 것은 두 번째 변수라는 뜻입니다. 결과적으로 두 번째 변수는 첫 번째 변수를 가려 버리고, 다시 다른 변수에 가려지거나 스코프가 끝날 때까지 그 이름에 대한 모든 사용을 자신이 차지합니다. 섀도잉은 다음처럼 같은 변수 이름을 다시 쓰고 let 키워드를 반복해서 사용하면 됩니다.

Filename: 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}");
}

이 프로그램은 먼저 x 를 값 5 에 바인딩합니다. 그런 다음 let x = 를 다시 사용해 새 변수 x 를 만들고, 원래 값에 1 을 더해 x 값을 6 으로 만듭니다. 이후 중괄호로 만들어진 내부 스코프 안에서 세 번째 let 문도 x 를 다시 섀도잉하고 새 변수를 만듭니다. 이번에는 이전 값에 2 를 곱해서 x 값을 12 로 만듭니다. 그 스코프가 끝나면 내부 섀도잉은 사라지고 x 는 다시 6 이 됩니다. 프로그램을 실행하면 다음과 같은 출력이 나옵니다.

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished `dev` profile [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 키워드를 다시 사용할 때 우리는 사실상 새 변수를 만들기 때문에 값의 타입을 바꾸면서도 같은 이름을 재사용할 수 있다는 점입니다. 예를 들어 어떤 프로그램이 사용자에게 텍스트 사이에 넣고 싶은 공백 수를 공백 문자로 입력하게 한 뒤, 그 입력을 숫자로 저장하고 싶다고 해 봅시다.

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
}

첫 번째 spaces 변수는 문자열 타입이고, 두 번째 spaces 변수는 숫자 타입입니다. 이처럼 섀도잉은 spaces_strspaces_num 같은 다른 이름을 굳이 придумать할 필요를 없애 주고, 더 단순한 spaces 라는 이름을 그대로 재사용하게 해 줍니다. 반면 여기에 mut 를 사용하려고 하면, 다음과 같이 컴파일 시점 오류가 납니다.

fn main() {
    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` (bin "variables") due to 1 previous error

이제 변수가 어떻게 동작하는지 살펴봤으니, 변수가 가질 수 있는 더 많은 데이터 타입을 살펴봅시다.