kakasoo

1. 러스트 학습 시작 전 기본 개념들 정리 본문

프로그래밍/Rust

1. 러스트 학습 시작 전 기본 개념들 정리

카카수(kakasoo) 2024. 3. 24. 21:12
반응형

모던 언어 Modern languages

  • 변수들은 기본적으로 불변입니다.
  • 타입 추론을 합니다.
  • 타입 세이프티를 강조합니다.
  • 반환 유형을 가진 함수를 사용합니다.
  • 여러 프로세스, 스레드에서 동시에 실행될 수 있는 쉬운 방법을 제공합니다.
  • 프로세스간 통신을 위한 쉬운 방법을 제공합니다.
  • 함수형 프로그래밍을 강조합니다.
  • 세미콜론을 끝에 필요로 하지 않습니다.
  • REPL을 제공합니다 (컴파일 없이 즉석에서 코드를 실행해서 바로 결과를 알 수 있는 방식)
  • 대부분을 정적 타입으로 선언합니다.
  • 복잡하고 장황하지 않은 깨끗하고 우아한 구문을 가지고 있습니다.

일반적으로 모던 언어라고 하면 말하는 장점들은 위와 같다.

 

모던 시스템 프로그래밍 언어

Rust는 런타임이나 가비지 컬렉터 없이도 엄청나게 빠르고 효율적인 메모리를 자랑합니다. - 공식 문서

 

메모리 안정성과 스레드 안정성을 보장해주는 이 두 가지는 러스트를 ‘메모리 안전 시스템 언어'로 만들어준다.

  • 풍부한 타입 시스템
  • 소유권 모델

크로미움의 심각한 보안 이슈의 70%는 메모리 안전 문제이며, 이는 마이크로소프트에서도 동일한 상황이었다.

이런 세계 최고의 팀들도 안전한 C, C++ 코드 작성에 실패할 정도로 메모리 관리의 난이도는 악랄하다.

따라서 메모리를 안전하게 관리하기 위해서 고수준 언어들은 가비지 컬렉터를 사용하여 메모리 안전을 보장한다.

하지만 놀랍게도 이 언어들과 달리 ‘모던 언어’로서 러스트는 가비지 컬렉터 없이 메모리 안전을 보장한다.

 

모던 “시스템” 언어로서의 러스트

  • 다른 언어와 달리 러스트에서는 null과 포인터 없다.
    • 대신 러스트는 존재하지 않는 값을 표현하기 위해 아주 풍부한 타입 시스템을 갖추고 있다.
    • 따라서 러스트에서는 포인터가 없는 참조 또는 null 포인터는 예외가 아니다.
  • 적절하게 오류를 처리하게 하기 위해 Exception 대신 타입 시스템을 사용한다.
  • 카고(cargo)라고 하는 훌륭한 패키자 관리자는 자바스크립트의 npm과 동일하게 동작한다.
  • 데이터 레이스 (다른 곳에서 읽을 가능성이 있는 어떤 메모리 위치에 쓰기 작업을 하는 것) 가 발생하지 않는다.

 

Rust 설치하기

Install Rust

 

Install Rust

A language empowering everyone to build reliable and efficient software.

www.rust-lang.org

$ curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | sh
1) Proceed with standard installation (default - just press enter)
2) Customize installation
3) Cancel installation
> Enter

위 명령어를 치고, 이후 나오는 스크립트에서 엔터를 눌러 기본 설치를 실행하면 된다.

이는 MacOS 기준이다.

 

$ rustc --version # 러스트 컴파일러
$ cargo --version # 러스트 패키지 매니저
$ rustup --version # 툴체인 인스톨러

설치가 완료되면 각각의 버전을 확인하여 cli에서 해당 명령어들을 인식하는지 체크한다.

 

vscode 개발 환경 세팅

vscode 확장에서 ‘rust’를 검색하여 ‘extesions for rust’와 ‘rust language support for vscode’라 써있는,

rust, 그리고 rust-anlayzer를 설치한다.

우리가 이 확장을 설치하는 이유는 아래의 기능들을 기대하고 있기 때문이다.

Features

 

카르고 (Cargo), Rust 패키기 관리자

$ cargo new example # example 프로젝트를 생성한다.

‘example’ 이라는 이름의 프로젝트를 생성한다. ( = 내부에는 git 저장소가 초기화된 상태로 존재한다. )

모든 Rust 프로젝트의 루트에는 cargo.toml 이라는 파일이 존재한다.

이 파일에는 프로젝트의 설정 값으로, 메타데이터와 컴파일러 셋팅, 그리고 의존성 목록들을 명시할 수 있다.

( 러스트에서는 패키지를 ‘크레이트 ( = Crate)’ 라고 부르기 때문에 이제부터 이 글에서도 크레이트라고 부른다. )

 

