00. 첫 걸음 - Rust

Shawn Kang·2023년 2월 13일
0

Rust

목록 보기
1/2
post-thumbnail

개요

퇴직(?) 후 할 게 없어서 개강까지 남은 시간동안 잠깐 Rust를 찍먹해보기로 했다. 공식 문서를 참고하면서 배운 내용을 간단히 정리하고자 쓰는 글이다.



설치

Rust 공식 홈페이지에서 작업 환경에 맞는 설치 파일을 다운로드하여 설치한다. 설치 경로를 변경할 수 있는데, Windows 기준으로 환경 변수에서 "RUSTUP_HOME""CARGO_HOME" 변수를 지정해주면, 설치 프로그램에서 해당 환경 변수를 인식하여 자동으로 설치 경로를 변경해준다.

추가로 나는 VS Code를 쓸 예정인데, Rust에서 공식적으로 지원하는 코드 분석 확장이 있다.



Hello, world!

모든 언어의 시작은 당연히 Hello, world!와 함께.

fn main() {
	println!("Hello, world!");
}

일단 C/C++ 계열 언어와 생김새가 상당히 비슷하다. 프로그램의 진입점으로 main 함수를 쓰는 것도 그렇고, 모든 라인의 끝마다 세미콜론이 붙는 것도 그렇다.

프로젝트가 아닌 단일 소스의 컴파일은 rustc source.rs 형식의 명령어로 하는 듯하다. 해당 명령어로 컴파일해서 .exe 파일을 생성하고 그걸 실행하는 식.



Cargo 사용

종속성 등 프로젝트 관리 전반에 쓰는 도구인 듯하다. 목적만 봐도 거의 사실상 반 필수로 사용될 것 같은 친구다.

프로젝트 생성

프로젝트 생성은 cargo new project_name 명령어로 진행한다. 프로젝트 생성 시 프로젝트 디렉토리에서 아래 두 파일을 발견할 수 있다:

  • /Cargo.toml
  • /src/main.rs

Cargo.toml 파일은 프로젝트 설정과 패키지 종속성을 정의하는 파일 같고, /src 디렉토리 내에서 코드를 짜면 되는 듯하다.

프로젝트 빌드

프로젝트 빌드는 프로젝트의 루트 디렉토리(대충 추측컨대 Cargo.toml이 존재하는 디렉토리)에서 cargo build 명령어를 사용한다. 다만 이 명령어는 위의 rustc source.rs의 경우처럼 소스를 컴파일하지만 실행은 하지 않는다. 대신 cargo run으로 컴파일 후 별도 명령어 없이 즉시 생성된 프로그램을 돌려볼 수 있다.

재밌는 점 하나, 기존 버전에서 별도의 코드 변경 없이 cargo run 명령어를 실행할 경우 Powershell에 컴파일 메시지가 뜨지 않는다. Git 마냥 소스 변경 내용을 Cargo에서 확인하고, 변경 없을 경우 컴파일 생략 후 바로 실행한다는 듯. 좀 쩐다. 이게 채-신 언어인가?

추가로 릴리즈 목적의 빌드를 위해서는 cargo build --release를 쓰면 된다. 릴리즈 빌드 시에는 컴파일에 시간을 더 쓰는 대신 최적화 과정이 붙어 프로그램의 실행 속도를 개선한다고 한다.

프로젝트 오류 확인

cargo check 명령어도 있는데, 이건 .exe 파일을 생성하지는 않고 컴파일이 가능한지 여부만 확인한다. 개발하면서 특정 시점에서 코드가 오류를 일으키는지 확인하고 싶을 때가 있을 거다. 그때마다 프로젝트를 컴파일하고 빌드하면 시간이 오래 걸리니, 컴파일은 생략하고 컴파일 가능 여부만 판단함으로써 불필요한 자원 소모를 방지하고자 하는 데 그 목적이 있다.

이건 여담이지만, Rust 문서 원문에서 이 명령어를 설명해 둔 부분을 잘 이해하지 못했다. 이게 컴파일만 하는 건지, 아니면 컴파일 후 .exe 파일까지 뽑아주는 건지, 잘 와닿지 않아서 구글링을 좀 해 봤다:

  1. 컴파일: 사용자가 작성한 코드를 컴퓨터가 이해할 수 있는 언어로 번역하는 일
  2. 빌드: 컴파일된 코드를 실제 실행할 수 있는 상태로 만드는 일
  3. 배포: 빌드가 완성된 실행 가능한 파일을 사용자가 접근할 수 있는 환경에 배치시키는 일
  4. 혹은 컴파일을 포함해 war, jar 등의 실행 가능한 파일을 뽑아내기까지의 과정을 빌드한다고도 함.

