본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: Rust 타입 시스템과 에러 처리
2026년 2월 15일·프로그래밍·

3장: Rust 타입 시스템과 에러 처리

구조체, 열거형, 트레이트부터 Result/Option, thiserror/anyhow를 활용한 에러 처리 패턴까지 Rust 타입 시스템의 핵심을 다룹니다.

14분803자12개 섹션
rust
공유
rust-backend3 / 11
1234567891011
이전2장: 소유권과 차용 체크 멘탈 모델다음4장: async/await와 Tokio 런타임

학습 목표

  • 구조체, 열거형, 트레이트의 활용법을 익힙니다
  • Result와 Option 타입을 정확히 이해합니다
  • ? 연산자를 활용한 에러 전파 패턴을 학습합니다
  • thiserror와 anyhow를 조합한 실전 에러 처리 전략을 파악합니다

구조체 (Struct)

Rust의 **구조체(struct)**는 관련 데이터를 묶는 기본 단위입니다. 백엔드 개발에서 도메인 모델, 요청/응답 DTO, 설정 등을 표현할 때 사용합니다.

구조체 정의와 사용
rust
#[derive(Debug, Clone)]
struct User {
    id: u64,
    name: String,
    email: String,
    active: bool,
}
 
impl User {
    // 연관 함수 (associated function) — 생성자 역할
    fn new(id: u64, name: String, email: String) -> Self {
        Self {
            id,
            name,
            email,
            active: true,
        }
    }
 
    // 메서드 — &self로 불변 빌림
    fn display_name(&self) -> &str {
        &self.name
    }
 
    // 가변 메서드 — &mut self로 가변 빌림
    fn deactivate(&mut self) {
        self.active = false;
    }
}

#[derive(Debug, Clone)]은 **매크로(derive macro)**로, 컴파일러가 Debug와 Clone 트레이트의 구현을 자동으로 생성합니다. 백엔드에서 자주 사용하는 derive 조합은 다음과 같습니다.

자주 사용하는 derive 조합
rust
use serde::{Deserialize, Serialize};
 
// API 응답 모델
#[derive(Debug, Clone, Serialize)]
struct UserResponse {
    id: u64,
    name: String,
    email: String,
}
 
// API 요청 모델
#[derive(Debug, Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

열거형 (Enum)

Rust의 **열거형(enum)**은 다른 언어의 열거형과 차원이 다릅니다. 각 변형(variant)이 데이터를 포함할 수 있어, **대수적 데이터 타입(Algebraic Data Type, ADT)**을 표현합니다.

데이터를 포함하는 열거형
rust
#[derive(Debug)]
enum PaymentMethod {
    CreditCard { number: String, expiry: String },
    BankTransfer { account: String, bank_code: String },
    DigitalWallet(String), // 지갑 주소
    Cash,
}
 
fn process_payment(method: &PaymentMethod) {
    match method {
        PaymentMethod::CreditCard { number, .. } => {
            println!("카드 결제: ****{}", &number[number.len()-4..]);
        }
        PaymentMethod::BankTransfer { account, bank_code } => {
            println!("계좌이체: {} ({})", account, bank_code);
        }
        PaymentMethod::DigitalWallet(address) => {
            println!("디지털 월렛: {}", address);
        }
        PaymentMethod::Cash => {
            println!("현금 결제");
        }
    }
}

match 표현식은 **패턴 매칭(pattern matching)**으로, 모든 변형을 빠짐없이 처리해야 합니다. 하나라도 빠뜨리면 컴파일 에러가 발생합니다. 이를 **소진적 매칭(exhaustive matching)**이라 하며, 새 변형을 추가할 때 처리하지 않은 곳을 컴파일러가 알려줍니다.

트레이트 (Trait)

**트레이트(trait)**는 공유 동작을 정의하는 메커니즘입니다. 다른 언어의 인터페이스와 유사하지만, 기본 구현을 제공할 수 있고 기존 타입에도 트레이트를 구현할 수 있습니다.

