
화제가 되고 있는 Rust는 어떤 언어일까? 간단히 말하자면 C/C++의 성능은 살리면서도 안전성을 고려한 언어이다. 여기서 안전성, Safety는 주로 메모리 안정성을 말한다. 개발자가 저지르는 가장 큰 실수 중 하나는 메모리 관리와 관련되어 있다. free 해놓고 또 한다던가, 할당한 크기 이상으로 사용하려던가 하는 것들이다. 실제로 MS 제품의 보안 버그 70%가 memory safety 문제이다.
Rust는 이 메모리 안전성 문제를 효과적으로 해결한다. 대표적으로 Multithreading 환경에서 일어나는 동기화 문제를 생각해보자. 여러 스레드가 데이터를 공유하면 어떤 것이 공유되는지, 누가 쓰는지 정적으로는 알 수 없다. Rust에서는 소유권 개념을 도입하여 누가 어떤 자원을 쓰는지 컴파일 타임에서 정적으로 확인한다. 이렇게 체크하여 다수의 읽기(Multiple Reader) 혹은 하나의 쓰기(Single Writer)만을 허용한다. 이렇게 Compile Time에 데이터 공유 문제 가능성 있는 걸 모두 체크하는 것을 Borrow Checker가 담당한다.
Rust가 동기화 문제를 효과적으로 회피하는 것은 전적으로 문제가 발생할 가능성이 있는 코드를 컴파일 타임에서 걸러내기 때문이다. 물론 multithreading 환경을 고려하여 코드를 "잘" 쓰면 동기화 문제는 일어나지 않는다. 그런데 어떻게 잘 쓸 수 있을까.
언제나 그렇지만 기본이 탄탄해야 한다. 컴퓨터의 구조를 잘 알아야 "잘"할 수 있다. CPU는 하나의 하드웨어이지만 그 안에는 각자의 역할이 있는 모듈들이 있다. 이 모듈들은 각자 다른 역할을 하기 때문에 거의 동시에 돌아갈 수 있다. 이것을 CPU의 Pipelining이라고 부른다.