출처는 여기다. 근데 생각해보니까 우리 학교 CS 커리큘럼에 이런 걸 가르치는 과목이 있을까? 난 배운 적 없는데. 혹시 소프트웨어공학에서 이런 거 배우나? 음... 잘 모르겠다.



문법 맛보기

표준 라이브러리

use std::io;

와 같이 쓰면 불러올 수 있다. 눈길이 가는 부분은 세미콜론. C와는 좀 다르게 라이브러리를 불러오는 구문에도 세미콜론을 넣는다.

그리고 표준 라이브러리 중 모든 프로그램에 포함되는 가장 기초적인 몇 가지 요소들이 있는데, 이들을 Prelude라고 부른다고 한다. 아마 뭐 정수와 같은 가장 기본적인 자료형들이겠지...?

변수

let mut guess: String = String::new();

하나하나 분해해보자.

let 변수를 선언한다는 의미
mut 변수의 값을 바꿀 수 있도록 한다는 의미
= 좌변에 우변의 값을 'bind'한다는 의미

mut 없이 let으로만 변수를 선언할 경우, 초기화 후에는 값을 변경할 수 없게 된다고 한다.

입력, 참조와 예외 처리

io::stdin()
	.read_line(&mut guess)
    .expect("Failed to read line")

입력 자체는 io::stdin().read_line(buf) 메소드로 하는 것 같다. 추가로 ::는 Python, Java, Kotlin 등에서 쓰는 Class.method()의 그것이 아닐까 싶다.

다음으로, & 연산자가 눈에 띈다. C에서 익숙하게 보던 그거다. & 연산자는 참조 연산자로 특정 값을 메모리 공간에 여러 번 복사하지 않고도 코드 전반에서 편하게 공유할 수 있게 해 주는 기능이다. 그리고 Rust의 핵심이라고도 한다.

다만 이 단계에서는 자세한 건 생략하고, 참조 역시 변수 선언과 마찬가지로 mut 여부를 결정할 수 있으며 mut 키워드 없이는 기본적으로 불변이다, 라는 정도만 알고 있으면 된다고 한다.

추가로 read_line(buf) 메소드는 Result 객체를 반환한다고 한다. 대충 추측해보건데 파일을 읽어오는 작업을 시도한 후 이 작업의 성공 여부를 반환하는 것 같다. Result 클래스(열거형같기는 하지만)에는 expect(msg)라는 메소드가 붙어 있는데, read_line(buf) 메소드 실행에 실패했을 때 msg에 있는 메시지를 반환하며 프로그램을 종료한다고 한다.

또 재밌는 점, Result 객체를 어떤 형식으로든 사용하지 않으면 컴파일러에서 경고를 보낸다고 한다. 아마 느슨한 예외 처리를 경고하기 위한 조치가 아닐까 싶다.

출력

println!("You guessed: {guess}");

일단 아까부터 왜 출력 함수에 느낌표가 자꾸 붙는지 모르겠다. 생각보다 굉장히 신경이 쓰인다. 별도의 설명이 없는데 나중에 알려주겠지...?

그건 그렇다 치고, 출력 자체는 저렇게 하는데 아마 변수를 String에 직접 넣기 위해서는 중괄호를 활용하면 되는 것 같다.

외부 크레이트 사용

Rust는 패키지, 라이브러리를 Crate라고 부른다고 한다. 그리고 무작위 함수를 사용하기 위해 rand라는 크레이트를 불러 올 필요가 있다.

처음에 봤던 Cargo.toml 파일의 [depencencies] 부분을 수정해주면 된다:

[dependencies]
rand = "0.8.5"

그리고 cargo build하면, 지정한 크레이트가 불러와진다. 그리고 이렇게 불러오는 모든 크레이트들은 crates.io에서도 확인할 수 있다고 한다. 아마 Dart의 pub.dev와 유사한 역할인 것 같다.

또한 Cargo는 개발자가 별도로 지시하지 않는 한 크레이트의 버전을 함부로 올리지 않는다고 한다. 아마 버전 업 되면서 발생할 수 있는 호환성 문제를 방지하기 위함인 듯하다.

심지어 처음에 0.8.5로 설정한 rand 크레이트 버전을 0.9.0으로 올리고 다시 빌드할 경우 Cargo가 빌드를 거부한다.

match 구문과 Shadowing

let mut guess: String = String::new();

io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read line");

let guess: u32 = guess.trim().parse().expect("Please type a number!");

match guess.cmp($secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => println!("You win!")
}

match 구문

guesssecret_number의 값 비교를 위해 사용한 match 구문이다. 이게 정확히 어떤 유형의 구문인지는 잘 모르겠지만, 아무튼 대충 봐서는 Kotlin의 when (value) {} 절과 비슷해 보인다.