# example/cargo.toml

[package]
name = "example"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at <https://doc.rust-lang.org/cargo/reference/manifest.html>

[dependencies]

TOML의 문법은 주로 키 = 값 쌍, [섹션 이름], 그리고 #(주석)으로 이루어져 있다. TOML의 문법은 다소 INI 파일의 문법과 유사하지만 형식적인 사양을 포함하고 있다.

 

Rust 패키지, 크레이트 (Crate)

crates.io: Rust Package Registry

 

crates.io: Rust Package Registry

 

crates.io

 

‘크레이트’에 가장 중요한 레지스트리는 creates.io 사이트에서 가져올 수 있다.

이 페이지에서 가장 많이 다운로드된 크레이트를 볼 수 있는데, 이 중 가장 많이 다운로드된 ‘rand’를 설치해보자.

 

Usage

Add this to your Cargo.toml:

[dependencies]
rand = "0.8.4"

 

  • 위 코드는 ‘rand’ 문서에서 가져온 것으로 Usage 섹션에 해당한다.

다운로드하고 싶은 크레이트에서 Usage 부분을 보고 패키지명과 버전 값을 가져와서 dependencies에 넣자.

( vscode에서 toml 파일에 컬러 하이라이트를 넣고 싶다면 Even Better TOML 확장을 설치하자 )

  • 전체 버전을 명시하지 않아도 사용 가능하며, 마우스만 올려도 사용 가능한 버전을 확인 가능하다.
  • 이는 creates 라는 vscode 확장인데 따로 설치하지 않더라도 Rust 확장만 설치했음 딸려오게 된다.

 

프로젝트 빌드와 실행

$ cargo build
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.17
   Compiling libc v0.2.153
   Compiling getrandom v0.2.12
   Compiling rand_core v0.6.4
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling example v0.1.0 (/Users/kakasoo/Desktop/github/rust-study/example)
    Finished dev [unoptimized + debuginfo] target(s) in 1.15s

cargo build 명령어를 치면 프로젝트의 의존 라이브러리를 모두 불러와 컴파일하고 연결 (link)한다.

