16. ? 연산자와 에러 전파
? 연산자는 Result 또는 Option 표현식 뒤에 붙여 "실패면 즉시 함수에서 반환" 을 한 글자로 줄여줍니다. 그래서 여러 단계의 IO·파싱이 깔끔하게 일자로 늘어집니다. From 트레잇을 통해 다른 종류의 에러 타입이 자동 변환되어 전파되는 메커니즘도 함께 살펴봅니다.
이 강의에서 배우는 것
- 1? 연산자로 Result/Option 의 실패를 즉시 반환한다
- 2main 함수 시그니처를 Result 반환으로 바꿔 ? 를 쓴다
- 3From 트레잇으로 서로 다른 에러 타입을 자동 변환·전파한다
- 4? 가 Option 에도 적용되는 조건을 안다
- 5Box<dyn Error> 로 간편한 에러 누적을 활용한다
소개
Result 매 단계마다 match 를 쓰면 코드가 들여쓰기 절벽으로 가게 됩니다. `?` 한 글자는 그걸 한 줄로 줄여줍니다. "이 단계에서 실패하면 그냥 그대로 위로 반환" 의 의미.
핵심 개념
1) ? — Result 에 붙이면
`expr?` 는 다음과 동치:
// expr?
// 의미:
match expr {
Ok(v) => v,
Err(e) => return Err(e.into()), // From 으로 변환
}2) ? — Option 에 붙이면
함수 반환 타입이 Option 인 경우, None 이면 즉시 None 반환.
3) From 트레잇으로 에러 자동 변환
함수가 반환하는 에러 타입 `MyError` 가 `From<io::Error>` 를 구현하면, io 작업의 ? 연산자 결과가 자동으로 `MyError` 로 변환돼 전파됩니다. 여러 종류 에러를 한 가지로 통합.
4) main 도 Result 반환 가능
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let n = "42".parse::<i32>()?;
println!("{}", n);
Ok(())
}핵심 예제
파일 읽고 첫 줄 파싱 — ? 가 없을 때 vs 있을 때:
use std::fs::File;
use std::io::{self, Read};
// ? 없는 버전 — 들여쓰기 절벽
fn read_first_number_v1(path: &str) -> Result<i32, String> {
let mut f = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(format!("open: {}", e)),
};
let mut s = String::new();
if let Err(e) = f.read_to_string(&mut s) {
return Err(format!("read: {}", e));
}
let first = s.lines().next().ok_or("empty")?;
first.trim().parse::<i32>().map_err(|e| format!("parse: {}", e))
}// ? 와 Box<dyn Error> 버전 — 한 줄씩 깨끗하게
use std::error::Error;
fn read_first_number_v2(path: &str) -> Result<i32, Box<dyn Error>> {
let mut s = String::new();
File::open(path)?.read_to_string(&mut s)?;
let first = s.lines().next().ok_or("empty")?;
let n = first.trim().parse::<i32>()?;
Ok(n)
}Option 과 함께 쓰기:
fn first_char_uppercased(s: &str) -> Option<char> {
let c = s.chars().next()?; // None 이면 즉시 None
c.to_uppercase().next()
}
fn main() {
println!("{:?}", first_char_uppercased("hello")); // Some('H')
println!("{:?}", first_char_uppercased("")); // None
}자주 하는 실수
Q. ? 가 함수 안에서만 동작하나요?
A. 네, 그 함수의 반환 타입이 Result 또는 Option 이어야 합니다. main 도 Result 반환이면 ? 사용 가능. 그렇지 않은 함수에선 컴파일 에러.
Q. 에러 종류가 다른데 ? 가 통과되나요?
A. From 트레잇이 구현돼 있으면 자동 변환됩니다. Box<dyn Error> 는 거의 모든 에러를 받아주는 "전능" 타입이라 간편하지만, 라이브러리에선 구체 에러 타입(thiserror 등) 이 권장됩니다 — 다음 강의에서.
Q. ? 를 식 중간에 못 쓰나요?
A. 씁니다. `let n = parse()? + 1;` 처럼. 단 Result/Option 인 위치에만.
정리
- ? = "실패면 즉시 반환" 한 글자 매크로
- 함수 시그니처가 Result/Option 이어야 사용 가능
- From 트레잇으로 다른 에러 타입 자동 변환
- main 도 Result 반환이면 ? 사용 가능 — 프로그램 진입점 단순화
과제
- ? 연산자로 두 단계 파싱(문자열 → 정수 → 양수 검증) 을 작성
- fn first_two_uppercased(s: &str) -> Option<(char, char)> 를 ? 로 작성
- main 을 -> Result<(), Box<dyn Error>> 로 바꿔 std 파일 IO 와 파싱을 ? 로 처리