-
-
Notifications
You must be signed in to change notification settings - Fork 0
모범 사례
RAprogramm edited this page Jan 7, 2026
·
2 revisions
프로덕션에서 entity-derive를 효과적으로 사용하기 위한 가이드라인입니다.
데이터베이스 테이블당 하나의 엔티티. 단일 엔티티에서 복잡한 관계를 모델링하려고 하지 마세요.
// 좋음: 별도의 엔티티 #[derive(Entity)] #[entity(table = "users")] pub struct User { #[id] pub id: Uuid, #[field(create, update, response)] pub name: String, } #[derive(Entity)] #[entity(table = "posts")] pub struct Post { #[id] pub id: Uuid, #[field(create, response)] pub author_id: Uuid, // 참조, 임베딩 아님 #[field(create, update, response)] pub title: String, } // 나쁨: 관계를 임베딩하려는 시도 pub struct User { pub id: Uuid, pub posts: Vec<Post>, // 이렇게 하지 마세요 }
각 필드의 목적에 대해 명시적으로 하세요:
// 좋음: 명확한 의도 #[field(create, response)] // 한 번 설정, 항상 표시 pub email: String, #[field(update, response)] // 변경 가능, 항상 표시 pub display_name: Option<String>, #[field(response)] // 읽기 전용, 다른 곳에서 계산/관리 pub post_count: i64, #[field(skip)] // 절대 노출되지 않음 pub password_hash: String, // 나쁨: 모든 곳에 전부 #[field(create, update, response)] // 정말 전부에 필요한가? pub internal_id: String,
데이터베이스 스키마와 일치시키세요:
// 데이터베이스: email VARCHAR NOT NULL #[field(create, update, response)] pub email: String, // 데이터베이스: bio TEXT NULL #[field(update, response)] pub bio: Option<String>,
// 비밀번호 #[field(skip)] pub password_hash: String, // API 키 #[field(skip)] pub api_key: String, // 내부 토큰 #[field(skip)] pub refresh_token: Option<String>, // 응답에 포함되면 안 되는 개인정보 #[field(skip)] pub ssn: String, // 내부 감사 데이터 #[field(skip)] pub created_by_ip: String,
관리자 전용 데이터의 경우 별도의 엔티티를 고려하세요:
// 공개 엔티티 #[derive(Entity)] #[entity(table = "users")] pub struct User { #[id] pub id: Uuid, #[field(create, update, response)] pub name: String, #[field(skip)] pub admin_notes: Option<String>, } // 관리자 전용 엔티티 (같은 테이블, 다른 뷰) #[derive(Entity)] #[entity(table = "users", sql = "trait")] pub struct AdminUser { #[id] pub id: Uuid, #[field(response)] pub name: String, #[field(update, response)] // 이제 표시되고 편집 가능 pub admin_notes: Option<String>, #[field(response)] pub last_login_ip: Option<String>, }
생성된 SQL과 싸우지 마세요. JOIN이나 복잡한 로직이 필요하면 직접 구현하세요:
// 단순 CRUD - 전체 생성 사용 #[entity(table = "categories", sql = "full")] // 복잡한 쿼리 필요 - 직접 구현 #[entity(table = "posts", sql = "trait")]
대량 삽입의 경우 커스텀 메서드를 구현하세요:
#[entity(table = "events", sql = "trait")] pub struct Event { /* ... */ } pub trait EventBatchRepository { async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error>; } #[async_trait] impl EventBatchRepository for PgPool { async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error> { let mut tx = self.begin().await?; for event in events { let entity = Event::from(event); let insertable = InsertableEvent::from(&entity); // 트랜잭션 내에서 삽입 } tx.commit().await?; Ok(()) } }
관련 엔티티를 하나씩 로딩하는 대신 JOIN을 사용하세요:
// 나쁨: N+1 쿼리 let posts = pool.list(100, 0).await?; for post in &posts { let author = pool.find_user_by_id(post.author_id).await?; // N개의 쿼리! } // 좋음: JOIN을 사용한 단일 쿼리 let posts_with_authors = pool.list_with_authors(100, 0).await?; // 1개의 쿼리
#[cfg(test)] mod tests { use sqlx::PgPool; async fn setup_test_db() -> PgPool { let url = std::env::var("TEST_DATABASE_URL") .expect("TEST_DATABASE_URL must be set"); let pool = PgPool::connect(&url).await.unwrap(); // 마이그레이션 실행 sqlx::migrate!("./migrations") .run(&pool) .await .unwrap(); pool } #[tokio::test] async fn test_create_user() { let pool = setup_test_db().await; let request = CreateUserRequest { username: "test_user".into(), email: "test@example.com".into(), }; let user = pool.create(request).await.unwrap(); assert_eq!(user.username, "test_user"); } }
#[test] fn test_user_response_excludes_password() { let user = User { id: Uuid::new_v4(), username: "test".into(), email: "test@example.com".into(), password_hash: "secret_hash".into(), created_at: Utc::now(), }; let response = UserResponse::from(&user); // password_hash는 UserResponse에 없음 assert_eq!(response.username, "test"); // response를 통해 password_hash에 접근할 방법 없음 } #[test] fn test_update_request_is_partial() { let update = UpdateUserRequest { username: Some("new_name".into()), email: None, // 이메일 업데이트 안 함 }; assert!(update.username.is_some()); assert!(update.email.is_none()); }
src/
├── entities/ # 엔티티 정의
│ ├── mod.rs
│ ├── user.rs
│ ├── post.rs
│ └── comment.rs
├── repositories/ # 커스텀 리포지토리 확장
│ ├── mod.rs
│ └── post_search.rs
├── handlers/ # HTTP 핸들러
│ ├── mod.rs
│ ├── users.rs
│ └── posts.rs
├── services/ # 비즈니스 로직
│ ├── mod.rs
│ └── auth.rs
└── main.rs
// src/entities/mod.rs mod user; mod post; pub use user::*; pub use post::*;
// src/entities/auth/mod.rs mod user; mod session; mod api_key; pub use user::*; pub use session::*; pub use api_key::*;
// 잘못됨: password_hash가 Response에 포함됨! pub struct User { pub password_hash: String, } // 올바름 #[field(skip)] pub password_hash: String,
관련 데이터가 필요하면 sql = "trait"를 사용하고 직접 구현하세요.
기억하세요: UpdateRequest 필드는 Option<T>입니다. 적용 전에 확인하세요:
// 생성된 UpdateUserRequest는 name에 대해 Option<String>을 가짐 // 업데이트 로직은 None(변경 없음) vs Some(변경)을 처리해야 함
검증과 비즈니스 규칙은 서비스 레이어에 두세요, 핸들러가 아니라:
// 좋음: 서비스 레이어 impl UserService { pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, AppError> { self.validate_email(&request.email)?; self.check_username_available(&request.username).await?; self.pool.create(request).await.map_err(Into::into) } } // 나쁨: 핸들러에 로직 분산 pub async fn create_user(pool: State<PgPool>, request: Json<CreateUserRequest>) -> ... { // 검증 여기 // 비즈니스 규칙 여기 // 리포지토리 호출 여기 // 전부 섞여있음 }
배포 전:
- 모든 민감한 필드에
#[field(skip)]있음 - DTO가 API 계약 기대치와 일치
- 복잡한 쿼리는
sql = "trait"사용 - 통합 테스트가 리포지토리 메서드 커버
- 에러 처리가 일관됨
- list 엔드포인트에 페이지네이션 구현됨
- 쿼리 패턴에 대한 데이터베이스 인덱스 존재
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级