Result 로 복구 가능한 에러 다루기
대부분의 에러는 프로그램 전체를 멈출 정도로 심각하지는 않습니다. 함수가 실패하더라도, 그 이유를 해석하고 대응하기 쉬운 경우가 많기 때문입니다. 예를 들어 파일을 열려고 했는데 파일이 존재하지 않아 실패했다면, 프로세스를 끝내는 대신 파일을 새로 만들고 싶을 수도 있습니다.
2장의 “Result 타입으로 발생 가능한 실패 처리하기” 절에서 본
것처럼, Result enum은 Ok 와 Err 두 variant를 가진다고 정의되어 있습니다.
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T 와 E 는 제네릭 타입 매개변수입니다. 제네릭은 10장에서 더 자세히 다룹니다.
지금은 T 가 성공했을 때 Ok variant 안에 들어가는 값의 타입을 뜻하고, E 는
실패했을 때 Err variant 안에 들어가는 에러 값의 타입을 뜻한다는 것만 알면 됩니다.
Result 가 이런 제네릭 매개변수를 가지기 때문에, 우리는 성공값과 에러값이 서로 다른
여러 상황에서 Result 타입과 그 위의 함수들을 유연하게 사용할 수 있습니다.
실패할 수 있는 함수가 Result 값을 반환하는 경우를 호출해 봅시다. 목록 9-3에서는
파일을 열려고 시도합니다.
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
File::open 의 반환 타입은 Result 입니다. 여기서 제네릭 매개변수 T 는
성공값 타입인 std::fs::File 로 채워지고, 에러값에 쓰이는 E 는 std::io::Error
로 채워집니다.
이 반환 타입은 File::open 호출이 성공하면 읽기나 쓰기에 사용할 수 있는 파일
핸들을 반환할 수 있다는 뜻입니다. 동시에 호출이 실패할 수도 있습니다. 예를 들어
파일이 존재하지 않거나, 우리가 접근 권한을 갖고 있지 않을 수 있습니다. 따라서
File::open 은 성공/실패 여부를 알려 주면서, 동시에 파일 핸들이나 에러 정보 중
하나를 함께 전달할 방법이 필요합니다. Result enum 이 바로 그 정보를 표현합니다.
File::open 이 성공하면 변수 greeting_file_result 의 값은 파일 핸들을 담은
Ok 인스턴스가 됩니다. 실패하면 greeting_file_result 는 어떤 종류의 에러가
발생했는지 더 많은 정보를 담은 Err 인스턴스가 됩니다. 이제 목록 9-3의 코드에,
File::open 이 어떤 값을 반환했는지에 따라 다른 행동을 하도록 코드를 추가해야
합니다.
목록 9-4는 6장에서 배운 기본 도구인 match 식을 사용해 Result 를 처리하는 한 가지
방법을 보여 줍니다.
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
Option enum과 마찬가지로, Result enum과 그 variant 역시 prelude 덕분에 이미
스코프에 들어와 있으므로, match arm 안에서 Ok, Err 앞에 Result:: 를 붙일
필요가 없습니다.
결과가 Ok 이면 이 코드는 Ok 안의 file 값을 꺼내어, 그 파일 핸들을
greeting_file 변수에 대입합니다. match 뒤에서는 이 파일 핸들을 읽기나 쓰기에
사용할 수 있습니다. 다른 arm은 File::open 으로부터 Err 값을 받은 경우를
처리합니다. 여기서는 그냥 panic! 매크로를 호출하기로 했습니다.
현재 디렉터리에 hello.txt 라는 파일이 없는 상태에서 이 코드를 실행하면,
panic! 매크로로부터 다음과 같은 출력을 보게 됩니다.
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
늘 그렇듯, 이 출력은 정확히 무엇이 잘못되었는지를 알려 줍니다.
서로 다른 에러를 서로 다르게 매칭하기
목록 9-4의 코드는 File::open 이 실패한 이유와 상관없이 무조건 panic! 을
일으킵니다. 하지만 우리는 실패 이유에 따라 다른 동작을 하게 만들고 싶습니다.
File::open 이 실패한 이유가 파일이 존재하지 않기 때문이라면, 파일을 새로 만들고
그 파일 핸들을 반환하고 싶습니다. 반면 권한이 없어서 열 수 없는 경우처럼, 다른
이유로 실패한다면 여전히 목록 9-4처럼 panic! 하고 싶습니다. 이를 위해 목록 9-5처럼
안쪽에 또 하나의 match 식을 추가합니다.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
File::open 이 Err variant 안에 담아 반환하는 값의 타입은 io::Error 인데,
이것은 표준 라이브러리가 제공하는 구조체입니다. 이 구조체에는 kind 라는 메서드가
있고, 이를 호출하면 io::ErrorKind 값을 얻을 수 있습니다. io::ErrorKind 역시
표준 라이브러리가 제공하는 enum으로, io 연산에서 발생할 수 있는 서로 다른 종류의
에러를 표현하는 variant들을 가집니다.
우리가 여기서 쓰고 싶은 variant는 ErrorKind::NotFound 입니다. 이 값은 열려고
했던 파일이 아직 존재하지 않는다는 뜻입니다. 따라서 우리는 먼저 greeting_file_result
에 대해 match 를 하고, 그 안에서 error.kind() 결과에 대해 또 match 를 합니다.
안쪽 match 에서 확인하고 싶은 조건은 error.kind() 가 ErrorKind enum의
NotFound variant 인지 여부입니다. 만약 그렇다면 File::create 로 파일을 만들려고
시도합니다.
하지만 File::create 도 실패할 수 있으므로, 안쪽 match 식에도 두 번째 arm이
필요합니다. 파일을 만들 수 없으면 다른 에러 메시지를 출력합니다. 바깥쪽 match
의 두 번째 arm은 그대로 유지되므로, “파일이 없음” 이외의 에러는 여전히 모두
패닉을 일으킵니다.
Result 에 match 를 쓰는 것의 대안
match 는 아주 유용하지만, 동시에 꽤 원시적인 도구이기도 합니다.
13장에서는 클로저를 배우게 되는데, 클로저는 Result 에 정의된 많은 메서드와 함께
사용됩니다. 이런 메서드들은 Result 값을 처리할 때, 직접 match 를 쓰는 것보다
더 간결할 수 있습니다.
예를 들어 목록 9-5와 같은 논리를, 이번에는 클로저와 unwrap_or_else 메서드를
사용해 다음처럼 쓸 수 있습니다.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
이 코드는 목록 9-5와 같은 동작을 하지만 match 식이 전혀 없고, 읽기에도 조금 더
깔끔합니다. 13장을 읽고 난 뒤 이 예제로 다시 돌아와 표준 라이브러리 문서에서
unwrap_or_else 메서드를 찾아보세요. 에러를 다룰 때 거대한 중첩 match 식을
치워 주는 메서드가 훨씬 더 많이 있습니다.
에러 시 패닉하기 위한 지름길
match 도 충분히 잘 동작하지만, 장황하게 느껴질 수 있고 의도를 항상 잘 드러내는
것도 아닙니다. Result 타입에는 더 구체적인 작업을 수행하기 위한 여러 헬퍼 메서드가
정의되어 있습니다.
unwrap 메서드는 목록 9-4에서 우리가 직접 쓴 match 식을 축약한 메서드입니다.
Result 값이 Ok variant면 unwrap 은 그 안의 값을 반환합니다. 반면 Err
variant 면 unwrap 이 대신 panic! 매크로를 호출합니다.
다음은 unwrap 을 사용하는 예입니다.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
만약 hello.txt 파일 없이 이 코드를 실행하면, unwrap 이 내부에서 호출한
panic! 으로부터 다음과 같은 에러 메시지가 나옵니다.
thread 'main' panicked at src/main.rs:4:49: called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
비슷하게 expect 메서드를 사용하면 panic! 메시지도 직접 선택할 수 있습니다.
다음이 expect 의 문법입니다.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
우리는 expect 역시 unwrap 과 같은 방식으로 사용합니다. 파일 핸들을 반환받거나,
아니면 panic! 매크로를 호출하도록 하는 것이지요. 다만 expect 가 panic! 에
전달하는 에러 메시지는 unwrap 이 쓰는 기본 메시지가 아니라, 우리가 직접 expect
에 넘긴 인수입니다.
결과는 다음과 같습니다.
thread 'main' panicked at src/main.rs:5:10: hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
실전 품질의 코드에서는 대부분의 Rustacean이 unwrap 보다는 expect 를 선호하고,
해당 연산이 왜 성공해야 하는지에 대한 맥락을 메시지에 넣어 둡니다. 그렇게 하면
나중에 그 가정이 틀렸을 때 디버깅에 더 많은 정보를 얻을 수 있습니다.
에러 전파하기
어떤 함수 구현이 실패할 수 있는 다른 함수를 호출할 때, 그 함수 안에서 직접 에러를 처리하는 대신 호출한 쪽 코드로 에러를 반환하여 거기서 무엇을 할지 결정하게 할 수 있습니다. 이를 에러를 전파(propagating) 한다고 하며, 더 많은 문맥과 로직을 가진 호출하는 쪽 코드에게 더 큰 제어권을 주게 됩니다.
예를 들어 목록 9-6은 파일에서 사용자 이름을 읽어 오는 함수를 보여 줍니다. 파일이 없거나 읽을 수 없다면, 이 함수는 그 에러를 함수를 호출한 쪽 코드에 그대로 돌려줍니다.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
}
이 함수는 훨씬 더 짧게 쓸 수도 있지만, 에러 처리를 살펴보기 위해 일부를 수동으로 길게 적어 보겠습니다. 마지막에 더 짧은 버전도 보여 줄 것입니다.
먼저 함수의 반환 타입부터 봅시다. Result<String, io::Error> 입니다. 이는 이 함수가
Result<T, E> 타입의 값을 반환한다는 뜻이고, 여기서 제네릭 매개변수 T 는
구체 타입 String 으로, 제네릭 타입 E 는 구체 타입 io::Error 로 채워졌다는
의미입니다.
이 함수가 문제 없이 성공하면, 이 함수를 호출한 코드는 파일에서 읽은 사용자 이름을
담고 있는 String 을 포함한 Ok 값을 받습니다. 반대로 함수가 문제를 만나면,
호출한 쪽 코드는 어떤 문제가 있었는지 더 많은 정보를 담은 io::Error 인스턴스를
포함한 Err 값을 받게 됩니다. 우리가 이 함수의 반환 타입으로 io::Error 를 선택한
이유는, 함수 본문 안에서 실패할 수 있는 두 연산인 File::open 함수와
read_to_string 메서드가 둘 다 io::Error 타입의 에러를 반환하기 때문입니다.
함수 본문은 먼저 File::open 을 호출하는 것으로 시작합니다. 그런 다음 목록 9-4의
match 와 비슷한 방식으로 이 Result 값을 처리합니다. File::open 이 성공하면
패턴 변수 file 안의 파일 핸들이 가변 변수 username_file 안으로 들어가고 함수는
계속 진행합니다. Err 인 경우에는 panic! 을 호출하는 대신 return 키워드로
함수 전체에서 조기에 빠져나오며, File::open 의 에러 값을(이제 패턴 변수 e
안에 있습니다) 이 함수의 에러 값으로 그대로 호출한 쪽에 돌려줍니다.
그래서 username_file 안에 파일 핸들이 있게 되면, 함수는 username 이라는 새
String 을 만들고, username_file 의 파일 핸들에 대해 read_to_string 메서드를
호출해 파일 내용을 username 안으로 읽어들입니다. read_to_string 역시 실패할 수
있기 때문에 또 다른 Result 를 반환합니다. File::open 이 성공했다고 해서
read_to_string 도 반드시 성공하는 것은 아니므로, 이 Result 도 다시 match
로 처리해야 합니다. read_to_string 이 성공하면 함수는 성공한 것이므로,
username 안에 들어 있는 사용자 이름을 Ok 로 감싸 반환합니다. 실패하면
File::open 의 반환값을 처리했던 match 에서와 같은 방식으로 그 에러 값을 반환합니다.
다만 여기서는 함수의 마지막 식이므로 return 을 명시적으로 쓸 필요가 없습니다.
그러면 이 함수를 호출한 코드는 String 을 담은 Ok 값이나, io::Error 를 담은
Err 값을 받게 되고, 그중 무엇을 받았는지에 따라 처리할 수 있습니다. 예를 들어
호출한 코드는 Err 값을 받으면 panic! 을 호출해 프로그램을 중단시킬 수도 있고,
기본 사용자 이름을 사용하거나, 파일이 아닌 다른 곳에서 사용자 이름을 읽어 올 수도
있습니다. 현재 함수 안에서는 호출한 쪽 코드가 실제로 무엇을 하려는지 알 수 없으므로,
성공과 에러 정보를 그대로 위로 전파해 적절히 처리하도록 맡기는 것입니다.
이런 식의 에러 전파 패턴은 러스트에서 너무 흔하기 때문에, 이를 더 쉽게 쓰도록
러스트는 물음표 연산자 ? 를 제공합니다.
? 연산자 지름길
목록 9-7은 목록 9-6과 같은 기능을 하는 read_username_from_file 구현을 보여 줍니다.
이번 구현은 ? 연산자를 사용합니다.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
}
? 연산자로 에러를 호출자에게 반환하는 함수Result 값 뒤에 붙은 ? 는, 목록 9-6에서 우리가 직접 쓴 match 식과 거의 같은
방식으로 동작하도록 정의되어 있습니다. Result 값이 Ok 라면, 그 안의 값이 이
식의 결과로 반환되고 프로그램은 계속 진행합니다. 값이 Err 라면, 마치 우리가 직접
return 키워드를 쓴 것처럼 함수 전체에서 그 Err 가 반환되어 에러 값이 호출하는
쪽 코드로 전파됩니다.
하지만 목록 9-6의 match 가 하던 일과 ? 가 하는 일 사이에는 차이도 있습니다.
? 를 호출한 에러 값은 표준 라이브러리의 From 트레이트에 정의된 from 함수를
거칩니다. 이 함수는 한 타입의 값을 다른 타입으로 변환할 때 사용됩니다. ? 연산자가
from 을 호출하면, 받은 에러 타입은 현재 함수의 반환 타입에 정의된 에러 타입으로
변환됩니다. 이는 함수가 여러 방식으로 실패할 수 있더라도 하나의 에러 타입으로 전부를
대표하고 싶을 때 유용합니다.
예를 들어 목록 9-7의 read_username_from_file 함수가 우리가 직접 정의한
OurError 라는 사용자 정의 에러 타입을 반환하도록 바꿀 수 있다고 해 봅시다.
그리고 io::Error 로부터 OurError 인스턴스를 만드는 impl From<io::Error> for OurError 도 정의한다면, read_username_from_file 본문 안의 ? 호출은 자동으로
from 을 사용해 에러 타입을 변환해 줍니다. 따라서 함수 안에 별도의 추가 코드가
필요하지 않습니다.
목록 9-7 맥락에서 보면, File::open 호출 뒤의 ? 는 Ok 안의 값을
username_file 변수로 돌려줍니다. 에러가 발생하면, ? 는 함수 전체에서 조기
반환하여 그 Err 값을 호출하는 코드에 넘깁니다. read_to_string 호출 끝의 ?
도 똑같이 동작합니다.
? 연산자는 보일러플레이트를 크게 줄여 주며, 함수 구현을 더 단순하게 만들어 줍니다.
심지어 ? 바로 뒤에 메서드 호출을 이어 붙여 코드 길이를 더 줄일 수도 있습니다.
목록 9-8을 보세요.
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
}
? 연산자 뒤에 메서드 호출을 체이닝하기이 버전에서는 함수 처음에 username 새 String 을 만드는 부분은 그대로 두고,
중간의 username_file 변수를 없앴습니다. 대신 File::open("hello.txt")? 의
결과에 직접 read_to_string 호출을 연결했습니다. read_to_string 호출 끝에는
여전히 ? 가 붙어 있고, File::open 과 read_to_string 둘 다 성공하면
username 을 담은 Ok 값을 반환합니다. 실패 시에는 에러를 반환합니다. 기능은
목록 9-6, 9-7과 동일하며, 단지 더 간결하고 사용하기 편한 방식으로 쓴 것뿐입니다.
목록 9-9는 fs::read_to_string 을 사용해 이 코드를 더 짧게 만드는 방법을 보여 줍니다.
#![allow(unused)]
fn main() {
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
}
fs::read_to_string 사용하기파일을 문자열로 읽는 일은 꽤 흔한 연산이므로, 표준 라이브러리는 파일을 열고, 새
String 을 만들고, 파일 내용을 읽어 그 안에 넣은 뒤, 그 문자열을 반환하는 편리한
fs::read_to_string 함수를 제공합니다. 물론 여기서는 에러 처리 과정을 설명하기 위해
더 긴 방식을 먼저 살펴본 것입니다.
? 연산자를 사용할 수 있는 곳
? 연산자는 그것이 적용되는 값과 호환되는 반환 타입을 가진 함수 안에서만 사용할 수
있습니다. 이는 ? 연산자가 목록 9-6의 match 식에서 우리가 했던 것처럼, 함수
바깥으로 값을 조기 반환하도록 정의되어 있기 때문입니다. 목록 9-6의 match 는
Result 값에 대해 동작했고, 조기 반환 arm은 Err(e) 값을 반환했습니다. 따라서
함수의 반환 타입도 그 return 과 호환되도록 Result 여야 했습니다.
목록 9-10에서는 반환 타입이 ? 를 사용하는 값과 호환되지 않는 main 함수 안에서
? 를 썼을 때 어떤 오류가 나는지 살펴봅니다.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
() 인 main 에서 ? 를 사용하려 하면 컴파일되지 않는다이 코드는 실패할 수 있는 파일 열기 연산을 수행합니다. ? 연산자는 File::open
이 반환한 Result 값에 붙어 있지만, 이 main 함수의 반환 타입은 Result 가
아닌 () 입니다. 이 코드를 컴파일하면 다음과 같은 오류를 얻게 됩니다.
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
이 오류는 ? 연산자를 Result, Option, 혹은 FromResidual 을 구현한 다른 타입을
반환하는 함수에서만 쓸 수 있다고 지적합니다.
이 오류를 고치는 방법은 두 가지입니다. 하나는 특별한 제약이 없다면, 함수의 반환 타입을
? 를 사용하는 값과 호환되도록 바꾸는 것입니다. 다른 하나는 match 나
Result<T, E> 의 메서드들을 사용해 그 Result<T, E> 를 적절한 방식으로 처리하는
것입니다.
오류 메시지는 또한 ? 를 Option<T> 값과 함께 사용할 수도 있다고 언급했습니다.
Result 에서 ? 를 쓰는 경우와 마찬가지로, Option 에 대한 ? 는 Option
을 반환하는 함수 안에서만 사용할 수 있습니다. Option<T> 에 대해 ? 가 동작하는
방식은 Result<T, E> 에 대해 동작하는 방식과 비슷합니다. 값이 None 이면 그
시점에서 함수 바깥으로 None 을 조기 반환합니다. 값이 Some 이면, Some 안의
값이 식의 결과가 되고 함수는 계속 진행합니다. 목록 9-11은 주어진 텍스트에서 첫 줄의
마지막 문자를 찾는 함수 예입니다.
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
assert_eq!(
last_char_of_first_line("Hello, world\nHow are you today?"),
Some('d')
);
assert_eq!(last_char_of_first_line(""), None);
assert_eq!(last_char_of_first_line("\nhi"), None);
}
Option<T> 값에 ? 연산자 사용하기이 함수는 문자 하나가 있을 수도 있고 없을 수도 있기 때문에 Option<char> 를
반환합니다. 이 코드는 문자열 슬라이스 인수 text 에 lines 메서드를 호출하는데,
이는 문자열 안의 줄들에 대한 반복자를 반환합니다. 함수는 첫 번째 줄만 확인하고 싶기
때문에, 반복자에 next 를 호출해 첫 값을 가져옵니다. 만약 text 가 빈 문자열이면,
이 next 호출은 None 을 반환하고, 이때 우리는 ? 를 사용해 함수를 멈추고
last_char_of_first_line 에서 None 을 반환합니다. 반대로 text 가 빈 문자열이
아니라면, next 는 첫 줄의 문자열 슬라이스를 담은 Some 값을 반환합니다.
? 는 그 문자열 슬라이스를 꺼내 주고, 우리는 그 슬라이스에 chars 를 호출해
문자 반복자를 얻을 수 있습니다. 우리는 첫 줄의 마지막 문자에 관심이 있으므로,
반복자에 last 를 호출해 마지막 항목을 반환합니다. 이 역시 Option 인데,
첫 줄이 빈 문자열일 가능성이 있기 때문입니다. 예를 들어 text 가 "\nhi" 라면,
첫 줄은 비어 있지만 뒤 줄에는 문자가 있습니다. 그러나 첫 줄에 마지막 문자가 있다면
그 값은 Some variant로 반환됩니다. 중간의 ? 연산자는 이 전체 로직을 매우 간결하게
표현하게 해 줍니다. 만약 Option 에 대해 ? 를 쓸 수 없었다면, 이 로직은 훨씬 더
많은 메서드 호출이나 match 식으로 구현해야 했을 것입니다.
중요한 점은, Result 를 반환하는 함수 안에서는 Result 에 대해 ? 를 사용할 수
있고, Option 을 반환하는 함수 안에서는 Option 에 대해 ? 를 사용할 수 있지만,
이 둘을 마음대로 섞어 쓸 수는 없다는 것입니다. ? 연산자는 Result 를 Option
으로, 혹은 그 반대로 자동 변환해 주지 않습니다. 그런 경우에는 Result 의 ok
메서드나 Option 의 ok_or 메서드처럼, 명시적으로 변환하는 메서드를 사용해야
합니다.
지금까지 우리가 사용한 모든 main 함수는 () 를 반환했습니다. main 함수는
실행 가능한 프로그램의 시작점이자 끝점이기 때문에, 프로그램이 기대한 대로 동작하려면
반환 타입에 몇 가지 제약이 있습니다.
다행히 main 도 Result<(), E> 를 반환할 수 있습니다. 목록 9-12는 목록 9-10의
코드를 가져와, main 의 반환 타입을 Result<(), Box<dyn Error>> 로 바꾸고
마지막에 Ok(()) 를 추가한 버전입니다. 이 코드는 이제 컴파일됩니다.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
main 이 Result<(), E> 를 반환하도록 바꾸면 Result 값에 ? 연산자를 쓸 수 있다Box<dyn Error> 타입은 trait object이며, 이는 18장의 “공통 동작을 추상화하기 위해
트레이트 객체 사용하기” 절에서 다룹니다. 지금은
Box<dyn Error> 를 “어떤 종류의 에러든 담을 수 있는 것” 정도로 읽으면 됩니다.
Box<dyn Error> 가 허용되기 때문에, Result<(), Box<dyn Error>> 를 반환하는
main 안에서는 어떤 Err 값이든 조기 반환될 수 있습니다. 이 main 함수 본문이
현재는 std::io::Error 타입 에러만 반환하더라도, 나중에 다른 종류의 에러를 반환하는
코드를 추가해도 이 시그니처는 여전히 올바르게 유지됩니다.
main 함수가 Result<(), E> 를 반환하면, main 이 Ok(()) 를 반환할 때
실행 파일은 종료 값 0 으로 끝나고, Err 를 반환하면 0이 아닌 값으로 끝납니다.
C로 작성된 실행 파일도 종료 시 정수를 반환합니다. 성공적으로 끝난 프로그램은 0,
오류로 끝난 프로그램은 0 이 아닌 정수를 반환합니다. 러스트도 이 관례와 호환되기
위해 실행 파일에서 정수를 반환합니다.
main 함수는 [표준 라이브러리의 std::process::Termination 트레이트]
termination를 구현한 어떤 타입이든 반환할 수 있습니다. 이
트레이트는 ExitCode 를 반환하는 report 함수를 포함합니다. 여러분 자신의 타입에
Termination 트레이트를 구현하는 방법은 표준 라이브러리 문서를 참고하세요.
이제 panic! 을 호출하는 경우와 Result 를 반환하는 경우의 세부를 모두 살펴봤으니,
어떤 상황에서 어느 쪽을 사용하는 것이 적절한지라는 주제로 다시 돌아가 보겠습니다.