Skip to content

Navigation Menu

Sign in
Sign up

커맨드

RAprogramm edited this page Jan 7, 2026 · 4 revisions

일반적인 CRUD 대신 비즈니스 지향 커맨드를 정의합니다. 커맨드는 도메인 언어를 API에 도입하고 CQRS(Command Query Responsibility Segregation) 패턴을 가능하게 합니다.

빠른 시작

#[derive(Entity)]
#[entity(table = "users", commands)]
#[command(Register)]
#[command(UpdateEmail: email)]
#[command(Deactivate, requires_id)]
pub struct User {
 #[id]
 pub id: Uuid,
 #[field(create, update, response)]
 pub email: String,
 #[field(create, response)]
 pub name: String,
 #[field(response)]
 pub active: bool,
}

생성되는 코드

커맨드 구조체

/// User의 Register 작업을 위한 커맨드 페이로드.
#[derive(Debug, Clone)]
pub struct RegisterUser {
 pub email: String,
 pub name: String,
}
/// User의 UpdateEmail 작업을 위한 커맨드 페이로드.
#[derive(Debug, Clone)]
pub struct UpdateEmailUser {
 pub id: Uuid,
 pub email: String,
}
/// User의 Deactivate 작업을 위한 커맨드 페이로드.
#[derive(Debug, Clone)]
pub struct DeactivateUser {
 pub id: Uuid,
}

커맨드 열거형

/// User 엔티티용 커맨드 열거형.
#[derive(Debug, Clone)]
pub enum UserCommand {
 Register(RegisterUser),
 UpdateEmail(UpdateEmailUser),
 Deactivate(DeactivateUser),
}
impl EntityCommand for UserCommand {
 fn kind(&self) -> CommandKind {
 match self {
 UserCommand::Register(_) => CommandKind::Create,
 UserCommand::UpdateEmail(_) => CommandKind::Update,
 UserCommand::Deactivate(_) => CommandKind::Custom,
 }
 }
 fn name(&self) -> &'static str {
 match self {
 UserCommand::Register(_) => "Register",
 UserCommand::UpdateEmail(_) => "UpdateEmail",
 UserCommand::Deactivate(_) => "Deactivate",
 }
 }
}

결과 열거형

/// User 커맨드 실행 결과 열거형.
#[derive(Debug, Clone)]
pub enum UserCommandResult {
 Register(User),
 UpdateEmail(User),
 Deactivate,
}

핸들러 트레이트

/// User 커맨드 처리를 위한 비동기 트레이트.
#[async_trait]
pub trait UserCommandHandler: Send + Sync {
 type Error: std::error::Error + Send + Sync;
 type Context: Send + Sync;
 /// 커맨드를 적절한 핸들러로 디스패치.
 async fn handle(&self, cmd: UserCommand, ctx: &Self::Context)
 -> Result<UserCommandResult, Self::Error>;
 /// Register 커맨드 처리.
 async fn handle_register(&self, cmd: RegisterUser, ctx: &Self::Context)
 -> Result<User, Self::Error>;
 /// UpdateEmail 커맨드 처리.
 async fn handle_update_email(&self, cmd: UpdateEmailUser, ctx: &Self::Context)
 -> Result<User, Self::Error>;
 /// Deactivate 커맨드 처리.
 async fn handle_deactivate(&self, cmd: DeactivateUser, ctx: &Self::Context)
 -> Result<(), Self::Error>;
}

커맨드 구문 참조

기본 커맨드

모든 #[field(create)] 필드를 사용합니다:

#[command(Register)]
// 생성됨: RegisterUser { email, name }

특정 필드

지정된 필드만 사용합니다 (자동으로 requires_id 추가):

#[command(UpdateEmail: email)]
// 생성됨: UpdateEmailUser { id, email }
#[command(UpdateProfile: name, bio, avatar)]
// 생성됨: UpdateProfileUser { id, name, bio, avatar }

ID만 있는 커맨드

ID 필드만 추가합니다:

#[command(Deactivate, requires_id)]
// 생성됨: DeactivateUser { id }
#[command(Delete, requires_id, kind = "delete")]
// 생성됨: DeleteUser { id }, () 반환

커스텀 페이로드

외부 구조체를 사용합니다:

pub struct TransferPayload {
 pub from_account: Uuid,
 pub to_account: Uuid,
 pub amount: i64,
}
#[command(Transfer, payload = "TransferPayload")]
// TransferPayload를 직접 사용

커스텀 결과