컴파일과 링킹이 모두 완료되면 바이너리, 즉 실행 파일을 출력한다.

 

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/example`
Hello, world!

cargo run 을 입력하면 프로젝트 폴더에서 프로젝트를 실행시킨다.

여기서는 방금 만든 프로젝트기 때문에 src/main.rc 의 파일이 컴파일되서 ‘Hello, world’가 출력되었다.

 

// example/src/main.rc
fn main() {
    println!("Hello, world!");
}

 

cargo-expand

$ cargo install cargo-expand

cargo-expand를 설치하고 나서 cargo expand 명령어를 사용할 수 있다.

cargo-expand가 무엇인지에 대한 설명은 일단 패스하고, 이 명령어에서 에러가 날 경우 그 의미만 설명한다.

아마도 에러가 난다면 ( 버전이 업데이트되면 안날 수도 있다 ) ‘toolchain is not installed’ 에러일 것이다.

cargo로 Rust 코드를 빌드하고 나면 3가지의 다른 채널을 통해 릴리즈되는데 기본 값은 Stable 이다.

  • Stable : 현재 지원되는 버전으로, 이 언어의 안정화 버전을 의미한다.
  • beta : 안정화 버전을 위한 일종의 후보자
  • nightly : 대부분의 개발이 이루어지는 최신 버전으로, 가장 최신이지만 다소 불안정할 수 있다.

만약 확장을 사용하다가 nightly 관련으로 에러가 난다면 최신 버전의 기능이 필요하다는 의미인데,

이 경우에는 툴체인 (컴퓨터 또는 시스템의 제품을 만들 때 필요한 프로그램 개발 도구들의 집합)이 필요하다.

앞서 rustup은 툴체인 인스톨러라고 한 줄로 짧게 설명을 했는데, rustup은 이 시점에 필요해진다.

 

$ rustup toolchain list
> stable-aarch64-apple-darwin (default)

rustup toolchain list로 현재 설치된 툴체인을 볼 수 있고, 여기서 stable인 것을 체크할 수 있다.

그러면 현재 필요로 하는 툴체인과 ( 아마도 nightly 버전을 필요로 할 수 있다 ) 현재 채널이 다른 것을 확인하고,

 

$ rustup toolchain install toolchain_name

다른 툴체인을 rustup toolchain install 명령어로 설치해줘서 오류를 해결할 수 있다.

 

수동 메모리 관리의 기본 개념

  • The Stack : 각각의 함수에 의해 생성된 변수들을 저장하는 프로세스 메모리 영역
  • The heap
  • Pointers
  • Smart Pointers

 

스택 The Stack

각각의 함수에 의해 생성된 변수들을 저장하는 프로세스 메모리 영역

 

각 함수의 메모리 영역을 스택 프레임이라고 한다.

여기에 지역 변수들이 저장되고 모든 함수 호출에 대한 새로운 스택 프레임이 현재 프레임 위에 생성된다.

이 때 스택 프레임을 생성한 함수만 여기에 접근할 수 있고, 이것이 함수의 범위를 결정한다.

스택에 있는 모든 변수의 크기는 컴파일 시에 미리 알려져야 한다.

만약 스택에 배열을 저장하고 싶다면 배열의 크기를 명시해야 하며 스택이 종료되면 스택 프레임도 해제된다.

이는 메모리 할당에 대해 걱정할 필요 없이 알아서 관리된다는 것을 의미한다.

 

힙 The Heap

힙은 자동으로 관리되지 않는 프로세스 메모리 영역

 

사용하고 나면 수동으로 메모리를 해제해야 하며 그렇지 않을 경우 메모리 누수로 이어질 수 있다.

힙은 크기의 제한이 없기 때문에 스택과 달리 방대한 데이터를 저장할 수 있고 시스템의 물리적 크기 제한만 있다.

또한 힙은 프로그램의 모든 위치에서, 모든 함수에 의해, 힙에 접근할 수 있다.

다만, 힙은 과도한 비용이 들기 때문에 힙에 할당은 가급적 피해야 한다.

만약 힙에 수 많은 정보들을 저장하고 해제를 반복하면 힙 공간은 파편화되고,

새로운 위치에 필요한 공간을 효율적으로 찾기가 어려워지기 때문에 프로그램의 성능이 저하되는 원인이 된다.

즉, 할당과 해제가 반복될 때마다 다음 번 할당은 점점 새 공간을 찾기 어려워지며 성능이 저하된다.

 

어떤 함수 A에서 힙에 데이터 B를 저장해 사용할 경우

stack A가 생성되고 거기서 Pointer B를 생성한다.

함수는 힙 공간에 데이터를 저장하고 그 주소값을 받아 Pointer B에 저장한다.

추후 해당 데이터를 접근하고 싶으면 포인터를 참조하여 그 주소에 저장되어 있는 값을 얻을 수 있다.

마찬가지로 포인터도 스택에 저장되는 변수이니 만큼 컴파일 시점에 미리 크기를 알아야 하는데,

포인터의 크기는 컴파일 시점에 미리 알 수 있으며, CPU 구조에 따라 다르지만 보통 32비트나 64비트다.

 

함수 A가 끝날 때 B를 해제하지 않은 경우

힙 공간은 언제 어디서나 접근할 수 있다고 했지만 이는 주소를 알고 있는 경우에 해당한다.

Pointer B는 A에 저장된 변수였기 때문에 A 함수가 끝나면 스택 프레임은 저장된 메모리를 모두 반납한다.

이 때, 더 이상 B 값이 저장된 주소를 접근할 방법이 없어 B는 프로그램이 모두 끝날 때까지 접근 불가능하다.

이런 사용할 수 없는 메모리 점유를 메모리 누수 ( = Memory Leak ) 라고 한다.

 

스마트 포인터

FUNCTION stack_and_heap {
	INTEGER d = 5
	POINTER e = ALLOCATE INTEGER 7
	DEALLOCATE e 
}

위는 이해를 돕기 위해 작성한 의사 코드로, 함수가 끝날 때 메모리 해제 ( DEALLOCATE ) 를 진행하고 있다.

이는 간단한 코드지만, 방대한 코드가 있는 경우에는 휴먼 에러가 발생하기 쉽기 때문에,

고수준 언어에서는 가비지 컬렉터를 따로 두어 메모리가 누수되는 경우를 막는다.

하지만 과도한 런타임은 시스템 언어에서는 선택사항이 아니며 너무 무겁기 때문에 Rust에서는 구현되지 않았다.

그래서 모던 C++와 러스트에서는 ‘스마트 포인터’ 라는 것을 사용한다.

스마트 포인터는 일종의 포인터에 대한 래퍼로, 포인터가 범위를 벗어났을 때 그것이 가리키는 메모리를 해제한다.

새로운 의사 코드로는 아래처럼 작성할 수 있겠다.

 

FUNCTION stack_and_heap {
	INTEGER d = 5
	POINTER e = SMART_POINTER(7) // 범위를 벗어날 때 알아서 해제되길 바라고 작성하는 포인터 개념
}

이렇게 작성해두면, 함수의 스택 프레임에 변수가 올라갈 때 스마트 포인터가 값으로 올라가게 될 것이고,

스택 프레임이 해제될 때 스마트 포인터가 해제되면서 약속했던대로 포인터의 메모리를 해제하게 된다.

반응형