Rust의 시작점 main 함수

2020년 7월 20일 수정

C 언어의 시작점과 동일하게 Rustmain 이라는 이름의 함수가 시작점이 된다. 이 main 함수과 관련된 내용을 정리해보자.

main 함수

프로그램이 시작되는 지점은 main 이라는 이름을 가진 함수다.

fn main() {
    ...
}

말 그대로 메인 함수다. 독자 실행형 소프트웨어라면 반드시 이 함수가 시작점에 해당한다. 이 정도면 왠만한 경우에도 별 무리 없는 시작점으로 활용 가능한 함수다.

리턴 타입이 없다?

C 언어에 익숙하다면 main 함수는 리턴 타입을 가질 수 있다는 것을 알고 있을 것이다.

int main() {
    return 0;  // Ok
}

참고로 유닉스 혹은 유닉스에서 유래한 OS에서 명령의 실행 결과는 전통적으로 0이 성공을 의미한다.

그런데 특이하게도 Rust 가이드 글들의 main 함수는 하나같이 아래와 같은 모양만 보인다.

fn main() {
    ...
}

리턴 타입도 없으므로 당연히 return 문도 사용하지 않는다.

사실 Rust의 main 함수는 별 다른 오류가 없었다면 성공한 것으로 간주하고 정상(0) 코드를 리턴한다. 그래서 아무 리턴이 없었더라도 스크립트에서도 성공으로 간주하여 동작하는 것을 볼 수 있다.

아래는 && 을 이용해 위의 리턴이 없는 main 함수를 가진 코드를 빌드한 someproj 라는 실행 파일로 시험한 예이다.

$ target/debug/someproj && echo "Ok"
Ok

&& 은 앞의 명령이 성공하면 그 뒤의 명령을 실행시키는 오퍼레이터다. 즉 someproj 명령이 성공(0)을 리턴했다는 말이다.

그렇다면 반대로 뭔가 런타임 에러가 발생되는 케이스는 어떻게 될까? 일부러 아래와 같은 코드를 작성해봤다.

use std::fs::File;

fn main() {
    File::open("unknown_file.txt").expect("File not found");
}

없는 파일을 열려고 하니 에러가 발생할거다. 그래서 expect 가 이걸 붙잡고 오류를 내면서 종료시킬 것이다.

$ target/debug/someproj
thread 'main' panicked at 'File not found: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

그리고 당연히 이 결과는 실패한 것으로 간주된다.

$ target/debug/someproj && echo "Ok"
thread 'main' panicked at 'File not found: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ echo $?
101

Ok 가 표시되지 않았다. 그리고 실제로 리턴된 결과도 101이었다. 참고로 에서 위처럼 $? 를 이용해 마지막으로 실행시킨 커맨드의 실행 결과를 알 수 있다.

리턴 타입이 있을 수도 있다

위에서는 없다고 했는데 사실 main 함수는 리턴 타입을 가질 수 있다. 다만 특이하게도 유닉스 스럽지 않게 Result 타입을 리턴하도록 강제된다.

fn main() -> Result<(), i32> {
    // ...
    Ok(())
}

위의 코드는 성공으로 간주하고 끝난다. 유닉스 기반의 소프트웨어들은 모두 성공 시 0을 리턴하는 게 관습처럼 정해져있기 때문에 성공시에는 별 다른 값 없이 Ok(()) 만을 리턴하면 알아서 0을 리턴하는 것으로 보인다.

$ target/debug/someproj && echo "Ok"
Ok

예상대로 문제 없이 동작한다.

자 그렇다면 에러를 리턴해야 하는 상황은 어떨까? Result 타입을 사용하므로 에러일 때는 당연하게도 Err() 을 리턴하면 될 것이다.

fn main() -> Result<(), i32> {
    // ...
    Err(1)
}

위 코드가 실행되면 Error: 1 이라는 것이 화면에 표시되면서 종료되는 특징이 있다. 어쨌든 이번에도 셸로 명령어 연결을 시도해보자.

$ target/debug/someproj && echo "Ok"
Error: 1

이번에는 Ok 가 화면에 찍히지 않는다. 즉 someproj 가 에러를 리턴하고 종료되었다는 말이다. 아마도 제대로 이해한 것 같다.

셸에서 리턴 코드를 확인해보면 동일하게 1이 리턴된 것을 알 수 있다.

$ target/debug/someproj
Error: 1
$ echo $?
1

Result 타입은 generic이기 때문에 리턴되는 타입 자체가 유동적이다. 아래와 같이 에러를 리턴하는 방식도 쓸 수 있다.

use std::io::Error;

fn main() -> Result<(), std::io::Error> {
    // ...
    Err(Error::from_raw_os_error(1))
}

이 외에 모듈에서 정의된 다양한 에러타입도 사용되는 것 같다.

파라미터(Parameter or Arguments)

다시 C 언어와 비교해보자. C 언어 main 함수의 기본 형태는 대체로 아래와 같은 식으로 arguments 목록을 얻기 위한 매개변수를 정의한다.

int main(int argc, char *argv[]) {
    // ...
}

하지만 Rust의 main 함수 예제에선 저런 파라미터 처리를 위한 매개변수를 전달받는 예제를 전혀 볼 수 있다.

이유는 단순하다. 파라미터(arguments) 전달 방식이 다르기 때문이다.

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

std::env::args 를 이용해 파라미터(arguments)를 받아올 수 있다. 실행시켜보면 아래와 같은 결과를 얻을 수 있다.

$ target/debug/someproj param1 param2 param3
["target/debug/someproj", "param1", "param2", "param3"]

첫 파라미터는 자기자신이고, 나머지는 공백을 기준으로 나눠진 파라미터 리스트(Vec)로 얻을 수 있다. 다른 언어에서 다뤄봤다면 매우 익숙하게 느껴질 것이다.

참고로 저렇게 빌드해서 실행하는 게 귀찮다면 Cargorun 커맨드에서도 파라미터를 줄 수 있으니 멀리 돌아가지 않아도 된다.

$ cargo run param1 param2 param3
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/someproj param1 param2 param3`
["target/debug/someproj", "param1", "param2", "param3"]

어쨌거나 파라미터를 벡터 타입으로 받아왔기 때문에 갯수나 각 파라미터를 얻는 과정이 어렵지는 않을 것이다.

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("Total {} arguments", &args.len());
    for param in &args {
        println!("{}", param);
    }
}

굳이 실행 결과가 필요할지는 의문이지만 대충 이렇게 된다.

$ target/debug/someproj param1 param2 param3
Total 4 arguments
target/debug/someproj
param1
param2
param3

뭐 아마도 이걸 그대로 쓰기 보다는 별도의 옵션 처리를 도와줄 패키지를 이용하는 편이 더 많겠지만 기본적인 지식은 알아두는 편이 좋을 지도 모르겠다.