Rust 프로그래밍

Rust는 변수가 왜 불변이라는 걸까?

plas 2020. 7. 3. 21:49

변수는 값이 바뀌는 것이고 상수는 값이 변하지 않는다고 배웠는데... Rust 언어의 온라인 문서(이하 Rust Book)를 읽으면서 기존의 프로그래밍 언어와 많이 달라진 개념을 하나씩 조금 다른 관점으로 정리해 보려고 합니다. Rust 언어의 프로그래밍에 대한 시각을 음미해 보려고 합니다.

https://rinthel.github.io/rust-lang-book-ko/ch03-01-variables-and-mutability.html

 

변수와 가변성 - The Rust Programming Language

2 장에서 언급했듯이, 기본 변수는 불변성입니다. 이것은 Rust가 제공하는 안전성과 손쉬운 동시성이라는 장점을 취할 수 있도록 코드를 작성하게끔 강제하는 요소 중 하나입니다. 하지만 여전히

rinthel.github.io

Rust의 let

Rust에서 변수는 메모리에 저장된 값을 가리키는 이름입니다. 값은 정수(i32)나 문자열(String) 같은 타입을 가집니다. 이러한 값은 메모리 어딘가에 저장되고 프로그램에서는 이름으로 그 데이터 또는 메모리 영역을 나타냅니다. (흔히 변수는 이름+값+메모리위치+타입 이렇게 네 개의 정보를 가진다고 합니다.)

Rust는 변수를 따로 타입 선언하지 않고 사용하는데, 대신 let을 통해 바인딩(선언 + 초기화)을 하게 됩니다. 이것은 이름을 대상이 되는 값에 연결하는 것입니다. 이 let의 개념은 예를 들어 수학에서 "x를 A의 나이라고 하자" 했을 때 x라는 변수가 수식이나 증명의 중간에 바뀌지는 않는다면, 이런 개념으로 러스트 변수의 불변성을 생각해 볼 수 있습니다.

이 때 변수가 불변성을 가진다는 것은 이름에 대한 속성으로 자바의 final과 같다고 볼 수 있습니다. 변수가 불변성을 가지면 한번 바인딩된 이름을 다른 것으로 바꾸어 지정할 수 없습니다. 

한편 이것은 자바에서 스트링이 불변이다 라고 할 때의 그 개념과는 다릅니다. 그때 불변의 의미는 메모리에 있는 객체가 바뀔 수 없음을 뜻합니다. 한번 일정한 크기로 배정된 스트링 객체는 길이가 바뀔 수 없습니다. 같은 길이라 하더라고 중간에 한 글자가 바뀐다거나 할 수도 없습니다. 그렇게 하므로써 자바는 스트링 객체를 자유롭게 전달하고 지정하고 필요할 때 바꾸어 사용하는 등 공유할 수 있었습니다.

변수가 불변일 때 어떤 점이 좋은가?

Rust Book에서는 변수가 불변이면 각 이름은 하나의 대상을 가리키게 되어 이름의 의미가 변하지 않으로 코드를 읽고 이해하기가 쉽다(가독성)라고 이야기합니다. 그 변수가 바뀌지 않을 것을 알면 우리가 코드를 읽을 때 값의 변화를 추적하는 노력이 필요없다는 것이지요. 그리고 불변이란 read-only를 의미하므로 값을 다시 메모리에 저장해야 할 필요가 없어 실행 코드의 성능을 훨씬 높일 수 있고 실행할 때에도 큰 유연성을 가지게 됩니다.

이것을 프로그래밍 언어의 개념인 참조투명성과 연결하여 생각해 볼 수 있습니다. 우리가 람다나 자바 스트림의 경험에서 알고 있듯이 참조투명성은 프로그램에서 코드 부분의 매우 좋은 성질입니다. 그 코드 부분이 부수효과가 없고 프로그램의 다른 부분에 아무런 변화를 가져오지 않습니다. 즉 프로그램의 상태에 영향을 줄 수 있는 변수(메모리)의 변화를 일으키지 않는다는 뜻입니다. 그렇게 되면 그 코드는 다른 부분과 독립적으로 생각할 수 있고 몇번을 수행하든 언제 수행하든 항상 같은 결과를 내는 것이 보장됩니다. 하나의 단위 연산(atomic operation)으로 생각해도 됩니다.

그럼 어떤 장점이 있을까요? 코드의 가독성 이외에도 이것은 분산병렬이라고 하는 실행 성능의 중요한 영역에 가능성을 열어 줍니다. 즉 이런 코드는 필요에 따라 데이터를 쪼개거나 코드 부분을 쪼개거나 다른 순서로 수행해도 되어서 분산병렬로 수행하기가 좋습니다. 이것이 Rust 언어의 성능에 핵심 키라고 할 수 있습니다.

변수가 불변? 발상의 전환

Rust는 변수의 불변성을 디폴트로 하므로써 프로그래밍 언어의 전통적인 개념에 혁신을 가져왔습니다. "변수란 값이 변하는 것이다" 라고 하는 기존의 개념을 깨고 변수는 어떤 값이든 가리키는 이름이다 라고 보는 것이죠.

이것은 프로그래밍에 대한 관점의 변화라고 볼 수 있습니다. 기존의 명령형 프로그래밍은 변수에 값을 지정하고 바꾸는 것(load & store)을 계산 수행의 기본으로 봅니다. 폰뉴만의 메모리와 CPU를 가진 컴퓨터 모델이지요. 그런데 사실 프로그램은 데이터에 연산을 수행하여 최종 결과를 얻는 것이 목적이지 그 중간에 값을 계속 기록해야 하는 것은 아닙니다. 명령형 언어냐 함수형 언어냐 하는 이분적인 구분이 아니라 계산이라는 것 자체를 보았을 때 그렇다는 것이지요.

사실 우리가 프로그램에서 변수에 값을 계속 바꾸면서 지정하는 경우는 for 루프에서 i 변수나 합계를 구하기 위한 sum 처럼 반복부에서 많이 발생합니다. 자세히 살펴보면 컴퓨터가 값을 메모리에 저장해두고 그것에 이름을 붙이는 용도로 변수를 쓰는 경우도 많다는 것을 알 수 있습니다. 사실 입력받은 변수값이나 계산에 필요한 값을 가진 변수들은 상당부분 변하지 않고 하나의 값을 가집니다. 물론 우리는 기존에 변수에 값을 바꿔가면서 프로그래밍하는 것에 더 익숙하고 그것이 문제라고 생각해 본 적이 없습니다. 그러나 잘 생각해 보면 진짜 바뀌어야 하는 변수는 일부입니다!!

최근에는 분산 병렬 및 대량 데이터의 처리를 위한 스트림 등의 방법에서 보이는 것처럼 연산의 결과를 다음 연산에 입력으로 보내는 함수형 프로그래밍 형태가 많이 사용됩니다. 변수에 값을 계속 기록하는 것이 아니라 자바의 스트림처럼 연속된 함수 호출의 형태로 프로그램을 작성하는 것이 더 바람직한 결과를 보여주는 경우가 많습니다. 물론 Rust는 함수형 언어를 지향하는 것은 아니지만 그러한 프로그래밍 패러다임이 중요하고 바람직하다고 생각하는 것이라 볼 수 있습니다. 두 가지 장점을 모두 취하기 위해 변수는 디폴트로 불변이라고 보고 꼭 필요한 것만 mut를 이용해 가변으로 선언하게끔 정한 것이지요.

Rust의 발상의 전환은 바로 이것입니다. 변수가 다 계속 값을 바꿔가면서 써야 하는 것은 아니다. 그렇게 바뀌어야 하는 변수들만 mut라고 표시해서 쓰자라는 것입니다.

지정과 가변 변수

let은 변수의 바인딩을 정해주는 것이고 이렇게 한번 정해진 바인딩은 디폴트로 불변입니다. 그러나 우리가 일반적으로 C나 자바 언어에서 사용하던 변수의 프로그래밍 패턴을 가능하게 하기 위해서는 값이 계속 변할 수 있는 변수도 필요합니다. 이런 경우 Rust는 mut라는 키워드를 통해 let에서 그 변수가 가변임을 명시적으로 정하도록 요구합니다.

그럼 그 변수는 기존 언어에서의 변수와 같이 지정 연산(=)를 통해 값을 바꿀 수 있습니다. 이 경우 Rust 컴파일러는 이 변수가 가리키는 값은 변할 수 있음을 알고, 그것이 스트링처럼 메모리 크기가 바뀔 수도 있는 타입이라면 그럴 수 있는 준비를 하게 됩니다. 그런 변수는 값이 바뀌면 메모리에 저장되어야 하고 바뀌었을 수도 있는 값을 참조할 수 없으며(병렬환경), 또한 그런 변수는 변경하는 부분과 그 후 사용하는 부분의 순서가 바뀌지 않아야 하는 것도 알고 있습니다. 즉 이런 변수에 대해서는 훨씬 더 많은 일을 하도록 코드가 생성될 것입니다.

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

위의 코드는 가변인 변수의 선언과 그 변수를 함수 매개변수로 넘기는 예를 보여줍니다. 여기서 볼 수 있는 것처럼 가변으로 선언된 변수는 가변인 매개변수로 넘길 수 있고 부르는 쪽과 불려지는 쪽 양쪽에서 mut를 명시해야 합니다. 그만끔 가변인 변수의 사용을 엄격하게 명시하게 함으로써 Rust는 위에서 얘기한 불변인 변수의 장점을 언어의 기본으로 삼고자 합니다. (여기서 &는 참조를 의미합니다.)

변수의 shadowing

Rust는 우리가 변수를 사용하던 방식을 면밀히 분석하여 변수는 하나의 값으로 변하지 않는 경우도 많고 계속 값이 바뀌면서 저장되어야 하는 경우도 있고, 또 어떨 때는 일부에서만 쓰이는 이름도 많다는 것을 알게 되었습니다. 그래서 여러 개의 이름, 즉 많은 변수를 만들어야 하는 경우도 있음을 발견했습니다. 이름의 낭비라고 볼 수 있는 거죠.

그래서 이름이 더이상 필요없게 되었을 때 그 이름을 재사용할 수 있게 해 줍니다. 즉 let a =로 한번 바인딩된 이름이 다음에 다시 let a=로 다른 값에 바인딩될 수 있습니다. 이것은 불변인 변수에 대해서도 아무 문제없이 가능합니다. 전혀 다른 타입의 값이 바인딩될 수도 있습니다. 컴파일러는 앞의 a의 바인딩이 끝나고 새로운 a의 바인딩이 시작되었음을 알 수 있습니다. 

변수에 대한 이러한 발상의 전환만으로도 Rust가 얼마나 프로그래밍에 대한 심오한 통찰력과 깊은 고민을 통해 언어를 만들었는가를 느끼게 됩니다. 언어가 얼마나 프로그래머에게 많은 영향을 미치는지도 다시한번 깨닫게 되네요.

“Learning another language is not only learning different words for the same things, but learning another way to think about things.”

— Flora Lewis

https://www.youtube.com/watch?v=S-01KjUJ3_Q