커스텀 결과 타입을 사용합니다:

pub struct TransferResult {
 pub transaction_id: Uuid,
 pub success: bool,
}
#[command(Transfer, payload = "TransferPayload", result = "TransferResult")]
// 엔티티 대신 TransferResult 반환

소스 옵션

사용할 필드를 제어합니다:

#[command(Create, source = "create")] // #[field(create)] 필드 사용 (기본값)
#[command(Modify, source = "update")] // #[field(update)] 필드 사용 (선택적)
#[command(Ping, source = "none")] // 페이로드 필드 없음

종류 힌트

결과 타입 추론에 영향을 줍니다:

#[command(Create, kind = "create")] // 엔티티 반환 (기본값)
#[command(Update, kind = "update")] // 엔티티 반환
#[command(Remove, kind = "delete")] // () 반환
#[command(Process, kind = "custom")] // 소스에서 추론

구현 예제

use async_trait::async_trait;
struct UserHandler {
 pool: PgPool,
 email_service: EmailService,
}
struct RequestContext {
 user_id: Option<Uuid>,
 correlation_id: Uuid,
}
#[async_trait]
impl UserCommandHandler for UserHandler {
 type Error = AppError;
 type Context = RequestContext;
 async fn handle(&self, cmd: UserCommand, ctx: &Self::Context)
 -> Result<UserCommandResult, Self::Error>
 {
 match cmd {
 UserCommand::Register(c) => {
 let user = self.handle_register(c, ctx).await?;
 Ok(UserCommandResult::Register(user))
 }
 UserCommand::UpdateEmail(c) => {
 let user = self.handle_update_email(c, ctx).await?;
 Ok(UserCommandResult::UpdateEmail(user))
 }
 UserCommand::Deactivate(c) => {
 self.handle_deactivate(c, ctx).await?;
 Ok(UserCommandResult::Deactivate)
 }
 }
 }
 async fn handle_register(&self, cmd: RegisterUser, ctx: &Self::Context)
 -> Result<User, Self::Error>
 {
 // 검증
 if cmd.email.is_empty() {
 return Err(AppError::Validation("이메일이 필요합니다".into()));
 }
 // 사용자 생성
 let user = User {
 id: Uuid::now_v7(),
 email: cmd.email.to_lowercase(),
 name: cmd.name,
 active: true,
 };
 // 저장
 sqlx::query(
 "INSERT INTO users (id, email, name, active) VALUES (1,ドル 2,ドル 3,ドル 4ドル)"
 )
 .bind(user.id)
 .bind(&user.email)
 .bind(&user.name)
 .bind(user.active)
 .execute(&self.pool)
 .await?;
 // 부수 효과
 self.email_service.send_welcome(&user.email).await?;
 Ok(user)
 }
 async fn handle_update_email(&self, cmd: UpdateEmailUser, ctx: &Self::Context)
 -> Result<User, Self::Error>
 {
 // 권한 확인
 if ctx.user_id != Some(cmd.id) {
 return Err(AppError::Forbidden("다른 사용자의 이메일을 업데이트할 수 없습니다".into()));
 }
 // 업데이트
 let user: User = sqlx::query_as(
 "UPDATE users SET email = 1ドル WHERE id = 2ドル RETURNING *"
 )
 .bind(&cmd.email.to_lowercase())
 .bind(cmd.id)
 .fetch_one(&self.pool)
 .await?;
 // 인증 발송
 self.email_service.send_verification(&user.email).await?;
 Ok(user)
 }
 async fn handle_deactivate(&self, cmd: DeactivateUser, ctx: &Self::Context)
 -> Result<(), Self::Error>
 {
 sqlx::query("UPDATE users SET active = false WHERE id = 1ドル")
 .bind(cmd.id)
 .execute(&self.pool)
 .await?;
 Ok(())
 }
}

커맨드 사용하기

async fn register_user(
 handler: &impl UserCommandHandler,
 email: String,
 name: String,
) -> Result<User, AppError> {
 let cmd = RegisterUser { email, name };
 let ctx = RequestContext {
 user_id: None,
 correlation_id: Uuid::new_v4(),
 };
 match handler.handle(UserCommand::Register(cmd), &ctx).await? {
 UserCommandResult::Register(user) => Ok(user),
 _ => unreachable!(),
 }
}
// 또는 특정 핸들러를 직접 호출
async fn update_email(
 handler: &impl UserCommandHandler,
 user_id: Uuid,
 new_email: String,
 ctx: &RequestContext,
) -> Result<User, AppError> {
 let cmd = UpdateEmailUser {
 id: user_id,
 email: new_email,
 };
 handler.handle_update_email(cmd, ctx).await
}

