에러 처리 (panic, Result, Option, unwrap, expect, ? 연산자)

Rust는 안전한 시스템 프로그래밍 언어답게, 명시적이고 예측 가능한 에러 처리 방식을 제공합니다. 오늘은 Rust의 주요 에러 처리 도구인 panic, Result, Option, unwrap, expect, ? 연산자에 대해 다루겠습니다.


1. 패닉(panic!)

fn main() {
    panic!("예기치 못한 오류 발생!");
}
  • panic!를 사용하면 프로그램이 즉시 종료되고, panic! 안에 있는 메시지를 보여줍니다.
  • 디버깅 중 주로 사용되며, 복구 불가능한 에러에 적합합니다.

let v = vec![1, 2, 3];
v[99]; // 존재하지 않는 인덱스 → 자동 panic!

Vector v의 요소가 3개이고, index의 최대값이 2인데, 인덱스를 99로 지정하면 “index가 경계를 넘어섰다”는 메시지를 보여주면서 프로그램이 멈춥니다.


2. Result<T, E> 타입

Resut<T, E>는 복구 가능한 에러 처리를 위한 열거형으로, 성공했을 때는 Ok(T), 에러가 발생했을 때는 Err(E)를 반환합니다.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

[예제]

use std::fs::File;

fn main() {
    let result = File::open("hello.txt");

    match result {
        Ok(file) => println!("파일 열기 성공"),
        Err(e) => println!("에러 발생: {}", e),
    }
}
  • use std::fs::File;
    : 표준 라이브러리의 fs 모듈에 정의된 File 구조체를 현재 스코프에서 사용할 수 있도록 가져오는 구문입니다. 
  • let result = File::open(“hello.txt”);
    : hello.txt 파일을 여는데, 그 결과를 result에 저장합니다. result는 Result 열거형으로 File 구조체와 Error라는 2개의 variant(변형)를 가지고 있습니다.
  • match result {
    : match 연산자를 이용해 result의 결과값에 따라 처리합니다.
  • Ok(file) => println!(“파일 열기 성공”),
    : 파일 열기가 성공(Ok)이면 File 구조체 형식의 file을 variant로 갖습니다.
  • Err(e) => println!(“에러 발생: {}”, e),
    : 파일 열기에 실패하면 Err가 발생하는데, e라는 에러 종류를 갖습니다.

프로그램과 같은 폴더에 hello.txt가 없으면
“에러 발생: 지정된 파일을 찾을 수 없습니다. (os error 2)”가 화면에 출력되고,
파일이 있으면 “파일 열기 성공”이 출력됩니다.

Ok(file) => println!(“{:?} 파일 열기 성공”, file), 라고 코드를 수정하고 실행하면, hello.txt 파일이 있을 경우 file 구조체가 출력문(println!)에 전달되어
“File { handle: 0xb4, path: “\\?\D:\rust-practice\day12\hello.txt” } 파일 열기 성공”이 출력됩니다.


3. Option<T> 타입

값이 있을 수도 있고[Some(T)] 없을 수도 있음(None)을 나타냅니다. Result의 경우는 Ok(T), Err(E)인 것과 대비됩니다.

fn main() {
    let some_number = Some(10);
    // let some_number: Option<i32> = None;

    match some_number {
        Some(n) => println!("Some: {n}"),
        None => println!("None"),
    }
}
  • let some_number = Some(10);인 상태에서 위 코드를 실행하면
    Match 제어 흐름 연산자에서 Some(n)에 해당되므로 “Some: 10″이 화면에 출력되고,
  • 첫번째 줄 let some_number = Some(10);을 주석 처리하고, 두번째 줄의 주석을 제거하면 None과 매칭되어 “None”이 하면에 출력됩니다.

4. unwrap, expect

let f = File::open("hello.txt").unwrap(); // 실패 시 panic!
let f = File::open("hello.txt").expect("파일 열기 실패"); // 사용자 메시지 포함
  • unwrap과 expect는 Some(T)이거나, Ok(T)인 경우 내부의 T를 꺼내는(unwrap) 기능을 하고, None이거나, Err(E)인 경우는 빠르게 실패(fail fast)하고 싶을 때 사용하는데, expect는 unwrap와 달리 에러 메시지를 제공합니다.

  • hello.txt가 있을 경우 println!(“{:?} 파일 열기 성공”, f}를 이용해 f를 출력해보면 둘 다 아래와 같이 f를 출력합니다.
    File { handle: 0xb4, path: “\\?\D:\rust-practice\day12\hello.txt” } 파일 열기 성공
    File { handle: 0xb8, path: “\\?\D:\rust-practice\day12\hello.txt” } 파일 열기 성공

  • 그러나, 파일이 없으면 둘다 패닉이 발생하고, 에러 메시지를 표출하는데,
    let f = File::open(“hello.txt”).unwrap();은 사용자 메시지가 없고,

  • let f = File::open(“hello.txt”).expect(“파일 열기 실패”);은 에러 메시지 전에 “파일 읽기 실패”라는 사용자 메시지를 표시하는 것만 다릅니다.

5. ? 연산자(에러 전파)

use std::fs::File;
use std::io::{self, Read};

fn read_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt");
    let mut content = String::from("This is a test file.\n");
    f.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    match read_file() {
        Ok(content) => println!("파일 내용:\n{}", content),
        Err(e) => eprintln!("파일 읽기 오류: {}", e),
    }
}
  • Err이면, 다시 말해 hello.txt가 없을 경우, ?는 panic을 발생시키지 않고 즉시 리턴을 해서 main문의 Err 분기를 처리합니다.
  • 그러나, ?가 없으면 다음 줄의 f.read_to_string(&mut content)?;으로 넘어가는데, read_to_string 메소드가 없다고 하면서 컴파일 에러가 발생합니다.
  • Result 타입에서만 사용 가능합니다.


