Rust 찍먹후기

2022년 7월 17일
Review

🤔 왜 Rust를?

언제 어떻게 알게 되었는지 기억이 나지 않는 언어지만, 추측해보건대 아마 WebAssembly 관련하면서 처음 들어본 것이 아닐까 한다. WebAssembly도 Rust도 어딘가에서의 컨퍼런스나 세미나에서 접한 지 한참이나 되었지만, 기술이 무르익고 필자도 받아들일 마음이 들기까지 오랜 시간이 걸린 것 같다.

WebAssembly는 Go로 해봐야지 하면서 극구 외면하고 있던 Rust를 어떤 글에 혹했는지 혹은 누군가의 뽐뿌였는지 모르겠지만, 지난 3월 불현듯 집어 들어 4개월이 넘은 시간이 지나서야 글로 정리할 기회를 가지게 되었다.

바쁜 와중에 짬이 날 때 진행하여 4개월이라는 기간치고는 알고 있는 것도 적고, 이미 잘 정리된 있는 글이 많아서 간단한 후기 정도의 컨셉으로 글을 작성해보고자 한다.

🔠 언어로서의 어떤 특징이 있나?

필자는 JavaScript를 주력으로 쓰고 있으며, 가끔 어디선가 접할 기회가 생기어 관심이 생긴다면 해당 언어를 배워보는 방식으로 언어들을 접했다. 필자는 필자가 마지막으로 접해본 컴파일 언어인 Go랑 비슷한 느낌으로 Rust를 바라보고 있었다. 그렇기 때문에 WebAssembly를 Go로 구현할 생각과 이에 대한 조사를 해봤고, 늘 Rust가 Go에 비해 더 나은 성능을 보여주는 것이 의문이었다. 이 부분에 대한 의문은 Rust를 배워가며 하나씩 해소될 수 있었다.

시스템 프로그래밍 언어로서 Rust는 JavaScript나 Python, Java 같은 언어를 사용할 때 생각하던 방식과는 확연히 달랐고 같은 컴파일 언어인 Go를 사용할 때와도 크게 다른 느낌이었다. Go는 간결한 문법으로 인해서 문법적 혼동이 먼저 왔을 뿐 사용하는 측면에서는 전반적으로 큰 어려움이 없었던 것 같다. Go와 달리 Rust는 보면서 과거 학부생 때 C나 C++을 배우던 시절이 주마등처럼 지나갔다.

Rust는 C/C++과 동등한 수준의 성능 및 안정성, 동시성을 목표로 하도록 설계되었다. 이에 JavaScript 등의 언어와 달리 메모리 직접 제어하는 로직들이 있으며, 이는 다른 언어에서 경험하지 못한 새로움을 준다. 그렇다고 C/C++과 같이 메모리 할당과 해제를 직접 하는 방식이 아닌, 선언된 인자(변수)에 소유권(Ownership)이 부여되고 이 소유권은 다른 인자에 할당되거나 블록의 끝에 도달하게 되면 해제되도록 구성되어 있다. 이 인자가 생성되고 소멸하기까지의 범위를 수명(Lifetime)이라고 하며, 각 인자 간의 수명을 보고 컴파일 단에서 참조할 수 있는 범위를 확인하여 댕글링 포인터(Dangling pointer)를 방지한다고 한다.

이외에 소유권으로 인자를 전달하는 방식 이외에, 다른 언어들의 포인터처럼 다른 변수를 참조할 수 있는 별도의 방법인 참조자(&)를 제공해주는데, 이 또한 앞서 말한 수명에 범위 안에서만 참조할 수 있다.