Pipeline을 설명할 때 가장 이해가 잘 가는 예시가 바로 빨래이다. 빨래 과정에는 세탁기에서 세탁하고, 건조기로 옮겨서 돌리고, 마른 빨래를 개는 과정이 포함된다. 빨래가 많이 쏟아져 들어오는 상황을 떠올려보자. 처음에는 세탁기만 돌리다가 시간이 지나면 세탁기에서 나온 빨래를 건조기에 넣고, 세탁기에도 다른 빨래를 넣는다. 그럼 세탁기와 건조기가 동시에 돌아가기 시작한다. 이제 건조기에서 나온 빨래를 받아 옆에서 접는다. 그럼 세탁, 건조, 빨래 접기가 동시에 이루어진다. 위 사진처럼 독립된 기능들이 순차적으로 실행되어 같이 실행되다가 다시 차례 차례 완료되어 꺼진다.
CPU pipelining의 가장 큰 목적은 최적화를 위함이다. CPU의 성능을 최대한으로 끌어올리려면 동시성이 최대가 되어야한다. 동시에 모든 모듈을 돌리려면? 프로그램의 명령어를 쪼개어 기능별로 각각의 모듈에게 주어야한다. 즉, 최적화를 위해서는 C와 같은 절차 지향 언어도 쓰여진 순서와 상관 없이(out of order) 재정렬되어 작동해야한다.
여기서 더 나아간 개념이 HyperThreading으로, 코어가 하나이면 worker가 1개인 전통적인 개념을 뒤엎고, 논리적인 Core를 2개로 만드는 것이다. 한 코어는 1 worker지만, 놀고 있는 step을 사용해서 마치 2개인 것처럼 운용하는 것이다.
즉, 코드는 개발자가 생각하는 것보다 순차적으로 돌아가지 않는다. 심지어 이 명령 재배열은 assembly어 한 줄 한 줄의 순서가 바뀌는 것이다. assembly어 단에서 코드의 순서가 바뀌는 것을 고려해서 코드를 짤 수 있는 사람은 많지 않다.
이 최적화는 CPU와 compiler에서 이루어진다. 단, 최적화는 single thread일 때만 보장된다. 즉, 공유 변수의 존재는 고려하지 않고 효율만을 위해서 순서를 바꾼다.
Multithreading에서는 이 최적화 때문에 지옥이 펼쳐진다. 누가 어떤 순서로 변수를 이용할지 모르니까. 절차형 언어에서 순서대로 진행한다면 교착 상태가 발생하지 않으리라 여기는 코드도, 스레드가 함께 실행되다보면 누가 먼저 실행되어 자원에 접근할 지 예측할 수 없게 된다.
Java는 최적화를 최대한 안 하는 걸로 이 문제를 해결한다. JVM이 코드를 해석해 CPU에 보낼 때 out of order를 최대한 안 쓰는 쪽으로 바꿔서 준다.
Rust는 이 문제를 컴파일 단위에서 체크해서 최적화로 성능을 살리면서도 개발자의 실수를 잡아준다.
Rust는 컴파일단에서 위험요소를 제거하므로 모든 걸 정적으로 지정하기 위해서 노력한다. 근데 컴파일러가 정적으로 알아내기 어려운 것들이 있다. 제대로 알기 위해서는 변수의 생애 주기를 알아내야 한다. 독립적인 multithreading 환경에서도. 예를 들어 메인 쓰레드가 있고 이 함수에서 쓰레드를 생성하는데 포인터를 이용한다고 생각해보자. out of order 환경에서 메인 쓰레드와 새 쓰레드 중 어떤 게 먼저 끝날 지 알 수 있을까? 예측하기 어렵다. 하지만 메인 쓰레드가 먼저 종료되어 사라지면 문제가 생긴다. 새 쓰레드를 가리키는 포인터가 메인 쓰레드 안에 있었기 때문이다. 이런 경우 공유 변수가 없는 multithreading 환경에서도 문제가 발생한다.
이처럼 생각지도 못하게 변수가 언제부터 언제까지 사용되는지 알아야한다. Rust에서는 Lifetime이라는 개념을 도입했다.
개발자가 직접 모호한 부분들을 지정해준다. "이건 정적이야", 혹은 "동적이야"하고 말이다. 이렇게 하면 Rust는 Garbage Collector가 없는데도 있는 것처럼 동작한다. 모든 변수의 Liftetime을 알고 있기 때문에 free를 안 해도 compiler가 알아서 해준다.
Garbage Collector는 runtime에 동적으로 작동한다. Garbage Collector가 동작할 때 검사하는 모든 자원에 lock을 걸어야 오류가 나지 않는다. 즉, Garbage Collector가 동작하려면 모두 멈춰야한다. 이는 곧 극심한 성능 저하로 이어진다.
Rust의 좋은 성능은 이에 기반한다. 실제로 Discord는 garbage collector 때문에 Go에서 Rust로 넘어갔다.
go를 사용하여 서버를 운영하던 디스코드는 신기한 현상을 발견한다.

CPU 성능에 spike가 튀는 것이다. 이 스파이크 때문에 response time에도 당연히 스파이크가 생긴다. 이 스파이크를 잘 보니 2초 주기라는 것을 발견한다. 그리고 go가 2초마다 의무적으로 garbage collector를 실행시킨다는 것을 알게 된다. 디스코드는 이 문제의 원인이 go라는 언어의 근본적인 현상이라는 것을 알게 된다. 그래서 garbage collector를 버리자!라는 결론을 내린다. Rust가 그런 거 없이도 있는 것처럼 돌아가게 할 수 있는 언어라는 사실을 발견한다. 게다가 runtime을 극한으로 없앨 수 있다. (vm을 안 쓰고 실행하니까) 이 완벽한 대체제로 디스코드는 latency spike를 해결했다.
Go를 선택하게 된 디스코드의 표현이 재미있다. "Even with just basic optimization, Rust was able to outperform the hyper hand-tuned Go version." Rust로 대충 만들어도 hyper hand-tuned go보다 빠르네?
Rust는 이 정적인 체크 가능을 조절할 수 있다. unsafe 모드로 불리는데 unsafe 키워드로 블록을 잡으면 이 안에서는 rust의 safe가 동작하지 않는다. C랑 똑같이 돌아간다. Borrow checker도 동작 안 한다. 이렇게 개발자가 조절할 수 있다는 것은 큰 장점이다. safe를 위해 비효율적으로 만드는 것을 피하고 대신 개발자가 책임진다. Garbage Collector를 컸다 껐다 하는 느낌이다. 따라서 동기화 문제가 나면 대체로 unsafe 블록에서 난다. 오류 찾기도 쉬워지는 것이다.
Rust는 뭐든 컴파일 타임에서 문제를 잡기 위해서 노력한다. 모두 정적으로! 대신 컴파일이 오래걸린다. C++의 Templete metaprogramming(컴파일 타임에서 미리 계산하는 게 효율적인 것들은 모두 먼저 계산해두자)과 유사하다.
Rust는 모든 변수가 Stack으로 들어간다. 정적이니까. 사이즈가 가변인 것은 절대 stack에 들어갈 수 없다. 개발자가 명시적으로 해야 동적인 Heap으로 들어간다. (사이즈가 가변인 건 모두 Heap으로 들어가고 포인터는 사이즈가 정해져 있으니까 이 포인터만 stack에 넣는다.)
C/C++는 매우 오래된 언어다. 그래서 legacy를 포기 못한다. (python은 포기했다. python2와 python3는 매우 크게 차이난다.)
C/C++는 #include로 해당 라이브러리의 코드를 그대로 갖다 붙여서 빌드한다. 이렇게 되면 코드가 너무 길어져 비효율적이다. java만 해도 import하면 끝나는데 말이다.
RUST는 젊은 언어라 C/C++의 단점을 대체로 고쳤다. 사실, C/C++는 외부 library를 잘 안 쓴다. open source가 크게 발달하지 않았을 때 형성된 문화인 탓도 있다. 대체로 개발자가 직접 만드는 것을 선호한다. 하지만 생산성을 위해서라도 외부 라이브러리를 사용하게 된다. 요새의 트랜드는 package manager로 외부 라이브러리를 관리하는 것이다. C/C++도 만들었는데… 잘 안 쓴다. 여기저기서 자체적으로 만들어 통일이 안 되어 있기 때문이다.
Rust는 official package manager “Cargo"가 있다.