EntityCommand 트레이트

모든 커맨드 열거형은 EntityCommand 트레이트를 구현합니다:

use entity_derive::{EntityCommand, CommandKind};
let cmd = UserCommand::Register(register_data);
// 커맨드 메타데이터 가져오기
assert_eq!(cmd.name(), "Register");
assert!(matches!(cmd.kind(), CommandKind::Create));
// 패턴 매칭
match cmd.kind() {
 CommandKind::Create => println!("엔티티 생성"),
 CommandKind::Update => println!("엔티티 업데이트"),
 CommandKind::Delete => println!("엔티티 삭제"),
 CommandKind::Custom => println!("커스텀 작업"),
}

커맨드 훅

commandshooks가 모두 활성화된 경우:

#[derive(Entity)]
#[entity(table = "orders", commands, hooks)]
#[command(Place)]
#[command(Cancel, requires_id)]
pub struct Order { /* ... */ }

생성되는 훅:

#[async_trait]
pub trait OrderHooks: Send + Sync {
 type Error: std::error::Error + Send + Sync;
 // 표준 CRUD 훅...
 // 커맨드 전용 훅
 async fn before_command(&self, cmd: &OrderCommand) -> Result<(), Self::Error>;
 async fn after_command(&self, cmd: &OrderCommand, result: &OrderCommandResult) -> Result<(), Self::Error>;
}

사용법:

async fn before_command(&self, cmd: &OrderCommand) -> Result<(), Self::Error> {
 // 커맨드 로깅
 tracing::info!(command = cmd.name(), "커맨드 처리 중");
 // 권한 부여
 match cmd {
 OrderCommand::Cancel(c) => {
 let order = self.find_order(c.id).await?;
 if order.status == "shipped" {
 return Err(AppError::Forbidden("배송된 주문은 취소할 수 없습니다".into()));
 }
 }
 _ => {}
 }
 Ok(())
}
async fn after_command(&self, cmd: &OrderCommand, result: &OrderCommandResult) -> Result<(), Self::Error> {
 // 감사 로그
 match (cmd, result) {
 (OrderCommand::Place(c), OrderCommandResult::Place(order)) => {
 self.audit_log("order_placed", order.id).await?;
 }
 (OrderCommand::Cancel(c), OrderCommandResult::Cancel) => {
 self.audit_log("order_cancelled", c.id).await?;
 }
 }
 Ok(())
}

모범 사례

  1. 도메인 언어 — 비즈니스 용어 사용: CreateUser 대신 RegisterUser
  2. 단일 책임 — 하나의 커맨드 = 하나의 비즈니스 작업
  3. 명시적 의도 — 커맨드 이름은 작업을 설명해야 함
  4. 핸들러에서 검증 — 검증 로직은 커맨드 핸들러에 유지
  5. 가능하면 멱등성 — 안전하게 재시도할 수 있도록 커맨드 설계
  6. 컨텍스트 사용 — 요청 메타데이터(user, correlation ID)는 컨텍스트로 전달

CQRS 패턴

커맨드는 CQRS의 절반입니다. 쿼리 측면을 위해 프로젝션과 결합하세요:

#[derive(Entity)]
#[entity(table = "orders", commands)]
#[projection(Summary: id, status, total_cents, created_at)]
#[projection(Details: id, status, items, shipping_address, total_cents)]
#[command(Place)]
#[command(Ship, requires_id)]
#[command(Cancel, requires_id)]
pub struct Order { /* ... */ }
// 커맨드 (쓰기 측면)
let result = handler.handle(OrderCommand::Place(place_order), &ctx).await?;
// 쿼리 (읽기 측면)
let summary = repo.find_by_id_summary(order_id).await?;
let details = repo.find_by_id_details(order_id).await?;

참고

  • — 커맨드 훅을 포함한 생명주기 훅
  • 이벤트 — 감사 로깅을 위한 이벤트 생성
  • 속성 — 전체 속성 참조

🌐 Language

🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文


🇬🇧 English

Home

Getting Started

Features

Advanced


🇷🇺 Русский

Главная

Начало работы

Возможности

Продвинутое


🇰🇷 한국어

시작하기

기능

고급


🇪🇸 Español

Inicio

Comenzando

Características

Avanzado


🇨🇳 中文

首页

入门

功能

高级


📚 Reference

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /