왜 Rust일까
애플리케이션들은 공통적으로 다음과 같은 요구사항을 갖는다.
- 안전하고 안정적이며 신뢰할 수 있어야 한다.
- 리소스를 효율적으로 사용해야 한다.
- 지연 시간을 최소화해야 한다.
- 높은 동시성을 지원해야 한다.
몇가지 요구사항을 더 추가하면
- 시작 및 종료가 짧아야한다.
- 유지보수 및 리팩터링이 쉬워야한다.
- 개발자 생산성을 보장해야 한다.
위 요구 사항들은 모두 개별 서비스 수준과 아키텍처 수준에서 처리될 수 있다는 점이 중요하다.
러스트를 선택했을 때
러스트를 사용해 애플리케이션 백엔드와 소프트웨어 인프라스트럭처 서비스를 구현할 때 얻을 수 있는 이점에 대해 알아보자. 위에서 언급했던 필요한 사항들을 어떻게 만족시키는지 알아볼 것이다.
1. 러스트는 안전하다.
프로그램의 안전성에 대해 얘기를 할 때에는 타입 안전성, 메모리 안전성, 스레드 안전성 세 가지를 다른 측면에서 고려해야 한다.
타입 안전성
러스트는 정적 타입 언어이다. 컴파일 시간에 타입 검사를 수행하며, 타입 제약 조건을 검증하고 강제한다.
변수에 타입을 지정하지 않으면 컴파일러가 유추하고 이를 수행할 수 없거나 충돌이 발생하면 알림과 함께 진행을 방지한다.
이러한 타입 안전성 관점에서 러스트는 자바, C/C++과 유사하다. 러스트의 컴파일러는 강력하게 타입 안전성을 강제하며 유용한 오류 메시지를 제공한다.
메모리 안전성
러스트는 가비지 컬렉션을 사용하지 않고 자동 메모리 관리와 메모리 안전성을 제공하는데 이는 러스트 고유의 소유권 모델을 통해 달성한다. 러스트는 개발자가 데이터 구조의 메모리 레이아웃을 제어할 수 있도록 하고 명시적으로 소유할 수 있도록 한다.
러스트의 자원 관리 소유권 모델은 C++의 RAII와 안전한 메모리 사용을 가능하게 하는 스마트 포인터를 기반으로 한다.
러스트는 선언된 각 값을 소유자에게 할당되며 값을 다른 소유자에게 주고 나면 원래 소유자는 그 값을 더 이상 사용할 수가 없게된다. 또한, 소유자가 값의 범위를 벗어나면 해당 값은 메모리에서 해제된다.
러스트는 변수나 함수에 값에 대한 일시적 접근 권한을 부여할 수 있다. 이를 대여라고 하며 러스트 컴파일러(대여 체커)는 값을 대여하는 동안 참조가 값의 수명을 벗어나지 않도록 보장한다. 값을 대여하기 위해 &(참조) 연산자를 사용한다. 참조에는 공유만 가능하고 수정은 불가능한 이뮤터블 참조 &T와 수정만 가능하고 공유는 불가능한 뮤터블 참조 &mut T의 두 가지 유형이 있다. 러스트는 객체의 뮤터블 대여가 존재하면 다른 대여가 존재하지 않게 보장한다. 이 모든 것은 컴파일 타임에 강제되며 잘못된 메모리 접근과 관련된 모든 오류를 제거한다.
러스트를 사용하면 가비지 컬렉터 없이도 잘못된 메모리 접근 걱정 없이 프로그래밍할 수 있다. 러스트 컴파일 타임에서 다음과 같은 범주의 메모리 안전성 오류를 방지한다.
- 널 포인터 역참조 null pointer dereference: 포인터가 널인 상태에서 역 참조가 일어나 프로그램이 중단된 경우
- 세그멘테이션 폴트 segmentation fault: 메모리의 제한된 영여에 접근하려고 시도하는 경우 발생
- 댕글링 포인터 dangling pointer: 값이 더 이상 존재하지 않는 포인터인 경우 발생
- 버퍼 오버플로 buffer overflow: 배열의 시작 또는 끝을 벗어나는 요소에 접근하는 경우 발생, 러스트 이터레이터는 범위를 벗어나지 않는다.
스레드 안전성
러스트에서 메모리 안전성과 스레드 안전성은 소유권이라는 동일한 기본 원칙을 사용하여 해결된다. 러스트는 기본적으로 데이터 경합으로 인한 미정의 동작이 발생하지 않도록 보장한다. 러스트는 다른 언어들과는 다르게 스레드 안전하지 않은 객체는 스레드 간에 공유하지 못하도록 한다. 또한 일부 데이터 타입을 스레드 안전으로 표시하고 이를 강제한다. 러스트 컴파일러는 분류적으로 모든 데이터 경합을 방지하므로 멀티스레드 프로그램이 훨씬 안전해진다.
지금까지 언급한 내용들 외에도 러스트는 프로그램 안전성을 개선하는 몇 가지 기능을 제공한다.
* 러스트의 모든 변수는 기본적으로 이뮤터블하며, 변수를 뮤테이션 하기 위해서는 명시적으로 선언해야 한다. 이는 개발자로 하여금 어떤 방법으로 어디에서 데이터가 수정되어야 하는지, 각 객체의 수명은 어떻게 되는지 다시 고려하게 한다.
* 러스트의 소유권 모델은 메모리 관리는 물론 네트워크 소켓, 데이터베이스 및 파일 핸들, 장치 설명자 등 다른 리소스를 소유하는 변수의 관리도 처리한다.
* 가비지 컬렉터를 제거함으로써 예측 불가능한 동작을 방지한다
* match 구문은 완전성을 가지며, 이는 컴파일러가 match 구문에서 가능한 모든 경우를 처리하도록 강제한다. 따라서 개발자가 의도치 않게 특정 코드 흐름 경로를 처리하지 못하고 예상치 못한 런타임 동작을 유발하는 것을 방지한다.
* 대수 데이터 타입을 제공함으로써 해당 데이터 모델을 간결하고 검증할 수 있는 방식으로 쉽게 표현할 수 있다.
컴파일러에 의해 강제되는 러스트의 정적 타입 지정 시스템, 소유 및 대여 모델, 가비지 컬렉터 부재, 이뮤터블한 기본값, 완전한 패턴 매치은 안전한 앱을 개발하는 데 큰 이점이 된다.
2. 러스트는 자원 효율 적이다.
소프트웨어는 일반적으로 확장성 문제를 해결하기 위해 하드웨어를 더 많이 사용하는 경향이 나타난다.
CPU, 메모리 및 디스크 리소스를 서버에 추가하여 성능을 높이는 수직확장(scaling up), 네트워크에 더 많은 기계를 추가하여 부하를 분산하는 수평확장(scaling out)을 주로 사용한다.
이런 방식이 인기를 얻은 이유 중 하나는 주로 사용되는 웹 개발 언어 Java, C#, 파이썬과 같은 고수준 웹 개발 언어는 메모리 사용을 제한한다는 세밀한 제어를 할 수가 없어 CPU의 멀티코어 아키텍처를 잘 활용하지 못해 효울적으로 메모리를 할당하지 못했기 때문이다.
이와 반대로 러스트는 아래와 같은 기승들을 제공해 자원 효울적인 서비스를 만들 수 있다.
- 메모리 관리에 관한 소유권 모델을 제공함으로써, 러스트는 메모리 누수 또는다른 리소스 누수를 유발하는 코드 작성을 어렵게 만든다.
- 러스트는 개발자들이 프로그램의 메모리 레이아웃을 엄격하게 제어할 수 있도록 한다.
- 러스트는 다른 몇몇 주류 프로그래밍 언어와 같은 가비지 컬렉터를 가지고 있지 않아, 추가 CPU와 메모리 자원을 소모하지 않는다.
- 러스트는 대규모의 복자한 런타임을 갖지 않는다. 그렇기 때문에 러스트 프로그램을 하드웨어 자원이 부족한 임베디드 시스템 및 가전 제품, 산업용 기계 등에서도 실행할 수 있다. 러스트는 커널 없이 베어 메탈 에서도 실행하 수 있다.
- 러스트는 힙에 할당된 메모리의 깊은 복사를 방지하며, 메모리 풋 프린트를 최적화하기 위해 다양한 유형의 스마트 포인터를 제공한다. 러스트는 런타임을 갖지 않으므로, 자원이 극도로 적은 환경에도 적합한 모던 프로그래밍 언어 중 하나이다.
러스트는 정적 타입, 세밀한 메모리 제어, 멀티코어 CPU의 효율적인 사용, 비동기 I/O 시맨틱스 내장 등 최고의 특징들을 겸비해 CPU 및 메모리 이용 측면에서 매우 자원 효율적이다. 이는 곧 소규모/ 대규모 애플리케이션 모두에서 적은 서버 비용 및 낮은 운영 비용으로 이어진다.
3. 러스트는 지연 시간이 짧다.
네트워크 요청과 응답에 대한 왕복 레이턴시는 네트워크 레이턴시와 서비스 레이턴시 모두의 영향을 받는다. 네트워크 레이턴시는 전송 매체, 전파 거리, 라우터 효율, 네트워크 대역폭 등 다양한 요소의 영향을 받는다. 서비스 레이터시는 요청 처리의 I/O 지연, 불확실한 지연을 유발하는 가비지 컬렉터, 하이퍼바이저의 일시적 중단, 문맥교환의 양, 직렬화 및 역직렬화 비용 등 다양한 요소에 따라 결정된다.
러스트는 시스템 프로그래밍 언어로서 저수준 하드웨어 제어를 통해 짧은 지연 시간을 제공한다. 또한 러스트는 가비지 컬렉터와 런타임을 갖지 않으며 논플로킹 I/O 기본 지원, 고성능 비동기 I/O 라이브러리 및 런타임으로 구성된 생태계, 제로 비용 추상화의 기본 언어 설계 원칙을 갖는다. 그리고 러스트의 변수는 기본적으로 스택에 존재하기 때문에 더 빠르게 관리할 수 있다.
4. 러스트는 동시성을 가능하게 한다.
러스트는 동시성 친화적인 언어이며 개발자들은 멀티 코어 프로세서의 성능을 활용할 수 있다.
러스트는 전통적인 멀티스레딩과 비동기 I/O의 두 가지 유형의 동시성을 제공한다.
멀티스레딩:
러스트는 전통적인 멀티스레딩- 공유 메모리 및 메시지 전달 동시성을 지원한다. 값 공유에 대한 타입 수준의 보장을 제공한다. 스레드는 값을 대여하고 소유권을 가정하며 값을 새로운 스레드의 범위로 전환할 수 있다. 또한 데이터 경합 안전성을 제공해 스레드 블로킹을 방지함으로써 성능을 향상한다. 메모리 효율을 높이고 스레드 사이에서 공유되는 데이터의 복사를 방지하기 위해 변수 사용을 추적하기 위한 참조계수를 제공한다. 이 계수가 0이 되면 값이 삭제되고 안전한 메모리 관리가 이루어진다. 또한 뮤텍스를 사용해 스레드 사이의 데이터를 동기화할 수 있다. 이뮤터블 데이터에 대한 참조는 뮤텍스를 사용하지 않아도 된다.
비동기:
러스트는 논블로킹 I/O와 동시성 프리미티브 기능에 기반한 비동기 이벤트 루프를 통해 zero-cost futures와
async-await를 구현한다. 논블로킹 I/O는 코드가 데이터 처리를 대기하는 동안 중단되지 않도록 한다.
또한 러스트의 불변성 규칙은 고도의 데이터 동시성을 제공한다.
5. 러스트는 생산적인 언어이다.
러스트는 기본적으로 시스템 지향 프로그래밍 언어지만, 고수준의 기능적 프로그래밍 언어의 편의성도 추가한다.
- 익명 함수를 포함하는 클로저
- 이터레이터
- 제네릭과 매크로
- Option 및 Result와 같은 열거형
- 트레이트를 통한 다형성
- 트레이트 객체를 통한 동적 디스패치
러스트는 개발자가 효율적이고, 안전하며, 성능이 뛰어난 소프트웨어를 개발하도록 도울 뿐만 아니라 표현성을 통해 개발자 생산성을 최적화한다.
지금까지 러스트가 어떻게 메모리 안전성, 자원 효율성, 짧은 지연 시간, 높은 동시성 및 개바자 생산성의 독특한 조합을 제공하는지 살펴 보았다.
시스템 프로그래밍 언어가 제공하는 저수준 제어와 속도, 고급 언어가 제공하는 개발자 생산성, 가비지 컬렉터가 없는 매우 독특한 메모리 모델이라는 특징을 부여한다. 이러한 특성들의 이점을 이용해 과부하 상황에서도 낮은 응답 지연성을 유지하고 멀티코어 CPU와 메모리 같은 시스템 자원을 효율적으로 사용한다.
이 뒤에서는 러스트가 가진 제약사항의 일부를 살펴보자.
러스트에 부족한 부분
- 러스트는 진입 장벽이 높다. 초보자나 동적 프로그래밍 또는 스크립팅 언어들을 사용하던 사람들, 때로는 경험이 충분한 개발자들도 러스트 구문을 읽기 어려울 수도 있다.
- 현 시점에서는 다른 컴파일 언어보다 컴파일러의 속도가 느리다. 하지만 점진적으로 개선 중이고 이를 위한 노력도 지속되고있다.
- 러스트 생태계와 커뮤니티가 다른 주요 언어들에 비해 작다.
- 대규모로 러스트 개발자를 찾거나 고용하기는 상대적으로 어렵다.
- 대규모의 기업에서 러스트를 채택하는 것은 아직 이르다.