트레이트 정의와 구현
rust
trait Repository {
    type Item;
    type Error;
 
    async fn find_by_id(&self, id: u64) -> Result<Option<Self::Item>, Self::Error>;
    async fn save(&self, item: &Self::Item) -> Result<(), Self::Error>;
    async fn delete(&self, id: u64) -> Result<(), Self::Error>;
 
    // 기본 구현
    async fn exists(&self, id: u64) -> Result<bool, Self::Error> {
        Ok(self.find_by_id(id).await?.is_some())
    }
}
 
struct PostgresUserRepository {
    pool: String, // 실제로는 sqlx::PgPool
}
 
impl Repository for PostgresUserRepository {
    type Item = User;
    type Error = String; // 실제로는 sqlx::Error
 
    async fn find_by_id(&self, id: u64) -> Result<Option<User>, String> {
        // 데이터베이스 조회 로직
        todo!()
    }
 
    async fn save(&self, item: &User) -> Result<(), String> {
        todo!()
    }
 
    async fn delete(&self, id: u64) -> Result<(), String> {
        todo!()
    }
    // exists()는 기본 구현을 사용
}
Info

Rust에서 트레이트는 **의존성 역전(Dependency Inversion)**을 구현하는 핵심 도구입니다. 위 예제처럼 Repository 트레이트를 정의하면, 테스트에서는 인메모리 구현을, 프로덕션에서는 PostgreSQL 구현을 사용할 수 있습니다.

Option 타입

Rust에는 null이 없습니다. 대신 값이 있을 수도 없을 수도 있는 상황을 Option 열거형으로 표현합니다.

Option 타입
rust
// Option은 표준 라이브러리에 다음과 같이 정의되어 있습니다
// enum Option<T> {
//     Some(T),
//     None,
// }
 
fn find_user(users: &[User], id: u64) -> Option<&User> {
    users.iter().find(|u| u.id == id)
}
 
fn main() {
    let users = vec![
        User::new(1, "Alice".into(), "alice@example.com".into()),
        User::new(2, "Bob".into(), "bob@example.com".into()),
    ];
 
    // 패턴 매칭으로 처리
    match find_user(&users, 1) {
        Some(user) => println!("찾음: {:?}", user),
        None => println!("사용자 없음"),
    }
 
    // 메서드 체이닝으로 간결하게
    let name = find_user(&users, 3)
        .map(|u| u.display_name())
        .unwrap_or("알 수 없음");
 
    println!("이름: {}", name);
}

Option의 유용한 메서드들은 다음과 같습니다.

메서드설명
unwrap()Some이면 값 반환, None이면 패닉 (프로덕션에서 지양)
unwrap_or(default)Some이면 값, None이면 기본값
map(f)Some이면 f 적용, None이면 None
and_then(f)Some이면 f 적용 (f가 Option 반환), None이면 None
ok_or(err)Option을 Result로 변환
is_some() / is_none()값 존재 여부 확인

Result 타입

Result는 성공 또는 실패를 표현하는 열거형입니다. 백엔드 개발에서 가장 많이 사용하는 타입 중 하나입니다.

Result 타입
rust
// Result는 표준 라이브러리에 다음과 같이 정의되어 있습니다
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }
 
use std::fs;
use std::io;
 
fn read_config(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}
 
fn main() {
    match read_config("config.toml") {
        Ok(content) => println!("설정 내용: {}", content),
        Err(e) => eprintln!("설정 파일 읽기 실패: {}", e),
    }
}

? 연산자로 에러 전파

? 연산자는 Result를 반환하는 함수에서 에러를 간결하게 전파합니다. Ok이면 값을 추출하고, Err이면 즉시 현재 함수에서 에러를 반환합니다.

? 연산자 활용
rust
use std::fs;
use std::io;
 
