Rust
1. 개요
2010년 7월 7일에 처음 발표되고 2015년 5월 15일에 안정 버전이 정식 발표된 이후, 모질라 재단에서 연구 목적으로 개발되고 있는 프로그래밍 언어.
C, C++, Go, D와 같은 컴파일 기반의 언어이자 시스템 프로그래밍 언어에 속하며, Go보다는 반 년 늦게 나왔지만[1] 그나마 비슷한 시기에 등장했다는 점과 두 언어 모두 C/C++를 서로 다른 방향에서 대체하려 한다는 점 때문에 라이벌 관계로 엮이기도 한다. 멀티코어 프로세싱이 중요시되는 현 추세에 따라 동시성 프로그래밍 및 병렬 프로그래밍에도 강점을 가지고 있다.
온라인 상으로 코드를 실행시켜 보고 싶다면 여기로.
Rust 프로그래머는 스스로를 Rustacean이라고 자칭하는데 이 때문인지 Rust 프로그램과 관련된 미디어에서는 게[2] 와 연관된 이미지가 많이 나온다. 가이드북 표지에도 게가 나온다.
2. 역사
자세한 내용은 Github의 rust-lang/rust 릴리스 참조.
2018년 12월 6일에 발표된 1.31.0 버전을 기점으로 Rust 2018 Edition으로 에디션이 변경되고 (가이드 북) 1.31.0 이전 버전은 Rust 2015 Edition으로 정의되었다.
원래는 모질라 소속의 개발자인 그레이던 호어의 개인 프로젝트였으나, 모질라 재단의 차기 웹 브라우저 엔진 프로젝트인 서보(Servo)를 개발하는 데에 쓰기 위해 함께 연구 프로젝트로 편입되었다.[3] 자세한 내용은 서보 참고.
3. 특징
Rust는 현대적인 시스템 프로그래밍 언어로, C/C++와 동등한 수준의 속도를 달성하면서 안전성, 속도, 동시성을 목표로 한다. "안전하지 않은" 코드를 사용하여 "안전한" 코드로 추상화할 수 있는 도구 또한 언어 차원에서 제공한다. 안전한 코드는 C++의 RAII(Resource Acquiration Is Initialization)를 강제하고 참조하는 변수의 수명을 컴파일러에서 확인한다. 또한 함수형 프로그래밍 언어로부터 발전된 타입 시스템을 도입하였으며, 클래스 대신 트레이트(Trait)를 기반으로 다형성을 달성한다.[4] 타입이 강제되는 매크로를 사용해 언어를 확장하는 것이 가능하며, 현대적인 모듈 시스템을 통해 쉽게 모듈화될 수 있다. 모듈들은 크레이트(Crate)라고 하는 단위로 묶여서 실행 파일이나 라이브러리로 배포될 수 있으며, Cargo라는 패키지 관리 프로그램을 통해 빌드 및 패키지 배포를 자동화하고 필요한 라이브러리를 Cargo를 통해 자동으로 다운로드받을 수 있다.
3.1. 안전한 메모리 관리
Rust는 메모리 관리의 안전성이 상당히 고려된 언어이다. '''Lifetime(수명)과 Ownership(소유권)를 컴파일 타임에 추적'''할 수 있도록 설계되었으며, 스택 영역에 할당되는 객체와 변수의 생성과 소멸 시기를 컴파일 타임에 모두 결정한다. 참조자는 수명을 자동적으로 결정할 수 없는 경우도 있는데, 메소드나 함수의 (매개) 변수에 Lifetime를 명시해주어야 한다. 이는 많은 프로그래머들이 컴파일 에러를 겪었던 원인 중 하나이다. 이러한 개념을 스마트 포인터와 혼동하는 경우가 있는데, 스마트 포인터는 힙 영역에 할당되는 객체를 자동적으로 관리하기 위한 것으로 객체의 수명이 런타임에 결정되므로, 위 개념과 스마트 포인터는 역할이 엄연히 다르다.
힙 영역에 할당하는 객체의 경우, Rust 표준 라이브러리에서 제공되는
std::boxed::Box
나 std::rc::Rc
를 통해 스마트 포인터를 사용할 수 있다. std::rc::Rc
와 같은 Reference Counting(참조 횟수 계산) 스마트 포인터의 경우, '''순환 참조에 의한 메모리 누수가 여전히 발생할 수 있다'''는 점에 유의하자. 이 경우 참조 횟수를 추가하지 않는 std::rc::Weak
를 상황에 맞게 사용해서 해결 할 수 있다.Rust는 시스템 프로그래밍 언어인 만큼, Zero-cost abstraction(무비용 추상화)를 지향하기 때문에 쓰레기 수집을 사용하지 않는다. 그리고 기본적으로 C/C++와 마찬가지로 시스템에서 메모리를 직접 할당받아 사용한다.[5] 포인터에서 값을 직접 가져오거나 함수를 호출하는 것은 금지된다.
unsafe
스코프나 함수 내에서는 허용되지만, 이는 안전하지 않은 코드를 안전한 코드로 추상화 하기 위해 존재하는 것이다.3.1.1. 소유권과 수명
Rust 컴파일러는 "안전한 코드"에서 변수의 소유권을 '''컴파일 단계에서''' 모두 추적할 수 있다. 이를 이용해 메모리 할당과 해제를 오버헤드 없이 암묵적으로(Implicit) 수행함으로써, Rust는 '''런타임 오버헤드가 없는 안전한 메모리 관리'''를 이루어낸다.
Rust에서 모든 값[6] 은 그 값이 대입된 변수나 구조체 필드, 넘겨받은 함수 인자 등의 이름에 귀속된다. 이름은 자기에게 귀속된 값에 대한 소유권(Ownership)을 가지며, 다른 이름으로 값을 대입하면 그 이름으로 '''소유권이 이전'''된다.
그리고 다른 언어와 달리,
a = b
와 같은 연산은 '''기본적으로 복사 연산이 아닌 이동 연산'''이다. b가 가지는 데이터의 소유권을 a로 이전하게 되며, b로는 데이터에 접근할 수 없게 된다. 다만, 단순히 값을 복사하는 것으로 객체를 복제할 수 있는 경우, 해당 타입에 #[derive(Copy)]
와 같은 마커로 Copy Trait에서 파생하게 하여, 이동 연산 대신 복사 연산을 수행하도록 할 수 있다. std::vec::Vec
과 같이 값을 단순히 복사하는 것으로 객체를 복제할 수 없는 경우 기본적으로 이동 연산을 그대로 사용하게 되며, 해당 객체에서 Clone Trait을 구현하여 clone()
메서드를 통해 명시적인 복제를 제공할 수 있다.함수에 넘겨진 인자는 함수가 기본적으로 소유권을 가지게 된다. 함수가 반환하는 변수는 함수 바깥 스코프(Scope)로 소유권을 넘겨준다.[7] 만약 소유권을 넘겨주지 못한 채로 함수나 해당 스코프가 종료되면, 거기에 묶여 있던 변수도 수명이 끝나게 된다.
소유권 규칙에 따라 하나의 값은 언제나 하나의 이름으로만 접근할 수 있게 되는데, 실제로 이렇게만 프로그래밍을 하려면 제약이 너무 심하다. 따라서 Rust에서 다른 변수를 참조할 수 있도록 Borrowed pointer(`&`)[8] 를 제공하고 있다. Borrowed pointer는 C나 C++에서의 포인터처럼 다른 변수를 참조할 수 있는데, 참조되는 변수는 참조하는 변수보다 수명이 같거나 길어야 한다. 쉽게 말하자면, "도서관에서 책을 빌렸으면 적어도 도서관이 망하기 전에는 책을 반납하시오"와 같다.[9] . 멀티 스레드 참조와 같은 상황으로 인해, 수명을 컴파일 타임에 결정 할 수 없는 경우 컴파일 에러가 발생하게 된다.
3.1.2. 변경성
Rust 언어에서는 '''모든 변수의 변경성(Mutability)을 명확하게 컴파일 타임에 구분'''한다. 변수 수정에 관한 규칙은 다음과 같다.
- 한번 초기화된 변수는 기본적으로 읽기만 가능하고 변경이 불가능하다.
-
키워드를 통해 변경 가능한 변수를 선언할 수 있다.mut
- 참조의 경우, 변경 불가능한 변수를 변경 가능한 변수로 참조할 수 없다.
- 변경 가능한 참조 변수(Mutable reference)는 스코프 내에서 두 개 이상 선언될 수 없다.
std::sync::Arc
나 std::sync::Mutex
와 같이 동시성 제어를 제공하는 타입으로 묶어서 안전하게 관리할 수 있다.3.2. 제네릭
C++, C\#, Java 등 대중적인 정적 타입 프로그래밍 언어들에서 흔히 제공하는 제네릭(Generic)을 Rust 또한 가지고 있다. C#이나 Java처럼 타입 인자를 제공하는 정도의 기능을 갖고 있으며, 내부적으로는 C++의 템플릿처럼 타입별로 코드를 생성하는 방식으로 동작한다. [10]
3.3. 트레이트
트레이트(Trait)는 일종의 인터페이스로, 객체가 가지는 메서드의 목록을 제공하는 역할을 한다. 정확하지 않지만 쉽게 말하자면, '''하나의 객체는 여러 (범용) 인터페이스를 가질 수 있다'''. 트레이트는 객체와 달리 변수를 가질 수 없으며 상속이 가능하다. 그리고
impl T for A
블록을 작성해서 어떤 클래스에 트레이트를 구현하게 되면, 구현된 메서드를 사용할 수 있다. 트레이트에 메서드의 기본 구현이 있는 경우, 기본 구현을 그대로 사용하도록 하는 것도 가능하다.트레이트가 가지는 중요한 역할 중 하나는, 제네릭 인자에 트레이트를 써서 인자로 들어갈 타입에 필요한 조건을 붙이는 것이다.[11] 예를 들어 값 세 개를 오름차순으로 정렬하는 제네릭 함수는 이렇게 만들 수 있다.
3.3.1. 특수한 트레이트
몇몇 트레이트(Trait)는 컴파일러에게 특수한 취급을 받는다. 이중 몇몇은 특수한 성질을 나타낼 때 쓰이고 몇몇은 문법적 추가 요소(Syntactic Sugar)로서 작동한다.
-
: 타입Send
가 스레드 경계선을 넘나들 수 있도록 하는 트레이트. Send가 아닌 타입은 드물지만 만약에 thread local storage 등을 활용하는 커스텀 타입을 만들 경우에는 Send가 아닐 수 있음.T
-
: 타입Sync
의 레퍼런스가 스레드 간에 공유되도록 하는 트레이트.T
는 기본적으로 읽기 전용을 뜻하기 때문에 스레드간에 공유되어도 괜찮지만 가끔&T
로 내부 데이터를 바꿀 수 있는 타입의 경우에는&T
를 금지하거나 또는 atomic operation 을 사용하여 공유되는 상황에도 문제가 없도록 해야함. 대표적인 예시로Sync
등의 타입이 있음.AtomicUsize
-
:Copy
에서 이동 연산 대신 복사 연산을 수행하도록 하는 트레이트. 단순히 값을 복사하는 것으로 복사/복제 동작을 안전하게 제공할 수 있는 경우에만 사용해야 한다.a = b
-
: 완전한 복제를 제공하도록 하는 트레이드.Clone
-
/Deref
: 스마트 포인터가 가진 객체를DerefMut
로 접근하기 위해서 제공되는 트레이트..
Iterator
/IntoIterator
등의 trait 등도 존재한다.3.4. 하이지닉 매크로
Rust의 매크로는 다음의 특징을 가지고 있다.
- 매개 변수의 이름으로 인해 텍스트 중복이 되지 않도록 자동 처리된다.
- 매크로에 입력될 매개 변수의 타입을 지정할 수 있다.
- 메타 프로그래밍이 가능하다.
MUL5(3)
은 (3 * 5)
로 치환될 것이다. 이것은 문맥에 따라 수많은 잠재적인 문제를 만들 수 있다. Rust는 LISP의 하이지닉 매크로 개념을 사용한다. 매크로는 단순한 텍스트가 아니며, 단순한 텍스트 중복이 문제가 안 되도록 컴파일러가 알아서 잘 처리한다. 단순한 문자열 치환이 아니라 문법의 일부이며, 전처리기가 아니라 컴파일러가 처리한다.C/C++의 매크로에서는
MUL5(3)
에서 3
이 곱셈이 가능한지, 실수인지 이런 여부를 판단할 수 없다. Rust의 매크로 시스템에서는 MUL5(x)
에 들어온 인자가 변수의 이름인지, 문자열인지, 네임스페이스인지, 람다 함수인지 등을 알 수 있다.또한, Rust의 매크로는 '''메타 프로그래밍'''을 지원한다. 메타 프로그래밍은 컴파일 타임에 모든 것이 결정되는 프로그램을 작성하는 것으로써, 입력되는 매개 변수에 따라 넣을 코드를 결정하거나 미리 계산하도록 할 수 있다.
Rust에서 제네릭과 메타 프로그래밍이 분리되어 지원되는 것은, C++에서는 템플릿 문법이라는 하나의 도구를 이용해 일반화 프로그래밍(제네릭)과 메타 프로그래밍 두 가지를 지원하는 것과 대비되는 점이다. 물론, Rust에서 메타 프로그래밍이 매크로로 분리되었다고 해도, 메타 프로그래밍 자체가 여전히 어려운 것은 변함이 없다.
3.5. 비동기 프로그래밍
https://areweasyncyet.rs/
Rust는 언어 차원에서 비동기 프로그래밍을 지원하고 있으며, Python, C\#과 같은 다른 언어에서 사용되는 async/await 구조의 비동기 구문을 비슷한 형태로 사용할 수 있다.#
다른 언어에서 흔히 사용되는 비동기 프로그래밍 패턴 중 Future 개념은 코루틴[12] 인 대표적인 예이다. 이러한 개념을 구현한
Future
# 트레이트와 async
/await
문법을 사용하면 되는데, 다른 언어와 달리 비동기 함수를 실행할 실행자(Executor)가 별도로 존재한다. 실행자는 기본적으로 구현되어 있지 않기 때문에, tokio와 같은 라이브러리를 사용하거나 직접 구현해야 한다.Rust에서 비동기 프로그래밍을 시작해보고 싶다면 Asynchronous Programming in Rust[13] 를 읽어보는 것을 추천한다. 아직 미완성이기는 하지만 Rust 언어에서 코루틴과 실행자를 구성하는 대략적인 구조를 이해하는데 도움이 되며, 부족한 내용은 표준 라이브러리 API 명세(Specification)나 웹사이트에서 관련 내용을 찾아보면서 보충할 수 있다.
4. IDE 지원 현황
IntelliJ IDEA와 CLion의 Rust 플러그인은 IntelliJ 플랫폼답게 히스토리 기반 코드 컴플리션, 리팩토링, 디버깅, Cargo 패키지 추적 등 여러가지 편의성을 제공한다. 비주얼 스튜디오 코드는 IDE는 아니지만, 공식으로 Rust 언어용 플러그인을 제공한다. 몇몇 다른 언어와 마찬가지로 Rust Language Server와 통신하는 방식으로 동작하며, Cargo 프로젝트 내의 모든 문제나 오류를 바로 쉽게 확인할 수 있다. 사용자 수가 적은 rust-analyzer 플러그인도 있는데 시각적으로 타입 힌트를 표시하는 등 공식 플러그인보다 더 편리한 기능들을 제공한다. Atom도 Rust를 플러그인 형태로 지원하고 있다.
5. 주목받고 있는 언어
5.1. 개발자들이 가장 좋아하는 언어
2015년부터 스택 오버플로우 설문조사에서 매년 가장 좋아하는 언어 중에 하나로 선정되고 있다. 2015년에는 3위에 진입했다가, 2016년부터 매년 연속 1위를 달성했다. (2017년, 2018년, 2019년, 2020년 설문조사 결과)
5.2. 마이크로소프트도 주목하고 있는 언어
비주얼 스튜디오를 개발한 마이크로소프트가 메모리 안정성이 좋고, C 및 C++를 대체할 수 있는 시스템 프로그래밍 언어라고 밝혔다. 심지어 Rust의 주목성에 탐이 났는지 Rust 같은 언어를 자체적으로 만들겠다는 소식이 나왔을 정도.
6. 사용 현황 및 전망
오버헤드 없는 안전한 메모리 관리가 가능하다는 점에서 시스템 프로그래머들이 주로 선호하는 편이다. 모질라 재단에서 개발한 만큼, 파이어폭스에서는 상당 부분을 Rust로 대체 하였고, 구글은 차기 운영체제인 퓨시아에서 Rust를 사용중이며, 페이스북도 내부 시스템 일부에 Rust를 적용한 상태라고 한다. 레딧에서는 백엔드 개발을 담당할 Rust 엔지니어를 구인한 바 있다. # 이 언어로 Redox란 운영체제도 개발되고 있다. 또한 페이스북의 암호화폐 리브라에서도 사용되고 있다. npm과 Cloudflare는 기반 언어를 C에서 Rust로 교체했다. 2020년 기준 Discord가 서버와 클라이언트단 언어를 Golang에서 Rust로 교체하였다. 차세대 자바스크립트 런타임인 Deno도 시스템 바인딩을 Rust로 작성하였다.
그러나 전체적으로 보면 여전히 Rust의 사용률은 미진한 편이다. TIOBE 순위에서는 순위가 떨어지는데, 이는 소프트웨어 인프라가 기존 언어로 대부분 갖추어져 있고, 전문 인력을 구하기 쉽지도 않으며, 라이브러리에 대한 지원이 적기 때문으로 보인다.# 순위는 5년동안 꾸준히 상승하고 최근 Scala와 Kotlin을 제치고 올라가고 있지만, 여전히 대부분의 기업은 시스템 프로그래밍에 거의 C/C++만을 사용하고 있다.
다만, 2019년 스택 오버플로우 개발자 설문조사에서 무려 파이썬과 자바스크립트를 제치고 83.5%의 높은 선호도로 "개발자들에게 가장 사랑받는 언어" 1위를 차지했다는 점이나 구글 트렌드나 각종 랭킹 사이트에서의 수치가 장기적으로는 증가하고 있다는 것을 고려하면 Rust의 미래가 어둡지 않다.
"Rust는 소유권과 수명을 직접 지정해줘야해 불편하고 문법이 어렵고 기능이 많아 배우기 힘들다"는 주장이 있으나, "Rust의 주 타겟이 되는 유저들은 Rust보다 훨씬 더 어렵고 불편한 C/C++를 쓰고 있기에 문제가 되지 않는다"는 반론도 있다. 페이스북이 공개한 자료에 따르면 자사 엔지니어가 Rust를 능숙하게 익히는데 두달이면 충분했다고 한다.
C++ 사용자들의 가장 큰 불만은 난이도가 아니라 메모리와 관련한 실수를 하기 매우 쉽다는 것이다. Rust의 설계 목표가 비교적 안전한 시스템 프로그래밍 언어인 만큼, Rust에서 메모리 안정성을 위해 문법을 강제함으로써 발생하는 불편함과 C++에서 메모리 버그를 코드 리뷰와 디버거로 일일이 찾아내는 수고는 비교가 안된다. 구글과 마이크로소프트에서는 Rust를 사용할 경우 자사 제품의 보안 버그 패치 비용의 70%를 절감할 수 있을 것이라는 내용의 문서도 공개하였다. 구글/마이크로소프트/인텔은 이미 Rust의 개발에 깊숙히 관여하고 있으며, 각종 컨퍼런스에서 공개적으로 이를 언급하고 있다.
Rust는 모질라 재단을 통하여 개발이 시작된만큼 모질라와 상당한 연관이 있는데, 2020년 8월 기부금으로 움직이는 모질라 재단이 코로나로 인해 자금 융통이 어렵다는 이유로 250명을 감축했으며, 이로 인해 Rust개발팀이 상당한 피해를 입었을 거라는 추측이 돌았다. # 그러나 일각에서는 Rust가 이미 모질라재단 소속의 기여보다 외부의 기여가 많아진 상황이라, 큰 영향은 없을 것으로 보기도 했다.
2020년 9월 TIOBE에서 18위를 달성했다.
2020년 11월 아마존(AWS 클라우드 팀)이 Rust 컴파일러의 공동 리드를 맡았던 펠릭스 클록(Felix Klock)을 개발자로 영입했다. 영입을 이끈 Matt Asay는 트위터를 통해 AWS에서도 Rust가 사용되고 있으며, 안정성과 퍼포먼스에 상당한 기여를 하고 있다고 밝히기도 했다. Matt Asay 트위터 또한 AWS는 2019년에 Rust의 스폰서가 되었으며, 이외에도 Rust 개발자를 적극적으로 구인하는 중이라고 밝히기도 했다.관련 기사 한 편으로는 이를 모질라재단의 감축으로 인한 효과라고 보는 시선도 있다.
7. 도서
이 책은 Rust 홈페이지에 있는 the book을 그대로 책으로 옮겨 실은 것이다. 오른쪽은 한국어로 번역된 번역서.
8. 관련 링크
- Cargo - Rust의 커뮤니티 crates 저장소
- 개발팀 블로그
- Reddit - 다만 참고하기 전, 레딧에서의 도를 넘은 행위들과 과장된 발언들로 인해 Rust 기반 웹 프레임워크였던 Actix의 개발이 중단되고 현재는 유지 및 보수만 이루어지고 있음을 유의하자.
- Awesome Rust - Rust로 구현된 각종 라이브러리 모음
- This Week in Rust - Rust 주간 뉴스레터
[1] 첫 발표 기준으로, 1.0 정식판 기준으로는 Rust가 3년 늦게 나왔다.[2] 유사단어인 Crustacean (갑각류)를 연상한 것으로 보인다.[3] 그레이던 호어는 이 뒤로도 한동안 수석 개발자로 개발에 참여하고 있다가, 현재는 수석 개발자 지위를 내려놓은 상태이다.[4] 기존의 객체 지향 프로그래밍 언어와 비교해서 상당한 차이가 있다. Rust는 상속을 지원하지 않으며, 인터페이스 역할을 하는 트레이트를 하나의 객체에 여러 개 합성 할 수 있다. 트레이트는 말 그대로 객체의 특성을 나타내기도 한다. 예를 들자면
Clone
트레이트는 객체가 복제될 수 있음을 나타내는 동시에 복제 인터페이스를 제공한다.[5] 1.32 이전에는 메모리 할당자인 jemalloc을 기본적으로 사용했기 때문에, 속도는 좀 더 빠르지만 OS 입장에서는 '''항상''' 사용하는 만큼만 메모리를 할당받는 것은 아니었다. 원하는 경우 라이브러리를 활용하거나 직접 구현하여 메모리 할당자를 바꿀 수 있다.[6] 메모리 영역이라고 이해해도 무리는 없다.[7] 값이 이동할 때 새로 메모리를 할당하고 메모리를 복사할 지, 그냥 주어진 메모리 영역을 그대로 쓸지 결정하는 것은 Rust 컴파일러의 몫으로, 최적화하기에 따라서는 함수로 인자를 넘기고 리턴받는 동안 단 한 번의 메모리 복사도 일어나지 않을 수도 있다. C++를 아주 잘 알고 있다면, RVO라는 약어가 익숙할 것이다.[8] Borrowed reference라고도 부른다[9] 그러나 Rust 컴파일러에서 수명과 관련한 오류를 확인하는 방식은 "책을 빌린 사람이 있으면 그 전에는 도서관 문을 닫지 마시오" 가 된다.[10] 의미불명. 보다 명확한 서술 혹은 참고문서가 필요[11] C++20의 Concepts 기능과 유사한 역할을 한다.[12] 코루틴은 서브 루틴을 일시 정지하고 재개할 수 있는 구성 요소를 말한다. 쉽게 말해 필요에 따라 일시 정지할 수 있는 함수를 말하며, 이를 활용하여 I/O 처리를 극대화할 수 있다. 이는 단순히 대기해야 하는 작업이 처리되기 전에 다른 작업을 먼저 처리하도록 할 수 있기 때문이다. 멀티 스레드 환경에서도 공유 자원의 접근이 필요할 때 뮤텍스의 락을 얻지 못하는 경우, 다른 작업을 먼저 처리하도록 하여 컴퓨팅 자원의 사용을 극대화할 수 있다.[13] 한글판