6. Option<T>와 Result<T, E> 비교

타입의미실패 시
Option<T>값이 있을 수도[Some(T)], 없을 수도 있음(None)None
Result<T, E>성공[Ok(T)], 또는 실패[Err(E)] 결과 포함Err(E)

둘 다 match, if let, unwrap 등을 통해 사용가능합니다.


7. 요약

도구용도설명
panic!치명적 에러프로그램 즉시 종료
Result<T, E>복구 가능한 에러성공과 실패 구분
unwrap/expect빠르게 실패간결하지만 안전하지 않음
? 연산자에러 전파Result를 간단히 처리
Option<T>존재 여부 표현값이 있을 수도 없을 수도 있음

열거형 (Enums)

Rust는 복잡한 데이터와 다양한 상태를 안전하게 표현할 수 있는 강력한 도구인 열거형(enum)을 제공합니다.
enum은 단순한 값 목록이 아닌, 각 변형(variant)에 고유한 데이터를 담을 수 있어 패턴 매칭(match)과 결합해 매우 유용하게 사용됩니다.


1. 기본 열거형의 정의와 사용

열거형은 여러 개의 이름 있는 변형을 정의하는 타입입니다.
enum 다음에 이름을 입력하고 중괄호 안에 variant(변형)를 입력합니다.

enum Direction {
    North,
    South,
    East,
    West,
}

아래와 같이 함수에 열거형을 사용할 때는 함수명을 적고 인수를 입력하는데, 인수의 형식은 열거형이 됩니다. 그리고, match 흐름 제어 연산자를 사용하는데, match 다음에 인수명을 기재하고, 분기(arm)를 정의하는데 열거형의 이름 다음에 ::을 추가하며, =>을 사용해 실행 코드를 지정합니다.

fn move_to(dir: Direction) {
    match dir {
        Direction::North => println!("북쪽으로 이동"),
        Direction::South => println!("남쪽으로 이동"),
        Direction::East  => println!("동쪽으로 이동"),
        Direction::West  => println!("서쪽으로 이동"),
    }
}

main 함수는 아래와 같이 let을 이용해 direction 변수에 열거형 이름::변형을 입력하고, 위 함수 move_to의 인수로 direction 변수를 입력하면 “북쪽으로 이동”이란 글자가 화면에 표시됩니다.

fn main () {
    let direction = Direction::North;
    move_to(direction); 
}

North를 달리하면 출력 결과가 달라지며, 변형에 없는 값, 아래에서는 North2를 입력하면 “Direction에서 (North2) variant가 발견되지 않는다”라는 에러 메시지가 표시되므로 정확한 입력을 보장할 수 있습니다.


2. 열거형 변형에 데이터 저장

열거형은 각 변형마다 다른 타입의 데이터를 가질 수 있습니다. 이 점이 Rust enum의 강력한 특징입니다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

각 변형은 정해진 구조의 데이터를 저장할 수 있습니다:

  • Quit은 데이터 없음
  • Move는 구조체 형태(중괄호 사용)
  • Write는 문자열
  • ChangeColor는 튜플 형태(소괄호 사용)
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("종료"),
        Message::Move { x, y } => println!("이동: ({}, {})", x, y),
        Message::Write(text) => println!("메시지: {}", text),
        Message::ChangeColor(r, g, b) => println!("색상 변경: {}, {}, {}", r, g, b),
    }
}

fn main() {
    let msg1 = Message::Quit;
    let msg2 = Message::Move { x: 10, y: 20 };
    let msg3 = Message::Write(String::from("안녕하세요"));
    let msg4 = Message::ChangeColor(255, 0, 0);

    process(msg1);
    process(msg2);
    process(msg3);
    process(msg4);
}

위 코드를 실행하면 아래와 같이 분기에 따라 실행 코드가 화면에 출력됩니다.