// ? 연산자 없이 — 중첩된 match
fn load_config_verbose(path: &str) -> Result<Config, io::Error> {
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => return Err(e),
    };
    // 파싱 로직...
    Ok(Config { content })
}
 
// ? 연산자 사용 — 깔끔한 코드
fn load_config(path: &str) -> Result<Config, io::Error> {
    let content = fs::read_to_string(path)?;
    // 파싱 로직...
    Ok(Config { content })
}
 
struct Config { content: String }

? 연산자는 에러 타입 간의 From 변환도 자동으로 수행합니다. 이를 통해 서로 다른 에러 타입을 하나로 통합할 수 있습니다.

thiserror: 라이브러리용 에러 타입

thiserror는 커스텀 에러 타입을 쉽게 정의할 수 있게 해주는 derive 매크로 크레이트입니다. 주로 라이브러리나 공유 모듈에서 사용합니다.

thiserror로 커스텀 에러 정의
rust
use thiserror::Error;
 
#[derive(Debug, Error)]
enum AppError {
    #[error("사용자를 찾을 수 없습니다: id={0}")]
    UserNotFound(u64),
 
    #[error("인증 실패: {reason}")]
    AuthenticationFailed { reason: String },
 
    #[error("데이터베이스 오류")]
    Database(#[from] sqlx::Error),
 
    #[error("잘못된 요청: {0}")]
    BadRequest(String),
 
    #[error("내부 서버 오류")]
    Internal(#[from] anyhow::Error),
}

주요 특징은 다음과 같습니다.

  • #[error("...")]: Display 트레이트를 자동 구현합니다
  • #[from]: From 트레이트를 자동 구현하여 ? 연산자로 자동 변환을 지원합니다
  • 구조화된 에러 정보를 포함할 수 있습니다

anyhow: 애플리케이션용 에러 처리

anyhow는 애플리케이션 수준에서 다양한 에러를 유연하게 처리할 수 있게 해주는 크레이트입니다.

anyhow로 유연한 에러 처리
rust
use anyhow::{Context, Result, bail};
 
// anyhow::Result<T>는 Result<T, anyhow::Error>의 축약
async fn initialize_app() -> Result<AppState> {
    let config = load_config("config.toml")
        .context("설정 파일 로드 실패")?;
 
    let db_pool = create_db_pool(&config.database_url)
        .await
        .context("데이터베이스 연결 실패")?;
 
    if config.max_connections == 0 {
        bail!("max_connections는 0보다 커야 합니다");
    }
 
    Ok(AppState { config, db_pool })
}

thiserror + anyhow 조합 전략

2026년 Rust 커뮤니티의 모범 사례는 두 크레이트를 함께 사용하는 것입니다.

계층에러 처리도구
main / 핸들러anyhow::Resultanyhow
서비스 / 비즈니스 로직커스텀 에러 열거형thiserror
라이브러리 / 공유 모듈커스텀 에러 열거형thiserror
실전 에러 처리 구조
rust
// 도메인 에러 (thiserror)
#[derive(Debug, thiserror::Error)]
enum UserError {
    #[error("사용자를 찾을 수 없습니다: id={0}")]
    NotFound(u64),
 
    #[error("이메일이 이미 존재합니다: {0}")]
    DuplicateEmail(String),
 
    #[error("데이터베이스 오류")]
    Database(#[from] sqlx::Error),
}
 
// Axum 핸들러에서 에러를 HTTP 응답으로 변환
impl axum::response::IntoResponse for UserError {
    fn into_response(self) -> axum::response::Response {
        let (status, message) = match &self {
            UserError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
            UserError::DuplicateEmail(_) => (StatusCode::CONFLICT, self.to_string()),
            UserError::Database(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "내부 서버 오류".to_string(),
            ),
        };
 
        let body = Json(serde_json::json!({
            "error": message,
        }));
 
        (status, body).into_response()
    }
}
Warning

데이터베이스 에러와 같은 내부 에러를 API 응답에 그대로 노출하면 보안 취약점이 됩니다. 클라이언트에게는 일반적인 메시지를 반환하고, 상세 에러는 서버 로그에 기록하세요.

From 트레이트를 활용한 에러 변환

? 연산자가 에러를 자동 변환하는 원리는 From 트레이트에 있습니다.

From 트레이트 수동 구현
rust
#[derive(Debug)]
enum ServiceError {
    Database(String),
    Validation(String),
}
 
// io::Error를 ServiceError로 자동 변환
impl From<std::io::Error> for ServiceError {
    fn from(err: std::io::Error) -> Self {
        ServiceError::Database(err.to_string())
    }
}
 
fn read_data() -> Result<String, ServiceError> {
    let data = std::fs::read_to_string("data.txt")?; // io::Error → ServiceError 자동 변환
    Ok(data)
}

thiserror의 #[from] 속성이 바로 이 From 구현을 자동 생성하는 것입니다.

에러 전파 패턴 정리

실전 에러 전파 흐름
rust
use anyhow::{Context, Result};
 
async fn handle_create_user(
    Json(req): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>, AppError> {
    // 1. 입력 검증
    validate_email(&req.email)
        .map_err(|e| AppError::BadRequest(e.to_string()))?;
 
    // 2. 비즈니스 로직 — ?로 에러 전파
    let user = create_user(&req)
        .await?; // UserError가 AppError로 자동 변환 (From 트레이트)
 
    // 3. 응답 변환
    Ok(Json(UserResponse::from(user)))
}

정리

이 장에서는 Rust의 타입 시스템과 에러 처리를 다루었습니다.

  • 구조체, 열거형, 트레이트는 Rust의 타입 시스템 핵심입니다
  • Option은 null을 대체하며, Result는 에러를 타입으로 표현합니다
  • ? 연산자로 에러를 간결하게 전파하며, From 트레이트가 자동 변환을 담당합니다
  • thiserror(라이브러리용) + anyhow(애플리케이션용) 조합이 2026년 모범 사례입니다
  • 에러를 HTTP 응답으로 변환하는 IntoResponse 구현이 Axum에서의 핵심 패턴입니다

다음 장에서는 async/await와 Tokio 런타임을 다룹니다. 비동기 프로그래밍의 기초부터 Tokio의 동시성 패턴까지 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#rust

관련 글

프로그래밍

4장: async/await와 Tokio 런타임

Rust의 비동기 프로그래밍 기초부터 Future 트레이트, Tokio 런타임, spawn/select/join, 채널 기반 동시성 패턴까지 체계적으로 다룹니다.

2026년 2월 17일·15분
프로그래밍

2장: 소유권과 차용 체크 멘탈 모델

Rust의 핵심 개념인 소유권 규칙 3가지, Move 시맨틱스, 불변/가변 빌림, 라이프타임 기초를 백엔드 개발자 관점에서 체계적으로 다룹니다.

2026년 2월 13일·17분
프로그래밍

5장: Axum 웹 프레임워크 기초

Axum의 Tower 기반 아키텍처, 라우팅, 핸들러, 추출자, 응답 타입, 미들웨어까지 Hello World에서 CRUD API까지 실습합니다.

2026년 2월 19일·10분
이전 글2장: 소유권과 차용 체크 멘탈 모델
다음 글4장: async/await와 Tokio 런타임

댓글

목차

약 14분 남음
  • 학습 목표
  • 구조체 (Struct)
  • 열거형 (Enum)
  • 트레이트 (Trait)
  • Option 타입
  • Result 타입
  • ? 연산자로 에러 전파
  • thiserror: 라이브러리용 에러 타입
  • anyhow: 애플리케이션용 에러 처리
    • thiserror + anyhow 조합 전략
  • From 트레이트를 활용한 에러 변환
  • 에러 전파 패턴 정리
  • 정리