동작하는 코드
모듈 16에서 Result와 unwrap()을 배웠어요. 이번에는 unwrap() 없이 에러를 우아하게 처리하는 방법을 배울 거예요.
파일을 읽는 프로그램을 만들어볼게요.
use std::fs;
fn main() {
// unwrap() 버전 — 파일이 없으면 panic!
// let content = fs::read_to_string("hello.txt").unwrap();
// match 버전 — 안전하지만 장황해요
match fs::read_to_string("hello.txt") {
Ok(content) => println!("파일 내용: {content}"),
Err(e) => println!("파일을 읽을 수 없어요: {e}"),
}
}
파일이 없어도 프로그램이 죽지 않아요! 하지만 match가 매번 들어가면 코드가 복잡해지겠죠?
직접 해보기
hello.txt파일을 만들고 내용을 넣은 후 실행해보세요.- 파일 이름을 존재하지 않는 이름으로 바꿔서 실행해보세요.
unwrap()버전의 주석을 풀고 파일 없이 실행해보세요 — 어떤 에러 메시지가 나오나요?
? 연산자 — match를 한 글자로
?는 Result를 반환하는 함수 안에서 사용할 수 있어요. Ok면 값을 꺼내고, Err면 즉시 에러를 반환해요.
먼저 ? 없이 match로 쓰면 이렇게 돼요.
use std::fs;
use std::io;
fn read_greeting() -> Result<String, io::Error> {
let content = match fs::read_to_string("hello.txt") {
Ok(text) => text,
Err(e) => return Err(e), // 에러면 즉시 반환
};
Ok(format!("인사말: {content}"))
}
?를 쓰면 이걸 한 글자로 줄일 수 있어요!
use std::fs;
use std::io;
fn read_greeting() -> Result<String, io::Error> {
let content = fs::read_to_string("hello.txt")?;
Ok(format!("인사말: {content}"))
}
fn main() {
match read_greeting() {
Ok(msg) => println!("{msg}"),
Err(e) => println!("에러: {e}"),
}
}
fs::read_to_string("hello.txt")? — 이 ?가 하는 일은 정확히 위의 match와 같아요. "성공하면 값을 꺼내고, 실패하면 에러를 호출자에게 전파해라."
? 체이닝
?는 여러 번 연속으로 쓸 수 있어요. 에러가 발생하면 그 시점에서 즉시 함수를 빠져나가요.
use std::fs;
use std::io;
fn read_number_from_file() -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string("number.txt")?; // 파일 읽기 실패?
let number = content.trim().parse::<i32>()?; // 숫자 변환 실패?
Ok(number)
}
fn main() {
match read_number_from_file() {
Ok(n) => println!("숫자: {n}"),
Err(e) => println!("에러: {e}"),
}
}
match로 썼다면 중첩된 match 지옥이 됐을 거예요. ? 덕분에 "해피 패스"만 깔끔하게 볼 수 있어요.
main()에서 Result 반환하기
?는 Result를 반환하는 함수 안에서만 쓸 수 있어요. main()에서도 Result를 반환하면 ?를 쓸 수 있어요!
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = fs::read_to_string("hello.txt")?;
println!("{content}");
Ok(())
}
Box<dyn std::error::Error>는 "어떤 에러든 담을 수 있는 상자"예요. 지금은 이렇게 외워두세요.
리팩토링 전/후
모듈 07의 숫자 맞추기 게임에서 unwrap()을 썼던 걸 기억하나요? ?로 어떻게 바뀌는지 보세요.
Before — unwrap() 투성이
use std::io;
fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let number: i32 = input.trim().parse().unwrap();
println!("입력한 숫자: {number}");
}
unwrap()이 두 곳이에요. 입력 실패든 변환 실패든 프로그램이 그냥 죽어요.
After — ? 연산자
use std::io;
fn read_number() -> Result<i32, Box<dyn std::error::Error>> {
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let number = input.trim().parse::<i32>()?;
Ok(number)
}
fn main() {
match read_number() {
Ok(n) => println!("입력한 숫자: {n}"),
Err(e) => println!("잘못된 입력: {e}"),
}
}
unwrap()이 사라지고, 에러가 나도 프로그램이 죽지 않아요!
사용자 정의 에러
실제 프로젝트에서는 여러 종류의 에러가 섞여요. enum으로 에러 타입을 만들고 From 트레이트를 구현하면 ? 연산자와 함께 깔끔하게 쓸 수 있어요.
use std::fs;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "파일 에러: {e}"),
AppError::Parse(e) => write!(f, "변환 에러: {e}"),
}
}
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn read_config_port() -> Result<u16, AppError> {
let content = fs::read_to_string("config.txt")?; // io::Error → AppError
let port = content.trim().parse::<u16>()?; // ParseIntError → AppError
Ok(port)
}
fn main() {
match read_config_port() {
Ok(port) => println!("포트: {port}"),
Err(e) => println!("설정 에러: {e}"),
}
}
From 트레이트를 구현해두면 ? 연산자가 자동으로 에러 타입을 변환해줘요. io::Error든 ParseIntError든 AppError로 바뀌어서 하나의 Result로 반환돼요.
더 알아보기: map_err()
에러 타입을 변환할 때 From을 구현하지 않고 map_err()를 쓸 수도 있어요.
use std::fs;
fn read_port() -> Result<u16, String> {
let content = fs::read_to_string("config.txt")
.map_err(|e| format!("파일 읽기 실패: {e}"))?;
let port = content.trim().parse::<u16>()
.map_err(|e| format!("포트 변환 실패: {e}"))?;
Ok(port)
}
간단한 경우에는 map_err()가 편하고, 여러 함수에서 같은 에러 타입을 쓸 때는 From이 나아요.
연습 1 (쉬움). 아래 함수에서 unwrap()을 ?로 바꿔보세요.
use std::fs;
fn read_file() -> Result<String, std::io::Error> {
let content = fs::read_to_string("data.txt").unwrap(); // ? 로 바꾸세요
Ok(content)
}
fn main() {
match read_file() {
Ok(text) => println!("{text}"),
Err(e) => println!("에러: {e}"),
}
}
연습 2 (보통). 파일에서 두 숫자를 읽어 더하는 함수를 작성하세요. 파일이 없거나 숫자 변환에 실패하면 에러를 반환해야 해요.
use std::fs;
fn sum_from_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
// 여기를 완성하세요
// 1. 파일을 읽으세요 (?)
// 2. 줄 단위로 나누고 각 줄을 i32로 변환하세요 (?)
// 3. 합계를 반환하세요
// 힌트: lines()와 parse::<i32>()를 사용하세요
}
fn main() {
match sum_from_file("numbers.txt") {
Ok(total) => println!("합계: {total}"),
Err(e) => println!("에러: {e}"),
}
}
연습 3 (도전). 아래 코드에 AppError 열거형과 From 구현을 추가해서 ?로 에러를 처리하세요.
use std::fs;
use std::io;
use std::num::ParseIntError;
// 여기에 AppError enum을 정의하세요
// From<io::Error>와 From<ParseIntError>를 구현하세요
fn load_age(path: &str) -> Result<u8, AppError> {
let content = fs::read_to_string(path)?;
let age = content.trim().parse::<u8>()?;
Ok(age)
}
fn main() {
match load_age("age.txt") {
Ok(age) => println!("나이: {age}"),
Err(e) => println!("에러: {:?}", e),
}
}
Q1. ? 연산자의 동작은?
- A) 항상 panic을 일으킨다
- B) Err를 무시하고 기본값을 반환한다
- C) Ok이면 값을 꺼내고, Err이면 에러를 호출자에게 전파한다
- D) Option만 처리할 수 있다
Q2. 다음 코드가 컴파일되지 않는 이유는?
fn main() {
let content = std::fs::read_to_string("hello.txt")?;
println!("{content}");
}
- A)
read_to_string은 존재하지 않는 함수다 - B)
main()이Result를 반환하지 않아서?를 쓸 수 없다 - C)
content가mut이 아니다 - D)
std::fs를 import하지 않았다
Q3. From 트레이트를 에러 타입에 구현하면 어떤 장점이 있나요?
- A) 프로그램이 더 빠르게 실행된다
- B)
?연산자가 에러 타입을 자동으로 변환해준다 - C) 에러가 발생하지 않게 된다
- D)
unwrap()을 안전하게 만들어준다