Skip to content
RAprogramm edited this page Jan 7, 2026 · 4 revisions

entity-derive

모든 것을 지배하는 하나의 매크로

English Русский 한국어 Español 中文


무엇인가요?

entity-derive는 단일 엔티티 정의에서 완전한 도메인 레이어를 생성하는 Rust 프로시저 매크로입니다. 단순한 CRUD가 아닌 — 이벤트, 훅, 커맨드, 타입 안전 필터링을 갖춘 아키텍처 프레임워크입니다.


문제점

약 10개 엔티티를 가진 일반적인 Rust 백엔드:

컴포넌트 코드 라인 수 문제점
DTO (Create, Update, Response) 엔티티당 ~60줄 수동 동기화, 누락된 필드
Repository trait + impl 엔티티당 ~150줄 런타임 SQL 오류, 복사-붙여넣기
Entity ↔ DTO 매핑 엔티티당 ~40줄 데이터 누출 (Response에 password_hash)
검증 및 훅 서비스에 분산 중복, 단일 소스 없음
이벤트/감사 없거나 임시 방편 변경 이력 없음

총계: 10개 엔티티에 ~2500줄 보일러플레이트. 그리고 스키마 변경마다 5곳 이상 수동 수정 필요.


해결책

#[derive(Entity)]
#[entity(table = "users", events, hooks, commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]
pub struct User {
    #[id]
    pub id: Uuid,

    #[field(create, update, response)]
    #[filter(like)]
    pub email: String,

    #[field(skip)]  // API에 절대 노출 안됨
    pub password_hash: String,

    #[auto]
    #[field(response)]
    pub created_at: DateTime<Utc>,
}

15줄 → 완전한 도메인 레이어:

  • CreateUserRequest, UpdateUserRequest, UserResponse
  • 타입 안전 SQL을 갖춘 UserRepository
  • UserEvent::Created, Updated, Deleted
  • 비즈니스 로직을 위한 UserHooks
  • RegisterUser, DeactivateUser 커맨드
  • 필터링을 위한 UserQuery

초보자도 쉽게

최소 예제 — 10줄:

#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
    #[id]
    pub id: Uuid,

    #[field(create, update, response)]
    pub title: String,

    #[field(create, update, response)]
    pub content: String,
}

완료. 다음을 갖게 됩니다:

  • CreatePostRequest, UpdatePostRequest, PostResponse
  • create(), find_by_id(), update(), delete(), list()를 가진 PostRepository
  • PostgreSQL용 타입 안전 SQL
  • 모든 것이 바로 작동

마법 없음. cargo expand 실행 — 직접 작성했을 코드와 정확히 동일. 단지 오류 없이 몇 초 만에.


왜 이벤트인가?

문제: CRUD 애플리케이션에는 이력이 없습니다. 누가 레코드를 변경했나요? 언제? 이전에 무엇이 있었나요? 감사에는 별도의 인프라와 규율이 필요합니다.

해결책: #[entity(events)]가 타입화된 이벤트를 생성합니다:

pub enum UserEvent {
    Created(User),
    Updated { id: Uuid, changes: UpdateUserRequest },
    Deleted(Uuid),
}

장점:

  • 기본 감사 — 이벤트를 구독하고 로그에 저장
  • Event Sourcing — 이벤트 이력에서 상태 복원 가능
  • 통합 — Kafka, WebSocket 알림, 캐시 무효화
  • 디버깅 — 모든 엔티티의 완전한 변경 이력

왜 훅인가?

문제: 비즈니스 로직이 분산되어 있습니다. 이메일 검증 — 컨트롤러에. 비밀번호 해싱 — 서비스에. 이메일 발송 — 별도 워커에. 사용자 생성 로직을 어디서 찾아야 하나요?

해결책: #[entity(hooks)]가 라이프사이클을 중앙화합니다:

impl UserHooks for MyHooks {
    async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Error> {
        dto.email = dto.email.to_lowercase();  // 정규화
        validate_email(&dto.email)?;            // 검증
        Ok(())
    }

