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

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|커스텀-SQL]]복잡한 쿼리
[[예제|예제]]실제 사용 사례
[[웹 프레임워크|웹-프레임워크]]Axum, Actix 통합
[[모범 사례|모범-사례]]프로덕션 가이드라인

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