error[E0382]: use of moved value: `s1`
 --> src/main.rs:4:27
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |     println!("{}, world!", s1);
  |                            ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`,
which does not implement the `Copy` trait

또한 Rust는 몇 가지 열거형과 패턴 매칭을 통해 명확한 처리를 할 수 있도록 도와준다. Result<T, E>나 Option와 같은 열거형을 다른 언어의 switch와 유사하지만 강력한 match를 사용해서 처리하면, 가독성도 확보하면서도 예외 등의 처리를 손쉽게 할 수 있었다.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

JavaScript에서는 없어 TypeScript에서 감지덕지 사용하고 있지만 한편으로 늘 아쉬운 부분이 열거형이었다. 대부분의 경우에는 상수나 문자열만으로 표현이 가능했지만 좀 더 복잡한 처리를 하고 싶을 때 이 부분이 늘 아쉬웠다. Rust는 값에 구조체나 튜플 등을 정의할 수 있고 열거형에 메소드를 구현할 수도 있어서 다양하게 활용할 수 있는 점이 좋았다.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

시스템 프로그래밍 언어답게 당연히 동시성 관련한 기능을 지원한다. 저수준의 동시성을 지원하며, 일반적으로 고수준의 크레이트(라이브러리)을 가져와 사용하거나 필요하다면 구현해서 사용할 수 있다. 동시성을 지원하기 때문에 공유상태의 처리가 필요한데 이에 대한 지원도 좋다. 소유권에서 메모리를 관리했던 것과 유사하게 필요한 시점에 락을 걸고 별도의 해제를 하지 않아도 블록이 끝나는 지점에 해제된다.

use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

인상 깊었던 특징을 위주로 작성해 보았는데, 이외에도 많은 기능과 특징이 있으니 좋은 다양한 문서를 참조해보기를 추천한다.

🛠️ 제공하는 환경은 어떠한가?

학부생 때와 이후 업무를 통해 겪은 Python, Java와 JavaScript 모두 각기 다른 측면이기는 하지만 패키지 관리 도구, 커뮤니티 등의 훌륭한 에코 시스템을 가지고 있다고 생각한다. 무엇인가 막혔다거나 필요한 도구가 있을 때 검색을 통해 쉽게 잘 관리된 도구를 사용할 수 있었다. 반면, 지금은 달라졌을 수 있으나 처음 Go를 사용했을 때 이런 부분에 어려움이 있었다. 무엇인가 부실한 패키지 관리 도구와 검색을 통해 찾았지만, 상당수 만들어지다 만 듯한 라이브러리들이 많다고 느껴졌다. 그러다 보니 내부 저장소에 하나둘씩 포크 되어 가는 라이브러리들을 생겨났고, 관리 포인트가 늘어났었다.

Rust에 대한 학습을 시작하면서 가장 먼저 눈여겨본 부분이 그 부분이었다. Rust의 모듈들은 Crate라는 단위로 관리되는데 이를 배포 및 관리하는 공간인 create.io가 존재한다. NPM과 유사한 수준의 경험을 제공하고 있어서 사용하는 데 큰 불편함은 없었다. Rust는 기본으로 제공하는 기능 이외에는 라이브러리를 사용해야 하는데(임의의 값을 가져와야 하는 경우나 시간 관련된 부분들까지도), 지금까지 사용해왔던 라이브러리들의 배포일들이 오래되지 않았고 기능 또한 부족함이 없다고 느껴져서 잘 관리되고 있다는 느낌을 받았다.

create.io

create.io

또한 패키지 관리 도구인 Cargo는 프로젝트를 구성해주거나, 주석을 통해 문서를 생성해주며, 포매팅, 린팅 등 다양한 기능을 제공해준다. 특히 주석을 통한 문서에는 테스트를 포함할 수 있어서, 문서와 검증 등 다양한 작업을 함께 할 수 있어서 더욱 좋았다.

/// Block in blockchain has sequence, data, time, and so on.
#[derive(Debug, Serialize, Deserialize)]
pub struct Block {
    /// Sequence in blockchain
    pub index: usize,

    /// Hash from other properties
    pub hash: String,

    /// Previous block hash
    pub previous_hash: String,

    /// Timestamp when created
    pub timestamp: usize,

    /// Data in block
    pub data: Vec<Transaction>,

    /// Difficulty to generate block
    pub difficulty: usize,

    /// Nonce to generate block
    pub nonce: usize,
}

Document

Document

Rust가 컴파일 언어다 보니 컴파일러 또한 무시하지 못할 부분이라고 생각한다. Rust는 안정성을 목표로 하기에 컴파일러가 강력하고 최대한 많은 오류를 걸러내도록 작동해서 그런지 Go와 비교했을 때는 컴파일에 더 많은 시간을 소요하는 것으로 보인다. 다만 걸러진 예외나 에러 등에 대해 많은 정보를 제공해 주어 사용하는 데는 큰 불편함이 없었다.

error[E0308]: `match` arms have incompatible types
  --> src\main.rs:17:14
   |
15 |       let some_variable = match my_number {
   |  _________________________-
16 | |         10 => 8,
   | |               - this is found to be of type `{integer}`
17 | |         _ => "Not ten",
   | |              ^^^^^^^^^ expected integer, found `&str`
18 | |     };
   | |_____- `match` arms have incompatible types

IDE 지원 부분은 IntelliJ에서는 코드 하일라이팅, 자동 완성, 테스트, 패키지 관리 등 모든 부분에서 불편함은 없었다. IntelliJ를 사용하기 전에 먼저 VS Code로 사용을 시도해보았는데, 설정이 미흡했던 것인지 자동 완성 측면 등에서 불편하다고 느껴져 IntelliJ를 사용하게 되었다.

📖 학습하기에 쉬운가?

학습은 학습하기 좋은 환경인지와 학습할 내용이 수월한지에 대해서 나눠서 생각해 보고자 한다.

우선 학습하기 좋은 환경인가 봤을 때는 인기 있는 언어들에 비해서는 그리 만만한 환경은 아니라고 생각한다. 인기 있는 언어들은 학습에 대한 가이드 및 다양한 한글로 된 서적들이 있는 반면에 Rust는 그런 측면에서 부족한 것은 사실이다. 다만 한글화 된지 좀 시간이 지나긴 했지만, 한글 번역된 책인 The Rust Programming Language 가 존재하며, David MacLeod가 작성한 Easy Rust 라는 책과 Easy Rust 한글 비디오 도 있어서 시작하는 데 큰 도움이 되었다.

필자는 The Rust Programming Language를 통해 먼저 전반적으로 간단하게 적응한 후에 Easy Rust를 책과 가이드를 병행하는 방식으로 진행하였다. 두 책의 차이점은 The Rust Programming Language는 한글화된 지 시간이 지나 Rust 현재 버전에서 일부 예제들이 실행이 안 되었다. 또한 전반적인 구성이 Easy Rust에 비해 어렵게 작성된 느낌이었다.

반면 Easy Rust는 번역된 책은 아니나 이해하기 쉬운 단어로 작성되어 있고 한글 비디오 또한 제공되어 이해하기 수월한 편인 것 같다. 또한 문법적인 측면 외에 실제로 코드를 작성하면서 필요한 부분들도 같이 언급되어 좋았다. 다만, The Rust Programming Language와 달리 소유권 등의 개념에 대한 설명이 전반적으로 녹아들어 있어서 이 책을 처음부터 봤다면 별도로 검색이 필요했을 것 같다.

학습할 내용 자체는 JavaScript나 Python, Java 등의 언어에 비해서는 어려웠다고 생각한다. 같은 컴파일 언어인 Go보다도 쉽지 않았다. 과거 C/C++을 배웠던 기억이 없다면 비교할 상대가 없어서 더 어려웠을 수도 있을 것 같다. 다만, 이런 부분은 시간이 해결해 주었고 사용하다 보니 조금 익숙해지는 것 같았다.

물론 여전히 쉽지 않은 부분이 있는 것도 부정할 수는 없다. 그저 전체 목록을 돌며 서로에 영향을 끼칠 필요가 있는 코드를 작성했을 뿐인데 가변성 인자는 두 번 참조할 수가 없다보니 많이 헤맸다.

fn update () {
    //...
    for i in 0..points.len() {
        for j in 0..points.len() {
            let (p1, mut p2) = if i < j {
                // `i` is in the left half
                let (left, right) = points.split_at_mut(j);
                (&mut left[i], &mut right[0])
            } else if i == j {
                // cannot obtain two mutable references to the
                // same element
                continue;
            } else {
                // `i` is in the right half
                let (left, right) = points.split_at_mut(i);
                (&mut right[0], &mut left[j])
            };

            //...
        }
        // ...
    }
}

▶️ 실제로 사용해보자

사실 필자는 처음 Rust에 대한 책을 볼 때 생소하기는 하나 그저 눈으로 보고 예제를 조금 치거나 복사해서 실행해 보면서 몹시 어렵지 않고 볼만하다고 생각했다. The Rust Programming Language를 한 달여 간 보고 Easy Rust를 병행해가면서도 직접 작성해보지 않으니 도저히 체감할 수가 없었다. 또 하나의 Template Library 시리즈 대상으로 잡을까 하다가도 쉽사리 손이 가지 않았다.

이때 업무적으로 블록체인에 대해서 접할 기회가 있었고 다시 한번 개념적으로 이해가 필요하다는 생각이 들었다. 이전에 블록체인에 대해 개념적으로 궁금해서 찾아봤을 때 JavaScript로 블록체인 만들기 라는 글이 있었던 것이 떠올라 Rust로 이 글을 따라서 한번 구현해보고자 했다.

이 예제를 구현하면서 기본적인 문법, 비동기, 직렬화, 시스템 접근, 인증 및 HTTP, Socket 통신 등 다양한 경험을 해볼 수 있었다. 책을 통해 알지 못했던 어려움을 느꼈고, 이를 찾아서 해결하고 라이브러리를 선정하는 과정을 경험할 수 있었다. 업무에서 필요하다고 생각하는 부분들을 하나씩 실험해보며 사용했을 때 검증하지 못한 Persistence를 제외하고는 만족할 수 있었다.

이에 대한 전반적인 구현한 코드는 이 Blockchain Study 저장소에 작성해 두었다.

앞선 예제를 통해서 좀 더 자신감을 얻어서, 필자의 영역에서 사용할 수 있는 부분에 대해서 고민해보았다. 다양한 측면에서 사용할 수 있는 부분을 생각할 수 있겠지만, 필자가 수월하게 작성하면서도 눈으로 보일 수 있는 예를 생각해보았다.

이를 위해 캔버스에 많은 수의 파티클들을 무작위로 이동하는 예제를 구현해보고자 하였다. 렌더링 등의 기본 요소는 동일하게 동작하도록 작성하고 파티클을 생성하고 옮기는 부분만 각기 JavaScript와 Rust로 작성된 WebAssembly로 구현하였다. 두 코드 모두 최초 랜덤한 위치와 속도를 가진 파티클을 주어진 개수만큼 생성하고 캔버스가 렌더링 되는 시점마다 다음 위치로 이동하게 된다.

const PlaygroundWasmParticlesJsOnlyPixiTemplate: FC<PropsType> = props => {
  const { controlInfo } = props;
  const [dimensions, setDimensions] = useState<Dimensions>(null);
  const [points, setPoints] = useState<Point[]>([]);

  useEffect(() => {
    if (!dimensions || !controlInfo.size) {
      return;
    }
    setPoints(generatePointsByDimensions(controlInfo.size, dimensions));  }, [dimensions, controlInfo.size]);

  const handleUpdate = useCallback(
    (points: Point[]) => {
      points.forEach(point => point.update(dimensions, points));      return points;
    },
    [dimensions],
  );

  return (
    <PixiView onResize={setDimensions}>
      {dimensions && (
        <>
          <ReactPixiGrid dimensions={dimensions} />
          <ReactPixiParticles dimensions={dimensions} points={points} onUpdate={handleUpdate} />
        </>
      )}
    </PixiView>
  );
};
const PlaygroundWasmParticlesRustWasmPixiTemplateInner: FC<PropsType> = props => {
  const { controlInfo } = props;
  const { wasm } = useWasmContext();
  const [dimensions, setDimensions] = useState<Dimensions>(null);
  const [points, setPoints] = useState<Point[]>([]);

  useEffect(() => {
    if (!dimensions || !controlInfo.size || !wasm) {
      return;
    }
    setPoints(wasm.generate_particles(controlInfo.size, dimensions));  }, [dimensions, controlInfo.size, wasm]);

  const handleUpdate = useCallback(
    (points: Point[]) => {
      return wasm.update_particles(dimensions, points);    },
    [dimensions, wasm],
  );

  return (
    <PixiView onResize={setDimensions}>
      {dimensions && (
        <>
          <ReactPixiGrid dimensions={dimensions} />
          <ReactPixiParticles dimensions={dimensions} points={points} onUpdate={handleUpdate} />
        </>
      )}
    </PixiView>
  );
};

300개 파티클 정도에서는 JavaScript와 WebAssembly로 작성된 코드 모두 성능 저하 없이 60fps로 작동하는 것을 확인할 수 있었다.

  • JS with 300 Particles
  • WebAssembly with 300 Particles

파티클의 개수를 800개로 늘리자 WebAssembly로 작성된 코드는 동일하게 60fps로 작동하는 것을 확인할 수 있었던 반면에, JavaScript로 작성한 코드는 성능이 많이 떨어져 8fps로 작동하는 것을 확인할 수 있었다.

  • JS with 800 Particles
  • WebAssembly with 800 Particles

예제를 작성하면서 성능 차이가 크게 발생하지 않으면 어떻게 하지 하면서 노심초사했는데, 그런 걱정이 물색할 정도로 생각보다 큰 차이를 확인할 수 있었다. 언급된 예제는 Labs / Particles 에서 확인할 수 있다.

🚀 마치며…

지난 4개월간 이런저런 이유로 여유롭지 않은 시간이었지만 틈틈이 진행했던 내용이어서 부족하지만 한번 마침표를 찍고 갈 수 있음에 기쁨을 감출 수 없다. 더욱이 기대했던 부분까지 확인할 수 있어서 더욱이 기쁜 것 같다. 이 기쁨을 자양분 삼아 아직 더 많이 해야 할 것들, 더 만들어봐야 할 것들을 해나가고자 한다.

Recently posts
© 2016-2023 smilecat.dev