    async fn after_create(&self, user: &User) -> Result<(), Error> {
        self.mailer.send_welcome(user).await?;  // 비즈니스 액션
        Ok(())
    }
}

장점:

  • 단일 장소 — 모든 엔티티 로직이 정의 옆에
  • 예측 가능성 — 무엇이 언제 실행되는지 명확
  • 테스트 용이성 — 훅을 모킹하고 독립적으로 테스트 가능
  • 구성 — 다른 컨텍스트에 다른 구현

왜 커맨드인가?

문제: REST API는 의도를 숨깁니다. POST /users — 등록인가요? 관리자 생성인가요? CSV 임포트인가요? PATCH /users/123 — 비활성화? 이메일 변경? 차단?

해결책: #[command(...)]가 비즈니스 도메인을 표현합니다:

#[command(Register)]           // 자가 등록
#[command(Invite)]             // 관리자 초대
#[command(Deactivate, requires_id)]  // 계정 비활성화
#[command(Ban, requires_id)]   // 위반으로 차단

비교:

// CRUD (무슨 일이 일어나고 있나요?)
pool.update(user_id, UpdateUserRequest { active: Some(false), ..default() }).await?;

// 커맨드 (명확한 의도)
handler.handle(DeactivateUser { id: user_id }).await?;

장점:

  • 자체 문서화 API — 커맨드 이름 = 비즈니스 어휘
  • 다른 로직DeactivateBan은 다른 사이드 이펙트 가능
  • CQRS 준비 — 커맨드를 라우팅, 로깅, 재시도하기 쉬움
  • 타입 안전성 — 컴파일러가 커맨드 존재 확인

왜 타입 안전 필터링인가?

문제: 문자열 쿼리 파라미터는 런타임 오류의 원인:

GET /users?stauts=active  // 오타 — 조용히 무시됨
GET /users?created_at=tomorrow  // 잘못된 날짜 — 런타임 패닉

해결책: #[filter]가 타입화된 구조체를 생성:

let query = UserQuery {
    email: Some("@company.com".into()),  // ILIKE '%@company.com%'
    created_at_min: Some(week_ago),       // >= week_ago
    created_at_max: Some(now),            // <= now
    ..Default::default()
};

let users = pool.list_filtered(&query, 100, 0).await?;

장점:

  • 컴파일 타임 검사 — 필드 이름 오타 = 컴파일 오류
  • 타입 안전성DateTimeString과 비교 불가
  • 자동완성 — IDE가 사용 가능한 필터 제안
  • SQL 인젝션 보호 — 파라미터가 바인딩됨, 연결되지 않음

투명성

매크로는 로직을 숨기지 않습니다. 생성되는 모든 것은 일반 Rust 코드이며:

  • 읽기cargo expand가 모든 생성 코드 표시
  • 이해 — 런타임 리플렉션 없음, 구조체와 트레이트만
  • 재정의sql = "trait"로 자체 SQL 작성
  • 디버그 — 컴파일러 오류가 매크로 내부가 아닌 사용자 코드를 가리킴
// 무엇이 생성되는지 이해하고 싶다면?
cargo expand --lib | grep -A 50 "impl UserRepository"

Zero magic. 매크로가 깨지면 — 항상 수동으로 코드 작성 가능. 락인 없음.


Rust의 전체 파워

컴파일 타임 보장

#[field(skip)]
pub password_hash: String,

이것은 "이 필드를 직렬화하지 마세요"라는 런타임 검사가 아닙니다. UserResponse 구조체에서 필드의 물리적 부재입니다. 실수로 반환할 수 없음 — 필드가 단순히 존재하지 않음.

Zero-cost 추상화

생성된 코드는:

  • Box/dyn 없는 일반 struct
  • 중간 레이어 없는 직접 sqlx 호출
  • 핫 패스에 #[inline]
  • 필요 이상의 할당 없음

벤치마크: 생성된 리포지토리는 수작업 코드와 동일한 속도로 실행. 왜냐하면 동일한 코드이기 때문.

기본 Async

// 모든 것이 async, 모든 것이 Send + Sync
let user = pool.find_by_id(id).await?;
let users = pool.list(100, 0).await?;

