모범 사례
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);
message는 Display 출력이 클라이언트에게 보여도 안전한 변형에만
포함하세요. (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::new는 const이며 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 매크로 · 컨텍스트와 메타데이터 · 오류 종류와 코드 · 마이그레이션