Rust는 open source라 심지어 library의 code도 제공해준다! 이렇게 하면 외부 라이브러리를 많이 써도 쉽게 cross compile 가능하다. 외부 라이브러리가 지원하는지 안 하는지 신경 안 써도 된다. 라이브러리 목록은 crates.io에서 확인할 수 있다.
재미있는 점은 async-await가 존재하는데 이것을 외부 라이브러리에 의존한다는 것이다. 컴파일 과정에서 비동기 로직이 없으면 runtime에 넣을 필요 없기 때문이다. 유명한 비동기 라이브러리로는 tokio가 있다.

그외에도 fomater Rustfmt이 공식적으로 정해둔 코딩 스타일로 포멧을 맞춰준다. Rustup로는 toolchain 핸들링으로 cross compile이 쉬워진다. Clippy는 Rust의 Linter로 antipattern을 db화해서 탐지한다. 발견한 것들을 고칠 수 있는 좋은 패턴을 추천해주고, 예제도 주고 관련 link도 준다. 상당히 친절한 도움으로 code quality를 높일 수 있다. 라이브러리 만들때 주석문을 '///'로 해주면 document로 만들어주는 Cargo Doc 기능도 있다.

Rust는 잠깐 언급했듯 runtime을 극한으로 줄일 수 있어서 운영체제 없이 올려 embeded용도로도 가능하다. 하드웨어랑 memory에 direct access가 필요할 때는 unsafe 모드를 사용한다.
Web assembly 쪽에서도 각광 받고 있다. C#과 Rust가 투톱이라해도 과언이 아닐 정도이다. 실제로 rust documentation에 포함된 모든 예제 코드는 web assembly 형태라서 직접 컴파일해서 결과를 볼 수 있다.
Web assembly는 로딩은 JS로 하지만 로컬로 코드를 다운 시켜서 로컬의 하드웨어 리소스로 실행할 수 있게 한다. 즉, 모든 일반 코드가 다 돌아간다.
Rust backend는 Java에 비해 빨르다. 주로 Rocket.rs 라이브러리를 사용하거나 빠른 응답 속도를 위해서 hyper.rs를 사용한다.
Rust를 서버 장애가 잘 안난다. 보통 서버 장애는 메모리를 잘 못 건드려서 생기는데 rust가 그 위험성을 효과적으로 막기 때문이다.
Rust에는 unwrap()이라는 용법이 있다. 어떤 명령이 실패할 수도 있지만 예외처리 안 한다고 개발자가 명시적으로 지정할 때 사용한다. 즉, 이 명령이 무조건 성공할 거라 가정한다.
Java는 null ptr인지 체크한다. Rust는 그런 개념은 없다. 대신, option이 있고 some과 none을 해줘야한다. none 일 때를 안하고 싶으면 unwrap을 쓰면 된다. 물론 모든 것은 개발자의 책임이 된다.
이 unwrap만 안 쓰면 rust는 서버 장애를 일으키지 않는다. 몰론 아주 가끔, 메모리가 부족해서 linux가 서버를 kill 해버려서 서버가 죽기도 한다. 이 정도 두 경우 밖에 없다.
C와 C++의 성능을 그대로 지키면서 안전성을 보완한 것이 Rust의 가장 큰 특징이다. C/C++과 성능을 비교해보자.
주요 언어들의 속도를 비교한 그래프이다. C, C++, Rust와 그 외의 언어의 속도가 크게 차이나는 것을 볼 수 있다.

위의 링크에서 Rust와 다른 언어들을 직접 비교한 결과를 볼 수 있다. 비교 대상은 C(GCC), CLang, Intel C, C++이다. 잠깐 집고 가자면, GCC, Clang, Intel C는 모두 다른 컴파일러들이다. GCC는 아주 전통적인 C 컴파일러이고 Clang은 LLVM을 사용하는 컴파일러, Intel C는 Intel CPU 체제에 최적화된 컴파일러이다.
compile을 위한 VM이다. 각 언어의 compiler가 LLVM에 넣어주기만 하면 LLVM은 알아서 아키텍쳐에 맞춰서 컴파일해준다. optimizer를 개별로 구현 안해도 LLVM이 대신해주기 때문에 새로운 언어를 개발할 때 할 일이 줄어든다. Clang과 Rust 모두 LLVM을 사용한다.
아무튼, 성능 비교표를 보면 C와 Rust가 엎치락 뒤치락한다. C보다는 비슷하거나 조금 느리지 않을까라는 것이 Rust 개발자의 의견이다. 어쨋든 종합적으로 봤을 때 C에 버금가는 속도임은 분명하다.
AWS: Here's why we are investing in the Rust programming language
AWS의 EC2, S3, cloud front는 이미 rust고 나머지 서비스들도 이제 다 rust로 바꿀 예정이라고 한다. AWS가 Rust를 선택한 이유는 조금 독특하다. Amazon은 거대 기업이고 ESG 경영에 신경쓴다.AWS가 Carbon Free에 동참하려면 Datacenter가 먹는 전기를 줄여야 된다. 이때 Rust가 50%정도 전기 소비 절감시켜 준다고 한다. (C도 비슷하다.)
재미있는 것은 엔지니어를 프로덕션 레벨에 투입 시키는데 교육시간이 3-6개월 정도 밖에 안 걸렸다는 사실이다. Rust가 어렵다는 진입장벽이 높은데, 기본 문법을 익히는데는 2~3주면 된다.
[Google Open Source Blog] Rust fact vs. fiction: 5 Insights from Google's Rust journey in 2022
물론 다른 언어와 다른 문법들이 장벽이 될 수는 있다. 예를 들어 rust는 기본이 상수다. 대부분 변수를 상수처럼 쓰니까. 변수일 때만 알려주는 게 효율적이라는 생각에서다. 변수로 선언하려면 "mut" 키워드로 mutable 변수인 것을 알려주어야한다.
포인터 개념이 없기도 하다. 따라서 기본이 move고 상수라서 넣어주려면 copy를 해줘야 한다.(clone) 이런 방식의 가장 큰 이유는 "소유권"을 추정해야하기 때문이다.
젊은 언어이기 때문에 test-friendly language이기도 하다. 테스트를 작성하고 시행하는데 매우 편리하다.
Rust는 객체/ 절차/ 함수 지향 등 여러가지를 제공하지만 함수 지향으로 쓰는 게 더 적합한 언어이기도 하다.
공식 홈페이지의 learn 항목을 살펴보면 3가지 버전으로 documentation을 제공해주는 것을 볼 수 있다.

book: 웹 북 느낌이다. (ch9까지만 봐도 코딩하는데 문제 없다.) 한국 러스트 사용자 그룹에 한국어 버전 있다. (번역본은 최신 버전이 반영되지 않았을 수 있다.)
Rustling: 학원처럼 코스를 제공해준다. Exercise를 통해 rust를 학습할 수 있도록 돕는다.
Rust By Example: 다양한 예제를 쫙 보여준다. Web Assembly 기반이라 코드를 빌드해서 결과를 볼 수 있다.
Microsoft가 제공하는 Rust step by step 강의도 있다.
Rust playgroud에 web editor가 있다. 특이하게도 코드를 assembly로 변환해서 어떻게 최적화가 되는지 볼 수도 있다.
Google Andriod 팀의 comprehnsive Rust는 구글 내부 교육을 위해 만든 자료로 3일짜리 책 형식이다.
Effective rust는 코드를 어떻게 하면 잘 쓸까에 대한 내용이다.