tokio, async-std, 모든 async 런타임과 완전 호환.

엄격한 타이핑

// 컴파일 오류: 그런 필드 없음
let query = UserQuery { naem: "test".into(), ..default() };
                        ^^^^ unknown field

// 컴파일 오류: 잘못된 타입
let query = UserQuery { created_at_min: "yesterday".into(), ..default() };
                                        ^^^^^^^^^^^^ expected DateTime<Utc>

코드가 컴파일되면 — 올바르게 작동합니다.


전문적인 아키텍처

Clean Architecture 준비

Domain Layer (entity-derive)
├── Entities — #[derive(Entity)]
├── DTOs — CreateRequest, UpdateRequest, Response
├── Repository Trait — 저장소 추상화
├── Events — 도메인 이벤트
├── Commands — 비즈니스 작업
└── Hooks — 라이프사이클 로직

Infrastructure Layer (사용자 코드)
├── Repository Impl — PgPool 자동 또는 커스텀
├── Event Handlers — 이벤트 구독
├── Command Handlers — 비즈니스 로직 구현
└── External Services — 통합

깨끗한 분리. Domain은 HTTP, 데이터베이스, Kafka에 대해 알지 못함. 그것들은 구현 세부사항.

CQRS/Event Sourcing 준비

// Command side
handler.handle(RegisterUser { email, name }).await?;

// Query side
let users = pool.list_filtered(&query, 100, 0).await?;

// Event side
match event {
    UserEvent::Created(user) => kafka.send("user.created", &user).await?,
    UserEvent::Updated { id, changes } => audit_log.record(id, changes).await?,
    _ => {}
}

간단한 CRUD를 원하시나요? 가능합니다. 전체 CQRS를 원하시나요? commandsevents 활성화. 아키텍처가 프로젝트와 함께 성장.

확장성

레벨 1: 기본 CRUD

#[entity(table = "users")]

레벨 2: + 필터링

#[entity(table = "users")]
// + 필드에 #[filter]

레벨 3: + 이벤트와 훅

#[entity(table = "users", events, hooks)]

레벨 4: + CQRS 커맨드

#[entity(table = "users", events, hooks, commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]

레벨 5: 전체 제어

#[entity(table = "users", sql = "trait", events, hooks, commands)]
// 자체 SQL, 자체 로직, 모든 DTO와 타입 유지

간단하게 시작. 성장하면서 기능 추가. 재작성하지 말고 — 확장.


보안

#[field(skip)]
pub password_hash: String,

skip은: 이 필드가 절대 나타나지 않음을 의미:

  • CreateUserRequest (외부에서 전달 불가)
  • UpdateUserRequest (API로 수정 불가)
  • UserResponse (클라이언트에 실수로 반환 불가)

password_hash를 다루는 유일한 방법 — 코드에서 엔티티를 통해 직접. 설계상 누출 불가능.


왜 이것이 대단한가

측면 얻는 것
개발 속도 하루 대신 한 시간에 10개 엔티티
신뢰성 모든 것의 컴파일 타임 검증
보안 실수로 데이터 누출 불가능
성능 Zero-cost, 수작업 코드와 동일
명확성 투명한 생성, 마법 없음
유연성 하나의 속성으로 단순 CRUD에서 CQRS까지
확장성 프로젝트와 함께 성장하는 아키텍처
유지보수성 단일 진실 소스, 적은 버그

문서

주제 설명
속성 전체 속성 참조
필터링 타입 안전 쿼리 필터링
관계 belongs_tohas_many
이벤트 라이프사이클 이벤트
Before/after 훅
커맨드 CQRS 패턴
커스텀 SQL 복잡한 쿼리
예제 실제 사용 사례
웹 프레임워크 Axum, Actix 통합
모범 사례 프로덕션 가이드라인

이것은 삶을 지시하는 프레임워크가 아닙니다. 이것은 루틴을 제거하고 올바르게 구축할 수 있게 해주는 도구입니다.


🌐 언어 변경

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


Crates.io Documentation License GitHub

Clone this wiki locally