종료
이동: (10, 20)
메시지: 안녕하세요
색상 변경: 255, 0, 0

3. match 표현식

enum과 함께 가장 강력하게 사용되는 문법이 match입니다. 모든 경우를 exhaustively(빠짐없이) 처리하도록 강제되어, 안전한 분기 로직을 작성할 수 있습니다.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String),
}

fn main() {
    
    let coin = Coin::Quarter("New York".to_string());

    match coin {
        Coin::Penny => println!("1원!"),
        Coin::Nickel => println!("5원!"),
        Coin::Dime => println!("10원!"),
        Coin::Quarter(state) => println!("25원 from {:?}", state),
    }
}

  • enum Coin이 네 가지가 있으므로 match 연산자는 네 가지 분기를 모두 작성해야 합니다. 위 코드에서 match 분기의 하나를 주석 처리하고 실행하면

“모든 경우를 소진하지 않았다(none-exaustive patterns)”고 하면서 ‘Coin:Dime” not covered라고 “Dime 코인을 커버하지 않았다”고 합니다.

  • 모든 경우를 망라하기 어렵다면 _를 이용해 위를 제외한 다른 경우는 모두 이에 해당하는 실행 코드를 적용하도록 할 수 있습니다.
    match coin {
        Coin::Penny => println!("1원!"),
        Coin::Nickel => println!("5원!"),
        // Coin::Dime => println!("10원!"),
        // Coin::Quarter(state) => println!("25원 from {:?}", state),
        _ => println!("기타 동전!"),
    }

4. if let 표현식

단일 패턴만 확인하고 나머지는 무시하고 싶을 때는 if let 구문을 이용해 더 간결하게 처리할 수 있습니다.

    let coin = Coin::Penny;
    if let Coin::Penny = coin {
        println!("1원!");
    }

if 문의 내용이 “같다면”인데 let 문이므로 ==이 아니라 =를 사용했다는 것, 그리고 Coin::Penny가 앞에 왔다는 점을 주의해야 합니다. 위 코드를 실행하면 “1원!”가 화면에 출력됩니다.

if let coin = Coin::Penny 이라고 순서를 바꿔 표시하면 에러는 나지 않는데 coin 변수를 사용하지 않았으므로 _coin으로 변수명을 바꾸라는 제안을 합니다.

match보다 간단하지만, 나머지 경우는 무시되므로 사용에 주의가 필요합니다.


5. 열거형은 메서드도 가질 수 있다

열거형도 구조체처럼 impl 블록을 통해 메서드를 정의할 수 있습니다.

enum Status {
    Ready,
    Waiting,
    Error(i32),
}

impl Status {
    fn print(&self) {
        match self {
            Status::Ready => println!("준비 완료"),
            Status::Waiting => println!("대기 중"),
            Status::Error(code) => println!("에러 코드: {}", code),
        }
    }
}
fn main() {
    let status1 = Status::Ready;
    let status2 = Status::Waiting;
    let status3 = Status::Error(404);

    status1.print();
    status2.print();
    status3.print();
}

위 코드를 실행하면 아래와 같이 화면에 표시됩니다.

준비 완료
대기 중
에러 코드: 404

6. Option 열거형

Rust 표준 라이브러리에는 매우 자주 쓰이는 열거형인 Option이있습니다. 이는 값이 있을 수도 있고, 없을 수도 있다는 개념을 타입 시스템으로 안전하게 표현합니다.

enum Option<T> {
    Some(T),
    None,
}

예시:

let some_number = Some(5);
let no_number: Option<i32> = None;

이 방식은 null을 사용하지 않고도 안전하게 결측 값을 표현할 수 있게 해줍니다.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(n) => Some(n + 1),
        None => None,
    }
}

위 함수 plus_one은 인수 x가 정수(i32)라면 +1을 하고, None이라면 None을 반환하라는 의미인데, Option Enum이라 정수를 그냥 입력하면 안되고, Some 괄호 안에 입력해야 합니다.

fn main() {
    let num1 = Some(5);
    let num2 = None;

    println!("Result 1: {:?}", plus_one(num1)); // Result 1: Some(6)
    println!("Result 2: {:?}", plus_one(num2)); // Result 2: None
}

위와 같이 main 함수를 만들어 실행하면 숫자 5가 Some(5)라고 입력되면 Some(6)이 반환되고, None이 입력되면 None이 반환됩니다.

Rust는 기존의 사고 방식을 모두 바꿔버리니 적응하기 어렵습니다.


마무리

Rust의 열거형은 단순한 열거 상수를 넘어서 다양한 형태의 상태를 표현하는 강력한 수단입니다.특히 Option, Result, match와 결합하면 null, 에러, 상태 관리 등의 문제를 컴파일 타임에 안전하게 해결할 수 있습니다.