Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

모범 사례

masterror 기반 서비스를 예측 가능하게 유지하는 패턴: 타입 기반 도메인 오류, 하나의 안정적인 코드 분류 체계, 경계에서의 전송 매핑, 그리고 내부 정보를 절대 누출하지 않는 공개 메시지.

도메인 오류를 파생하고 한 번만 매핑

각 바운디드 컨텍스트를 #[derive(Error)]가 붙은 열거형으로 모델링하고 #[app_error(...)]AppError 매핑을 인라인으로 선언하세요. 파생은 Display, 래핑된 소스에 대한 From<...>, 그리고 AppError/AppCode로의 변환을 생성합니다 — 호출 지점마다 손으로 match를 작성할 필요가 없습니다.

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
pub enum UserError {
    #[error("user {0} not found")]
    #[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
    NotFound(u64),

    #[error("email already registered")]
    #[app_error(kind = AppErrorKind::Conflict, code = AppCode::Conflict, message)]
    DuplicateEmail,

    #[error("storage failure")]
    #[app_error(kind = AppErrorKind::Database, code = AppCode::Database)]
    Storage(#[from] std::io::Error)
}

let app: AppError = UserError::DuplicateEmail.into();
assert_eq!(app.kind, AppErrorKind::Conflict);

messageDisplay 출력이 클라이언트에게 보여도 안전한 변형에만 포함하세요. (Storage처럼) 생략하면 텍스트는 내부용으로 유지되고 — 클라이언트는 해당 종류의 제목만 보게 됩니다. 전체 속성 레퍼런스: Derive 매크로.

서비스당 하나의 AppCode 분류 체계

AppCode는 공개 API 계약이며, 클라이언트는 이를 기준으로 분기합니다. 집합을 작게, 문서화된 상태로, 서비스별로 유지하세요:

  • 내장 코드(NOT_FOUND, CONFLICT, VALIDATION 등)를 우선 사용하세요 — 이미 정규 HTTP/gRPC/problem-type 매핑을 갖고 있습니다.
  • 커스텀 코드는 호출 지점에서 인라인으로 만들지 말고 중앙에서 발행하세요:
use masterror::AppCode;

pub const CODE_PLAN_LIMIT: AppCode = AppCode::new("PLAN_LIMIT_EXCEEDED");

AppCode::newconst이며 SCREAMING_SNAKE_CASE가 아닌 것은 컴파일 타임에 패닉합니다. 런타임 문자열에는 AppCode::try_new를 사용하세요. 코드 이름 변경은 파괴적 API 변경입니다 — 추가는 열거형 변형 추가처럼 취급하세요.

전송 매핑은 경계에서만

도메인 및 서비스 계층은 AppResult<T>를 반환하며 HTTP에 대해 아무것도 모릅니다. 크레이트의 단일 IntoResponse/ResponseError/Status 구현이 핸들러 계층에서 매핑을 수행합니다:

async fn get_user(id: u64, repo: &Repo) -> masterror::AppResult<User> {
    repo.find(id).await?          // sqlx::Error -> AppError::NotFound/Database
}

비즈니스 로직에서 상태 코드를 직접 만들지 말고, 두 번째 응답 변환을 구현하지도 마세요 — 웹 프레임워크의 안정적인 AppErrorKind → status 테이블이 유일한 진실의 원천입니다.

민감 데이터는 리덕션하고 텔레메트리는 유지

독립적인 두 가지 조절 수단이 있습니다:

  • 메시지 리덕션err.redactable()(또는 #[masterror(...)]redact(message))은 로그에는 남기면서 와이어 페이로드에서 detail을 숨깁니다.
  • 필드 리덕션 — 메타데이터 직렬화 시 적용되는 필드별 정책:
use masterror::{AppError, FieldRedaction, field};

let err = AppError::bad_request("Invalid credentials")
    .with_field(field::str("email", "user@example.com").with_redaction(FieldRedaction::Hash))
    .with_field(field::str("card", "4111111111111111").with_redaction(FieldRedaction::Last4))
    .with_field(field::str("ip", "192.168.1.100").with_redaction(FieldRedaction::Redact));

Hash는 원시 문자열을 노출하지 않으면서 상관 분석 가치(같은 입력 → 같은 다이제스트)를 유지하고, Last4는 카드/토큰 접미사에 적합하며, Redact는 값을 완전히 제거합니다. 기본값은 None입니다. 사용자를 식별할 수 있는 것은 기본적으로 리덕션하고, 의식적으로만 예외를 두세요 — 그 반대가 아니라요.

Context vs 파생

  • 파생은 오류 타입이 도메인 어휘의 일부일 때 사용하세요: 변형이 있고, 시그니처에 나타나며, 매핑이 정적인 경우입니다.
  • Context(ResultExt::ctx 사용)는 호출 지점에서 인프라 오류를 즉석에서 래핑하고 분류가 타입이 아니라 작업에 따라 달라질 때 사용하세요:
#[cfg(feature = "std")] {
use masterror::{AppErrorKind, Context, ResultExt, field};

fn read_state() -> masterror::AppResult<Vec<u8>> {
    std::fs::read("/var/lib/app/state.bin").ctx(|| {
        Context::new(AppErrorKind::Internal)
            .with(field::str("path", "/var/lib/app/state.bin"))
            .track_caller()
    })
}
}

ctx는 지연 평가됩니다 — 클로저는 오류 경로에서만 실행됩니다. 사람이 읽을 수 있는 메모만 필요하면 단순한 .context("message")를 사용하세요. 자세한 내용: 컨텍스트와 메타데이터.

문자열이 아니라 종류와 코드를 테스트

포맷된 메시지가 아니라 안정적인 분류 체계에 대해 단언하세요:

use masterror::{AppCode, AppError, AppErrorKind, ProblemJson};

let err = AppError::not_found("user 42 missing");
assert_eq!(err.kind, AppErrorKind::NotFound);
assert_eq!(err.code, AppCode::NotFound);

let problem = ProblemJson::from_ref(&err);
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");
  • ProblemJson::from_ref를 사용하면 통합 테스트가 서버를 띄우지 않고도 정확한 와이어 계약을 단언할 수 있습니다.
  • mapping_for_code(&code)는 테이블 기반 테스트를 위한 정규 HTTP 상태, gRPC 코드 및 problem-type URI를 노출합니다.
  • 리덕션 테스트에서는 redactable() 오류에 대해 problem.detail.is_none()을 단언하고 metadata().iter_with_redaction() 정책을 확인하세요.

공개 메시지 vs 내부 텔레메트리

생성하는 모든 오류에 유용한 규칙:

채널내용
message / detail사람 지향적이고, 민감하지 않으며, 사용자에게 보여도 될 만큼 안정적
Metadata 필드ID, 시도 횟수, 엔드포인트, 소요 시간 — 리덕션 정책과 함께 로그/메트릭용
source 체인원시 하위 오류 — 로그에만 기록되며, 클라이언트에게 절대 직렬화되지 않음

masterror는 마지막 행을 강제합니다(소스는 와이어 페이로드에 절대 기록되지 않음). 하지만 첫 두 행은 여러분의 책임입니다: 문자열에 브라우저에 출력하지 않을 내용이 포함되어 있다면, 리덕션 정책과 함께 메타데이터에 넣거나 오류를 redactable()로 표시하세요.

함께 보기: Derive 매크로 · 컨텍스트와 메타데이터 · 오류 종류와 코드 · 마이그레이션