그리고 한 줄 코드를 중괄호 없이 쓸 수 있게 해 주는 것도 좋다. 사실 이거는 비교적 최근 언어라고 하면 대부분 다 지원해주지 않나 싶긴 하다.

Shadowing

그리고 처음에 String으로 선언했던 guess 변수를 u32로 재할당했다. 이렇게 기존에 선언된 변수에 다른 값을 할당하는 것을 Shadowing이라고 부른다고 한다.

다만 여기서 궁금한 점이 하나 생긴다. 지금은 가변하게 설정해 둔 변수에 값을 재할당하는 상황이다. 만약 처음에 불변하게 A 자료형으로 설정한 값에 새로운 B 자료형 값을 할당하는 게 가능할까? 그래서 무작위 정수를 정하는 코드를 아래와 같이 바꿔보았다:

// Before
let secret_number = rand::thread_rng().gen_range(1..=100);
    
// After 1: 같은 i32 자료형으로 변경
let secret_number = rand::thread_rng().gen_range(1..=100);
let secret_number = 81;

// After 2: 처음은 i32, 다음은 String
let secret_number = rand::thread_rng().gen_range(1..=100);
let secret_number = 81;
let secret_number = "Alpha";

일단 두 개의 After 코드 모두 컴파일은 된다. 근데 경고를 주긴 한다. 각각의 실행 결과는 아래와 같다:

  • After 1의 경우에는 secret_number가 81로 고정된다. 값도 정상적으로 바뀌는 것 같다.
  • After 2의 경우에도 secret_number를 출력하면 "Alpha"라고 잘만 뜬다.

그럼 대체 어떤 점에서 mut가 없으면 불변한 변수라고 말한 걸까?

// After 3: let을 빼고 재할당
let secret_number = rand::thread_rng().gen_range(1..=100);
secret_number = 32

let을 빼고 변수를 같은 자료형으로 재할당하면 컴파일이 안 되고 오류가 뜬다. secret_number를 가변하게 설정해 보라는 조언까지 같이 준다. 이렇게 세 코드를 보니 대충 어떤 의미에서 mut이 없는 변수가 불변하다는지 이해가 간다.

반복

loop {
	// To do
    break;
}

별 거 없다. 그냥 중괄호 안에 대충 코드 때려넣으면 무한 반복이란다. 반복 깨는 것도 익히 알려진 break 쓰면 된다.

잘못된 입력 대처하기

// Before
let guess: u32 = guess.trim().parse().expect("Please type a number!");

// After
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue
};

공식 문서에서 적어 둔 대로 코드를 바꿔보았다. 숫자가 아닌 값을 입력할 경우 프로그램을 처음부터 다시 시작하도록 하는 코드다.

근데 분명 guess의 자료형이 u32로 명시적으로 지정되어 있는데, 어떻게 continue라는 코드가 들어갈 수 있는지가 이해가 안 간다. match 문을 잘 모르는 상태에서 대충 보면, Ok(num)일 경우 num을 반환하고 아닐 경우 continue를 반환한다는 것처럼 읽히는데, 확실하게 알 수 있는 건 continueu32가 절대 아니다. 뭐 나중에 공식 문서에서 더 자세히 알려주겠지... 일단은 넘기자.



결론

Rust의 존재를 알게 된 계기가 소마 당시 멘토님 덕분이었는데, 그 분이 '얘는 목표가 런타임에서 오류를 가능한 한 제거하고 프로그램을 돌린다는 거다. 개발자 책임을 최대한 더는 거다. 그래서 타입 체크도 빡세고 다른 보편적 언어랑은 좀 느낌이 다르다...'라는 뉘앙스로 말씀하셨었다.

간단하게 튜토리얼만 해 보았는데도 그 말이 정말 맞다는 게 느껴졌다. 오류 및 경고 메시지가 굉장히 친절하고 어떻게 코드를 바꾸어야 할지도 제안해 준다. 타입 체크는 빡세겠지만, 퍼즐 푸는 느낌도 들어서 나름의 재미도 있다.

그리고 이거 쓰면서 느낀 또 다른 깨달음 하나는, 확실히 여러 언어를 배워 놓으면 다른 언어를 시작하기가 훨씬 더 편해진다는 점이었다. 이번에 잠깐 프론트엔드 개발하면서 Kotlin을 처음 만져봤는데, val value: Int처럼 변수 뒤에 콜론을 붙이고 자료형(함수의 경우는 반환형)을 쓰는 부분이나,

return when(intValue) {
	1 -> // To do
	2 -> // To do
	3 -> // To do
	else -> // To do
}

처럼 C 언어에서는 볼 수 없는 다양한 조건문과 스위칭 문의 형태들이 다른 일부 언어에서도 조금씩 공유되고 있기 때문이다.

배움의 소소한 재미를 느낄 수 있었던 3시간이었다.

profile
i meant to be

0개의 댓글