실습 프로젝트를 통해 일반적인 러스트 개념이 어떻게 활용되는지 살펴보자.
let, match, 메서드, 연관 함수, 외부 크레이트(external crates) 등의 활용 방법
이번 프로젝트는 cargo를 이용할 것이다.
$ cargo new guessing_game
$ cd guessing_game
첫 번째 명령어는 guessing_game이라는 프로젝트를 생성한다.
두 번째 명령어는 프로젝트 파일로 이동한다.
앞에서 보았듯이 cargo run을 해보면 Hello, world!를 출력하는 기본 프로그램이 작성되어 있다.
프로그램의 첫 부분에서는 사용자 입력 요청, 입력값의 처리 후 입력값이 개대하던 형식인지 검증해야 한다.
use std::io;
fn main()
{
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
여기서 살펴봐야 할 점이 몇 가지 있다.
C/C++에서는 라이브러리를 불러올 때 import를 사용한다.
하지만, rust에서는 use를 사용한다.
기본적으로 러스트는 모든 프로그램의 스코프로 가져오는 표준라이브러리에 정의된 아이템 집합을 가지고 있는데, 이 집합을 프렐루드라고 한다.
원하는 타입이 프렐루드에 없다면 use문을 사용해 명시적으로 타입을 가져와야 한다.
이 프로그램의 use std::io에서는 입출력과 관련된 기능을 제공한다.
입력값을 저장할 변수(variable)를 생성하는데, 특이한 점이 있다.
mut라는 키워드가 들어가있는데 이는 가변(mutable) 변수라는 것을 의미한다.
러스트에서는 기본적으로 불변(immutable) 변수로 선언되는데, 이 말은 한 번 값을 쓰면 그 뒤로는 값을 안 바꿀 것이다라는 뜻이다.
만약, 러스트에서 값을 언제나 바꿀 수 있는 변수로 선언하려면 mut 키워드를 사용해야 한다.
let apples = 5; // 불변
let mut bananas = 5; // 가변
여기서는 let mut guess가 String::new로 선언이 되어 있고, 이 뜻은 guess는 새로운 String 인스턴스가 묶인다는 뜻이다.
그 다음 줄은
io::stdin().read_line(&mut guess)
로 되어 있는데, 먼저 stdin은 입력을 받는다는 뜻인데, 여기서 &는 reference parameter이고 기본적으로 불변이다.
하지만, guess는 가변 변수이기 때문에 &를 가변으로 바꿔야 한다.
그래서 read_line에 &mut guess가 들어갔다.
.으로 연결된 함수들은 사실 한 줄로 이어서 쓸 수 있지만, 가독성을 위해 줄을 구분해 쓰는 것이 좋다.
read_line은 Result값을 반환하는데, Result는 enum이라고 하는 열거형이다.
즉, 여러 개의 가능한 상태 중 하나의 값이 될 수 있는 타입을 의미하고 이런 가능한 상태 값을 variant라고 한다.
Result의 variant는 Ok 또는 Err이다. Ok는 처리가 성공했음을 의미하고 Err는 처리에 실패했고 예외를 발생시킨다.
except 메서드는 인수로 넘겨 받은 메시지를 예외가 발생했을 때 출력되도록 한다.
물론 except 처리를 안하더라도 프로그램은 컴파일되지만 컴파일러가 경고해주고 올바르게 작성된 코드라고 보기 어려워진다.
마지막 줄에서는 {}가 있는 것을 볼 수 있는데 이것을 자리표시자(placeholder)라고 한다.
{}를 어떤 위치에 값을 자리하도록 하는 작은 집게발이라고 생각하면 쉽다.
어떤 것은 {} 안에 넣고, 어떤 것은 C언어처럼 쓰기도 한다.
첫 번째 경우는 변수의 값을 그대로 출력할 때 사용하고,
두 번째 경우는 어떤 식 연산의 결과를 출력할 때 사용한다.(이때 연산을 표현식 이라고 한다.)
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
이 코드의 결과는 x = 5 and y + 2 = 10을 출력한다.
비밀번호는 랜덤함수를 사용해서 구현하는 것이 일반적이지만 안타깝게도 러스트는 기본적인 랜덤함수가 없다.
대신 이 기능을 가지고 있는 rand 크레이트를 제공한다.
크레이트: 러스트 코드 파일들의 모음
우리가 만들고 있는 프로젝트: 바이너리 크레이트
다른 프로그램에서 사용되기 위한 용도: 라이브러리 크레이트
카고에서는 직접 다운로드할 필요 없이 Cargo.toml의 dependencies 부분에 그냥 입력하면 된다.
[dependencies]
rand = "0.8.5"
카고에서는 버전명에 유의적 버전(Semantic Versioning)을 사용한다.
이뜻은 0.8.5는 실제로 ^0.8.5의 축약형을 의미하고, 최소 0.8.5 이상이지만 0.9.0 아래의 모든 버전을 의미한다.
이 방식을 사용하면 직접 Crates.io에서 데이터 복사본인 레지스트리에서 해당 의존성이 필요로 하는 모든 크레이트를 최신 버전으로 가져온다.
카고는 Cargo.lock을 통해 프로젝트 빌드할 때 기준을 만족하는 모든 의존성의 버전을 확인하고 기록하기 때문에 나중에 프로젝트를 빌드할 때는 Cargo.lock 파일이 존재하는지 확인하여 그 안의 명시된 버전들만 사용한다.
이 뜻은 어디서 실행하던 상관없이, 개발자의 환경과 동일하게 세팅할 수 있음을 의미하고 명시적으로 업그레이드 하지 않는 이상 원래 개발하던 환경과 동일한 버전의 크레이트를 사용한다.
만약 버전을 올리고 싶다면 다음의 명령어를 사용한다.
$ cargo update
다만, 0.9.x 버전을 사용하고 싶다면 Cargo.toml 파일의 dependencies 부분을 다음과 같이 수정해야 한다.
[dependencies]
rand = "0.9.0"
use를 사용하여 rand를 사용한다고 명시한 다음 난수 생성기를 구현한다.
먼저 코드를 보면 다음과 같다.
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number});
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line!");
println!("You guessed: {guess}");
}
먼저 난수 생성기는 rand::thread_rng()를 통해 호출한다. 이 방식은 운영체제가 시드를 정하고 현재 스레드에서만 사용되는 난수 생성기를 만든다.
gen_range는 난수의 범위를 정할 때 사용하고, 표현식은 start..=end다. 위 경우에서는 1 ~ 100 사이의 숫자를 표현한다.
일단 코드부터 보자.
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// -- 생략 --
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
std::cmp::Ordering 타입을 가져오고 Ordering은 열거형이다. 이 타입은 Less, Greater, Equal 이라는 variant를 가지고 있다.
cmp는 두 값을 비교하여 비교하고자 하는 값의 참조자를 받는다.(즉, 레퍼런스로 받아야 한다는 점이다.)
match는 여러 갈래(arm)들로 이루어져 있고, 하나의 갈래는 하나의 패턴과 match 표현식에서 주어진 값이 패턴과 맞는다면 실행할 코드로 구성되어 있다.
러스트는 갈래를 순서대로 확인한다.
하지만 지금의 코드에서는 에러가 발생할 것이다.
당연히 비교할 때에는 타입이 동일해야 한다. 지금 문제는 secret_number는 숫자인데, guess는 String::new()라는 점이다.
즉, 지금 해야할 것은 guess를 정수형으로 받는 것이고 개선한 코드는 다음과 같다.
// --생략--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!);
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
러스트는 섀도잉을 허용하는데 그 뜻은 같은 두개의 고유 변수를 만들도록 강제하기 보다는 변수 이름을 재사용하도록 해준다는 것이다.
일단 문자열은 맨 뒤에 개행 문자(\n)가 추가되어 있다. 그렇기 때문에 숫자형으로 바꾸기 위해서는 .trim() 사용하여 정렬해야 한다.
parse는 문자열을 다른 타입으로 바꿔주는 역할을 한다. 이 경우에도 u32 타입으로 변경하도록 해주었다.
이 예제를 통해 한 가지 더 알 수 있는 사실이 있는데, 기본적인 숫자는 u32 타입이라는 점이다.
이제 cargo run을 하면 실행되는 데 조금 아쉬운 점이 있다.
정답을 맞을 때까지 실행하고 싶은데, 한 번만 실행되고 끝난다는 점이다.
반복문을 활용해 이 점을 해결할 수 있다.
rust에서는 loop을 통해 무한 루프를 제공한다.
하지만 문제는 이것을 사용하면 프로그램이 종료되지 않는다는 점이다.
// --생략--
println!("The secret number is: {secret_number}");
loop {
println("Please input your guess.");
// --생략
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!")
}
}
}
물론 quit을 입력하면 프로그램은 종료되기는 하지만, 일반적인 경우는 아닐 것이다.
사용자가 정답을 맞췄을 때 게임이 종료되도록 break 문을 추가합니다.
// --생략
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
이렇게 하면 loop를 탈출하면서 프로그램이 정상적으로 종료된다.
parse가 입력 문자를 정수로 바꾸지 못하면 에러가 발생한다. parse는 Result 형이고 동작을 좀 더 다듬어 보자.
// --생략
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
// -- 생략
이렇게 하면 올바른 입력을 받을 때까지 반복 실행이 된다.
Err 쪽을 보면 _이 있는데, 이것은 캐치올이라고 하고 모든 값에 매칭이 될 수 있다.
이 예제에서는 숫자가 아니면 무슨 값이 있던지 모든 Err를 매칭하도록 설정한 것이다.
이후에 continue를 실행하여 loop의 다음 반복으로 넘겼다.
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}