@su-record/vibe 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +35 -35
- package/.claude/settings.local.json +24 -25
- package/.claude/vibe/constitution.md +184 -184
- package/.claude/vibe/rules/core/communication-guide.md +104 -104
- package/.claude/vibe/rules/core/development-philosophy.md +52 -52
- package/.claude/vibe/rules/core/quick-start.md +120 -120
- package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
- package/.claude/vibe/rules/languages/go.md +396 -396
- package/.claude/vibe/rules/languages/java-spring.md +586 -586
- package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
- package/.claude/vibe/rules/languages/python-django.md +371 -371
- package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
- package/.claude/vibe/rules/languages/rust.md +425 -425
- package/.claude/vibe/rules/languages/swift-ios.md +516 -516
- package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
- package/.claude/vibe/rules/languages/typescript-node.md +375 -375
- package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
- package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
- package/.claude/vibe/rules/languages/typescript-react.md +525 -525
- package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
- package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
- package/.claude/vibe/rules/quality/checklist.md +276 -276
- package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
- package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
- package/.claude/vibe/rules/standards/code-structure.md +291 -291
- package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
- package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
- package/.claude/vibe/setup.sh +31 -31
- package/.claude/vibe/templates/constitution-template.md +184 -184
- package/.claude/vibe/templates/contract-backend-template.md +517 -517
- package/.claude/vibe/templates/contract-frontend-template.md +594 -594
- package/.claude/vibe/templates/feature-template.md +96 -96
- package/.claude/vibe/templates/spec-template.md +199 -199
- package/CLAUDE.md +345 -323
- package/LICENSE +21 -21
- package/README.md +744 -724
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/e2e-tester.md +266 -266
- package/agents/explorer.md +48 -48
- package/agents/implementer.md +53 -53
- package/agents/research/best-practices-agent.md +139 -139
- package/agents/research/codebase-patterns-agent.md +147 -147
- package/agents/research/framework-docs-agent.md +181 -181
- package/agents/research/security-advisory-agent.md +167 -167
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +152 -152
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +119 -119
- package/agents/tester.md +49 -49
- package/agents/ui-previewer.md +137 -137
- package/commands/vibe.analyze.md +245 -180
- package/commands/vibe.reason.md +223 -183
- package/commands/vibe.review.md +200 -136
- package/commands/vibe.run.md +838 -836
- package/commands/vibe.spec.md +419 -383
- package/commands/vibe.utils.md +101 -101
- package/commands/vibe.verify.md +282 -241
- package/dist/cli/index.js +385 -385
- package/dist/lib/MemoryManager.d.ts.map +1 -1
- package/dist/lib/MemoryManager.js +119 -114
- package/dist/lib/MemoryManager.js.map +1 -1
- package/dist/lib/PythonParser.js +108 -108
- package/dist/lib/gemini-mcp.js +15 -15
- package/dist/lib/gemini-oauth.js +35 -35
- package/dist/lib/gpt-mcp.js +17 -17
- package/dist/lib/gpt-oauth.js +44 -44
- package/dist/tools/analytics/getUsageAnalytics.js +12 -12
- package/dist/tools/index.d.ts +50 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +61 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
- package/dist/tools/planning/generatePrd.js +46 -46
- package/dist/tools/prompt/enhancePromptGemini.js +160 -160
- package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/hooks/hooks.json +121 -103
- package/package.json +73 -69
- package/skills/git-worktree.md +178 -178
- package/skills/priority-todos.md +236 -236
|
@@ -1,425 +1,425 @@
|
|
|
1
|
-
# 🦀 Rust 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (core에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄
|
|
11
|
-
✅ 중첩 ≤ 3단계
|
|
12
|
-
✅ Cyclomatic complexity ≤ 10
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Rust 특화 규칙
|
|
16
|
-
|
|
17
|
-
### 1. 에러 처리 (Result, Option)
|
|
18
|
-
|
|
19
|
-
```rust
|
|
20
|
-
// ❌ unwrap() 남용
|
|
21
|
-
let content = fs::read_to_string("config.json").unwrap();
|
|
22
|
-
|
|
23
|
-
// ✅ ? 연산자와 적절한 에러 처리
|
|
24
|
-
fn read_config(path: &str) -> Result<Config, ConfigError> {
|
|
25
|
-
let content = fs::read_to_string(path)
|
|
26
|
-
.map_err(|e| ConfigError::IoError(e))?;
|
|
27
|
-
|
|
28
|
-
let config: Config = serde_json::from_str(&content)
|
|
29
|
-
.map_err(|e| ConfigError::ParseError(e))?;
|
|
30
|
-
|
|
31
|
-
Ok(config)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ✅ 커스텀 에러 타입 (thiserror)
|
|
35
|
-
use thiserror::Error;
|
|
36
|
-
|
|
37
|
-
#[derive(Error, Debug)]
|
|
38
|
-
pub enum AppError {
|
|
39
|
-
#[error("설정 파일을 읽을 수 없습니다: {0}")]
|
|
40
|
-
ConfigError(#[from] std::io::Error),
|
|
41
|
-
|
|
42
|
-
#[error("잘못된 요청입니다: {0}")]
|
|
43
|
-
BadRequest(String),
|
|
44
|
-
|
|
45
|
-
#[error("리소스를 찾을 수 없습니다: {resource} (ID: {id})")]
|
|
46
|
-
NotFound { resource: String, id: String },
|
|
47
|
-
|
|
48
|
-
#[error("데이터베이스 오류: {0}")]
|
|
49
|
-
DatabaseError(#[from] sqlx::Error),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ✅ anyhow로 간편한 에러 처리 (애플리케이션 레벨)
|
|
53
|
-
use anyhow::{Context, Result};
|
|
54
|
-
|
|
55
|
-
fn process_file(path: &str) -> Result<String> {
|
|
56
|
-
let content = fs::read_to_string(path)
|
|
57
|
-
.context(format!("파일을 읽을 수 없습니다: {}", path))?;
|
|
58
|
-
|
|
59
|
-
Ok(content)
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 2. 구조체와 트레이트
|
|
64
|
-
|
|
65
|
-
```rust
|
|
66
|
-
// ✅ 구조체 정의
|
|
67
|
-
use chrono::{DateTime, Utc};
|
|
68
|
-
use serde::{Deserialize, Serialize};
|
|
69
|
-
use uuid::Uuid;
|
|
70
|
-
|
|
71
|
-
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
72
|
-
pub struct User {
|
|
73
|
-
pub id: Uuid,
|
|
74
|
-
pub email: String,
|
|
75
|
-
pub name: String,
|
|
76
|
-
pub created_at: DateTime<Utc>,
|
|
77
|
-
pub updated_at: DateTime<Utc>,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
impl User {
|
|
81
|
-
pub fn new(email: String, name: String) -> Self {
|
|
82
|
-
let now = Utc::now();
|
|
83
|
-
Self {
|
|
84
|
-
id: Uuid::new_v4(),
|
|
85
|
-
email,
|
|
86
|
-
name,
|
|
87
|
-
created_at: now,
|
|
88
|
-
updated_at: now,
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ✅ 트레이트 정의
|
|
94
|
-
#[async_trait]
|
|
95
|
-
pub trait UserRepository: Send + Sync {
|
|
96
|
-
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, AppError>;
|
|
97
|
-
async fn find_by_email(&self, email: &str) -> Result<Option<User>, AppError>;
|
|
98
|
-
async fn create(&self, user: &User) -> Result<User, AppError>;
|
|
99
|
-
async fn update(&self, user: &User) -> Result<User, AppError>;
|
|
100
|
-
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ✅ 트레이트 구현
|
|
104
|
-
pub struct PostgresUserRepository {
|
|
105
|
-
pool: PgPool,
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
#[async_trait]
|
|
109
|
-
impl UserRepository for PostgresUserRepository {
|
|
110
|
-
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, AppError> {
|
|
111
|
-
let user = sqlx::query_as!(
|
|
112
|
-
User,
|
|
113
|
-
"SELECT * FROM users WHERE id = $1",
|
|
114
|
-
id
|
|
115
|
-
)
|
|
116
|
-
.fetch_optional(&self.pool)
|
|
117
|
-
.await?;
|
|
118
|
-
|
|
119
|
-
Ok(user)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ... 다른 메서드 구현
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### 3. Actix-web / Axum 핸들러
|
|
127
|
-
|
|
128
|
-
```rust
|
|
129
|
-
// ✅ Axum 핸들러
|
|
130
|
-
use axum::{
|
|
131
|
-
extract::{Path, State, Json},
|
|
132
|
-
http::StatusCode,
|
|
133
|
-
response::IntoResponse,
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
pub async fn get_user(
|
|
137
|
-
State(repo): State<Arc<dyn UserRepository>>,
|
|
138
|
-
Path(id): Path<Uuid>,
|
|
139
|
-
) -> Result<Json<User>, AppError> {
|
|
140
|
-
let user = repo
|
|
141
|
-
.find_by_id(id)
|
|
142
|
-
.await?
|
|
143
|
-
.ok_or(AppError::NotFound {
|
|
144
|
-
resource: "사용자".to_string(),
|
|
145
|
-
id: id.to_string(),
|
|
146
|
-
})?;
|
|
147
|
-
|
|
148
|
-
Ok(Json(user))
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
pub async fn create_user(
|
|
152
|
-
State(repo): State<Arc<dyn UserRepository>>,
|
|
153
|
-
Json(dto): Json<CreateUserDto>,
|
|
154
|
-
) -> Result<(StatusCode, Json<User>), AppError> {
|
|
155
|
-
let user = User::new(dto.email, dto.name);
|
|
156
|
-
let created = repo.create(&user).await?;
|
|
157
|
-
|
|
158
|
-
Ok((StatusCode::CREATED, Json(created)))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ✅ Actix-web 핸들러
|
|
162
|
-
use actix_web::{web, HttpResponse, Result};
|
|
163
|
-
|
|
164
|
-
pub async fn get_user(
|
|
165
|
-
repo: web::Data<dyn UserRepository>,
|
|
166
|
-
path: web::Path<Uuid>,
|
|
167
|
-
) -> Result<HttpResponse, AppError> {
|
|
168
|
-
let id = path.into_inner();
|
|
169
|
-
let user = repo
|
|
170
|
-
.find_by_id(id)
|
|
171
|
-
.await?
|
|
172
|
-
.ok_or(AppError::NotFound {
|
|
173
|
-
resource: "사용자".to_string(),
|
|
174
|
-
id: id.to_string(),
|
|
175
|
-
})?;
|
|
176
|
-
|
|
177
|
-
Ok(HttpResponse::Ok().json(user))
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### 4. 소유권과 생명주기
|
|
182
|
-
|
|
183
|
-
```rust
|
|
184
|
-
// ❌ 불필요한 클론
|
|
185
|
-
fn process(data: &Vec<String>) -> Vec<String> {
|
|
186
|
-
let cloned = data.clone(); // 불필요
|
|
187
|
-
cloned.iter().map(|s| s.to_uppercase()).collect()
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ✅ 참조 활용
|
|
191
|
-
fn process(data: &[String]) -> Vec<String> {
|
|
192
|
-
data.iter().map(|s| s.to_uppercase()).collect()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ✅ 생명주기 명시
|
|
196
|
-
pub struct UserService<'a> {
|
|
197
|
-
repo: &'a dyn UserRepository,
|
|
198
|
-
cache: &'a dyn CacheRepository,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
impl<'a> UserService<'a> {
|
|
202
|
-
pub fn new(
|
|
203
|
-
repo: &'a dyn UserRepository,
|
|
204
|
-
cache: &'a dyn CacheRepository,
|
|
205
|
-
) -> Self {
|
|
206
|
-
Self { repo, cache }
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ✅ 소유권 이전 vs 빌려오기
|
|
211
|
-
fn take_ownership(s: String) { /* s의 소유권을 가짐 */ }
|
|
212
|
-
fn borrow(s: &str) { /* s를 빌려옴 */ }
|
|
213
|
-
fn borrow_mut(s: &mut String) { /* s를 가변 빌려옴 */ }
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### 5. 비동기 처리 (Tokio)
|
|
217
|
-
|
|
218
|
-
```rust
|
|
219
|
-
// ✅ 비동기 함수
|
|
220
|
-
use tokio::time::{sleep, Duration};
|
|
221
|
-
|
|
222
|
-
pub async fn fetch_with_retry<T, F, Fut>(
|
|
223
|
-
f: F,
|
|
224
|
-
max_retries: u32,
|
|
225
|
-
) -> Result<T, AppError>
|
|
226
|
-
where
|
|
227
|
-
F: Fn() -> Fut,
|
|
228
|
-
Fut: std::future::Future<Output = Result<T, AppError>>,
|
|
229
|
-
{
|
|
230
|
-
let mut attempts = 0;
|
|
231
|
-
|
|
232
|
-
loop {
|
|
233
|
-
match f().await {
|
|
234
|
-
Ok(result) => return Ok(result),
|
|
235
|
-
Err(e) if attempts < max_retries => {
|
|
236
|
-
attempts += 1;
|
|
237
|
-
let delay = Duration::from_millis(100 * 2_u64.pow(attempts));
|
|
238
|
-
sleep(delay).await;
|
|
239
|
-
}
|
|
240
|
-
Err(e) => return Err(e),
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ✅ 동시 실행
|
|
246
|
-
use futures::future::join_all;
|
|
247
|
-
|
|
248
|
-
pub async fn fetch_users(ids: Vec<Uuid>) -> Vec<Result<User, AppError>> {
|
|
249
|
-
let futures: Vec<_> = ids
|
|
250
|
-
.into_iter()
|
|
251
|
-
.map(|id| fetch_user(id))
|
|
252
|
-
.collect();
|
|
253
|
-
|
|
254
|
-
join_all(futures).await
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ✅ tokio::spawn으로 태스크 생성
|
|
258
|
-
pub async fn background_job() {
|
|
259
|
-
tokio::spawn(async {
|
|
260
|
-
loop {
|
|
261
|
-
process_queue().await;
|
|
262
|
-
sleep(Duration::from_secs(60)).await;
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### 6. 테스트
|
|
269
|
-
|
|
270
|
-
```rust
|
|
271
|
-
#[cfg(test)]
|
|
272
|
-
mod tests {
|
|
273
|
-
use super::*;
|
|
274
|
-
use mockall::predicate::*;
|
|
275
|
-
use mockall::mock;
|
|
276
|
-
|
|
277
|
-
// ✅ Mock 생성
|
|
278
|
-
mock! {
|
|
279
|
-
pub UserRepo {}
|
|
280
|
-
|
|
281
|
-
#[async_trait]
|
|
282
|
-
impl UserRepository for UserRepo {
|
|
283
|
-
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, AppError>;
|
|
284
|
-
async fn create(&self, user: &User) -> Result<User, AppError>;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ✅ 단위 테스트
|
|
289
|
-
#[tokio::test]
|
|
290
|
-
async fn test_get_user_success() {
|
|
291
|
-
let mut mock_repo = MockUserRepo::new();
|
|
292
|
-
let user_id = Uuid::new_v4();
|
|
293
|
-
let expected_user = User::new("test@example.com".into(), "테스트".into());
|
|
294
|
-
|
|
295
|
-
mock_repo
|
|
296
|
-
.expect_find_by_id()
|
|
297
|
-
.with(eq(user_id))
|
|
298
|
-
.returning(move |_| Ok(Some(expected_user.clone())));
|
|
299
|
-
|
|
300
|
-
let service = UserService::new(Arc::new(mock_repo));
|
|
301
|
-
let result = service.get_user(user_id).await;
|
|
302
|
-
|
|
303
|
-
assert!(result.is_ok());
|
|
304
|
-
assert_eq!(result.unwrap().email, "test@example.com");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ✅ 에러 케이스 테스트
|
|
308
|
-
#[tokio::test]
|
|
309
|
-
async fn test_get_user_not_found() {
|
|
310
|
-
let mut mock_repo = MockUserRepo::new();
|
|
311
|
-
let user_id = Uuid::new_v4();
|
|
312
|
-
|
|
313
|
-
mock_repo
|
|
314
|
-
.expect_find_by_id()
|
|
315
|
-
.returning(|_| Ok(None));
|
|
316
|
-
|
|
317
|
-
let service = UserService::new(Arc::new(mock_repo));
|
|
318
|
-
let result = service.get_user(user_id).await;
|
|
319
|
-
|
|
320
|
-
assert!(matches!(result, Err(AppError::NotFound { .. })));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### 7. 의존성 주입
|
|
326
|
-
|
|
327
|
-
```rust
|
|
328
|
-
// ✅ 생성자 주입
|
|
329
|
-
pub struct UserService {
|
|
330
|
-
repo: Arc<dyn UserRepository>,
|
|
331
|
-
cache: Arc<dyn CacheRepository>,
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
impl UserService {
|
|
335
|
-
pub fn new(
|
|
336
|
-
repo: Arc<dyn UserRepository>,
|
|
337
|
-
cache: Arc<dyn CacheRepository>,
|
|
338
|
-
) -> Self {
|
|
339
|
-
Self { repo, cache }
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ✅ Builder 패턴
|
|
344
|
-
#[derive(Default)]
|
|
345
|
-
pub struct ServerBuilder {
|
|
346
|
-
port: Option<u16>,
|
|
347
|
-
host: Option<String>,
|
|
348
|
-
timeout: Option<Duration>,
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
impl ServerBuilder {
|
|
352
|
-
pub fn new() -> Self {
|
|
353
|
-
Self::default()
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
pub fn port(mut self, port: u16) -> Self {
|
|
357
|
-
self.port = Some(port);
|
|
358
|
-
self
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
pub fn host(mut self, host: impl Into<String>) -> Self {
|
|
362
|
-
self.host = Some(host.into());
|
|
363
|
-
self
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
pub fn timeout(mut self, timeout: Duration) -> Self {
|
|
367
|
-
self.timeout = Some(timeout);
|
|
368
|
-
self
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
pub fn build(self) -> Server {
|
|
372
|
-
Server {
|
|
373
|
-
port: self.port.unwrap_or(8080),
|
|
374
|
-
host: self.host.unwrap_or_else(|| "127.0.0.1".into()),
|
|
375
|
-
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// 사용
|
|
381
|
-
let server = ServerBuilder::new()
|
|
382
|
-
.port(3000)
|
|
383
|
-
.host("0.0.0.0")
|
|
384
|
-
.timeout(Duration::from_secs(60))
|
|
385
|
-
.build();
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
## 파일 구조
|
|
389
|
-
|
|
390
|
-
```
|
|
391
|
-
project/
|
|
392
|
-
├── src/
|
|
393
|
-
│ ├── main.rs # 엔트리포인트
|
|
394
|
-
│ ├── lib.rs # 라이브러리 루트
|
|
395
|
-
│ ├── config.rs # 설정
|
|
396
|
-
│ ├── error.rs # 에러 정의
|
|
397
|
-
│ ├── domain/ # 도메인 모델
|
|
398
|
-
│ │ ├── mod.rs
|
|
399
|
-
│ │ └── user.rs
|
|
400
|
-
│ ├── handlers/ # HTTP 핸들러
|
|
401
|
-
│ │ ├── mod.rs
|
|
402
|
-
│ │ └── user.rs
|
|
403
|
-
│ ├── services/ # 비즈니스 로직
|
|
404
|
-
│ │ ├── mod.rs
|
|
405
|
-
│ │ └── user.rs
|
|
406
|
-
│ ├── repositories/ # 데이터 액세스
|
|
407
|
-
│ │ ├── mod.rs
|
|
408
|
-
│ │ └── user.rs
|
|
409
|
-
│ └── middleware/ # 미들웨어
|
|
410
|
-
├── tests/ # 통합 테스트
|
|
411
|
-
├── migrations/ # DB 마이그레이션
|
|
412
|
-
├── Cargo.toml
|
|
413
|
-
└── Cargo.lock
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
## 체크리스트
|
|
417
|
-
|
|
418
|
-
- [ ] unwrap()/expect() 최소화, ? 연산자 활용
|
|
419
|
-
- [ ] thiserror/anyhow로 에러 처리
|
|
420
|
-
- [ ] 트레이트로 추상화, 의존성 주입
|
|
421
|
-
- [ ] Clone 최소화, 참조 활용
|
|
422
|
-
- [ ] async/await 적절히 사용
|
|
423
|
-
- [ ] clippy 경고 해결
|
|
424
|
-
- [ ] cargo fmt 적용
|
|
425
|
-
- [ ] #[cfg(test)] 모듈로 테스트 작성
|
|
1
|
+
# 🦀 Rust 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Rust 특화 규칙
|
|
16
|
+
|
|
17
|
+
### 1. 에러 처리 (Result, Option)
|
|
18
|
+
|
|
19
|
+
```rust
|
|
20
|
+
// ❌ unwrap() 남용
|
|
21
|
+
let content = fs::read_to_string("config.json").unwrap();
|
|
22
|
+
|
|
23
|
+
// ✅ ? 연산자와 적절한 에러 처리
|
|
24
|
+
fn read_config(path: &str) -> Result<Config, ConfigError> {
|
|
25
|
+
let content = fs::read_to_string(path)
|
|
26
|
+
.map_err(|e| ConfigError::IoError(e))?;
|
|
27
|
+
|
|
28
|
+
let config: Config = serde_json::from_str(&content)
|
|
29
|
+
.map_err(|e| ConfigError::ParseError(e))?;
|
|
30
|
+
|
|
31
|
+
Ok(config)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ✅ 커스텀 에러 타입 (thiserror)
|
|
35
|
+
use thiserror::Error;
|
|
36
|
+
|
|
37
|
+
#[derive(Error, Debug)]
|
|
38
|
+
pub enum AppError {
|
|
39
|
+
#[error("설정 파일을 읽을 수 없습니다: {0}")]
|
|
40
|
+
ConfigError(#[from] std::io::Error),
|
|
41
|
+
|
|
42
|
+
#[error("잘못된 요청입니다: {0}")]
|
|
43
|
+
BadRequest(String),
|
|
44
|
+
|
|
45
|
+
#[error("리소스를 찾을 수 없습니다: {resource} (ID: {id})")]
|
|
46
|
+
NotFound { resource: String, id: String },
|
|
47
|
+
|
|
48
|
+
#[error("데이터베이스 오류: {0}")]
|
|
49
|
+
DatabaseError(#[from] sqlx::Error),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ✅ anyhow로 간편한 에러 처리 (애플리케이션 레벨)
|
|
53
|
+
use anyhow::{Context, Result};
|
|
54
|
+
|
|
55
|
+
fn process_file(path: &str) -> Result<String> {
|
|
56
|
+
let content = fs::read_to_string(path)
|
|
57
|
+
.context(format!("파일을 읽을 수 없습니다: {}", path))?;
|
|
58
|
+
|
|
59
|
+
Ok(content)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 2. 구조체와 트레이트
|
|
64
|
+
|
|
65
|
+
```rust
|
|
66
|
+
// ✅ 구조체 정의
|
|
67
|
+
use chrono::{DateTime, Utc};
|
|
68
|
+
use serde::{Deserialize, Serialize};
|
|
69
|
+
use uuid::Uuid;
|
|
70
|
+
|
|
71
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
72
|
+
pub struct User {
|
|
73
|
+
pub id: Uuid,
|
|
74
|
+
pub email: String,
|
|
75
|
+
pub name: String,
|
|
76
|
+
pub created_at: DateTime<Utc>,
|
|
77
|
+
pub updated_at: DateTime<Utc>,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl User {
|
|
81
|
+
pub fn new(email: String, name: String) -> Self {
|
|
82
|
+
let now = Utc::now();
|
|
83
|
+
Self {
|
|
84
|
+
id: Uuid::new_v4(),
|
|
85
|
+
email,
|
|
86
|
+
name,
|
|
87
|
+
created_at: now,
|
|
88
|
+
updated_at: now,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ✅ 트레이트 정의
|
|
94
|
+
#[async_trait]
|
|
95
|
+
pub trait UserRepository: Send + Sync {
|
|
96
|
+
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, AppError>;
|
|
97
|
+
async fn find_by_email(&self, email: &str) -> Result<Option<User>, AppError>;
|
|
98
|
+
async fn create(&self, user: &User) -> Result<User, AppError>;
|
|
99
|
+
async fn update(&self, user: &User) -> Result<User, AppError>;
|
|
100
|
+
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ✅ 트레이트 구현
|
|
104
|
+
pub struct PostgresUserRepository {
|
|
105
|
+
pool: PgPool,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[async_trait]
|
|
109
|
+
impl UserRepository for PostgresUserRepository {
|
|
110
|
+
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, AppError> {
|
|
111
|
+
let user = sqlx::query_as!(
|
|
112
|
+
User,
|
|
113
|
+
"SELECT * FROM users WHERE id = $1",
|
|
114
|
+
id
|
|
115
|
+
)
|
|
116
|
+
.fetch_optional(&self.pool)
|
|
117
|
+
.await?;
|
|
118
|
+
|
|
119
|
+
Ok(user)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ... 다른 메서드 구현
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. Actix-web / Axum 핸들러
|
|
127
|
+
|
|
128
|
+
```rust
|
|
129
|
+
// ✅ Axum 핸들러
|
|
130
|
+
use axum::{
|
|
131
|
+
extract::{Path, State, Json},
|
|
132
|
+
http::StatusCode,
|
|
133
|
+
response::IntoResponse,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
pub async fn get_user(
|
|
137
|
+
State(repo): State<Arc<dyn UserRepository>>,
|
|
138
|
+
Path(id): Path<Uuid>,
|
|
139
|
+
) -> Result<Json<User>, AppError> {
|
|
140
|
+
let user = repo
|
|
141
|
+
.find_by_id(id)
|
|
142
|
+
.await?
|
|
143
|
+
.ok_or(AppError::NotFound {
|
|
144
|
+
resource: "사용자".to_string(),
|
|
145
|
+
id: id.to_string(),
|
|
146
|
+
})?;
|
|
147
|
+
|
|
148
|
+
Ok(Json(user))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pub async fn create_user(
|
|
152
|
+
State(repo): State<Arc<dyn UserRepository>>,
|
|
153
|
+
Json(dto): Json<CreateUserDto>,
|
|
154
|
+
) -> Result<(StatusCode, Json<User>), AppError> {
|
|
155
|
+
let user = User::new(dto.email, dto.name);
|
|
156
|
+
let created = repo.create(&user).await?;
|
|
157
|
+
|
|
158
|
+
Ok((StatusCode::CREATED, Json(created)))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ✅ Actix-web 핸들러
|
|
162
|
+
use actix_web::{web, HttpResponse, Result};
|
|
163
|
+
|
|
164
|
+
pub async fn get_user(
|
|
165
|
+
repo: web::Data<dyn UserRepository>,
|
|
166
|
+
path: web::Path<Uuid>,
|
|
167
|
+
) -> Result<HttpResponse, AppError> {
|
|
168
|
+
let id = path.into_inner();
|
|
169
|
+
let user = repo
|
|
170
|
+
.find_by_id(id)
|
|
171
|
+
.await?
|
|
172
|
+
.ok_or(AppError::NotFound {
|
|
173
|
+
resource: "사용자".to_string(),
|
|
174
|
+
id: id.to_string(),
|
|
175
|
+
})?;
|
|
176
|
+
|
|
177
|
+
Ok(HttpResponse::Ok().json(user))
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 4. 소유권과 생명주기
|
|
182
|
+
|
|
183
|
+
```rust
|
|
184
|
+
// ❌ 불필요한 클론
|
|
185
|
+
fn process(data: &Vec<String>) -> Vec<String> {
|
|
186
|
+
let cloned = data.clone(); // 불필요
|
|
187
|
+
cloned.iter().map(|s| s.to_uppercase()).collect()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ✅ 참조 활용
|
|
191
|
+
fn process(data: &[String]) -> Vec<String> {
|
|
192
|
+
data.iter().map(|s| s.to_uppercase()).collect()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ✅ 생명주기 명시
|
|
196
|
+
pub struct UserService<'a> {
|
|
197
|
+
repo: &'a dyn UserRepository,
|
|
198
|
+
cache: &'a dyn CacheRepository,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
impl<'a> UserService<'a> {
|
|
202
|
+
pub fn new(
|
|
203
|
+
repo: &'a dyn UserRepository,
|
|
204
|
+
cache: &'a dyn CacheRepository,
|
|
205
|
+
) -> Self {
|
|
206
|
+
Self { repo, cache }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ✅ 소유권 이전 vs 빌려오기
|
|
211
|
+
fn take_ownership(s: String) { /* s의 소유권을 가짐 */ }
|
|
212
|
+
fn borrow(s: &str) { /* s를 빌려옴 */ }
|
|
213
|
+
fn borrow_mut(s: &mut String) { /* s를 가변 빌려옴 */ }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 5. 비동기 처리 (Tokio)
|
|
217
|
+
|
|
218
|
+
```rust
|
|
219
|
+
// ✅ 비동기 함수
|
|
220
|
+
use tokio::time::{sleep, Duration};
|
|
221
|
+
|
|
222
|
+
pub async fn fetch_with_retry<T, F, Fut>(
|
|
223
|
+
f: F,
|
|
224
|
+
max_retries: u32,
|
|
225
|
+
) -> Result<T, AppError>
|
|
226
|
+
where
|
|
227
|
+
F: Fn() -> Fut,
|
|
228
|
+
Fut: std::future::Future<Output = Result<T, AppError>>,
|
|
229
|
+
{
|
|
230
|
+
let mut attempts = 0;
|
|
231
|
+
|
|
232
|
+
loop {
|
|
233
|
+
match f().await {
|
|
234
|
+
Ok(result) => return Ok(result),
|
|
235
|
+
Err(e) if attempts < max_retries => {
|
|
236
|
+
attempts += 1;
|
|
237
|
+
let delay = Duration::from_millis(100 * 2_u64.pow(attempts));
|
|
238
|
+
sleep(delay).await;
|
|
239
|
+
}
|
|
240
|
+
Err(e) => return Err(e),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ✅ 동시 실행
|
|
246
|
+
use futures::future::join_all;
|
|
247
|
+
|
|
248
|
+
pub async fn fetch_users(ids: Vec<Uuid>) -> Vec<Result<User, AppError>> {
|
|
249
|
+
let futures: Vec<_> = ids
|
|
250
|
+
.into_iter()
|
|
251
|
+
.map(|id| fetch_user(id))
|
|
252
|
+
.collect();
|
|
253
|
+
|
|
254
|
+
join_all(futures).await
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ✅ tokio::spawn으로 태스크 생성
|
|
258
|
+
pub async fn background_job() {
|
|
259
|
+
tokio::spawn(async {
|
|
260
|
+
loop {
|
|
261
|
+
process_queue().await;
|
|
262
|
+
sleep(Duration::from_secs(60)).await;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 6. 테스트
|
|
269
|
+
|
|
270
|
+
```rust
|
|
271
|
+
#[cfg(test)]
|
|
272
|
+
mod tests {
|
|
273
|
+
use super::*;
|
|
274
|
+
use mockall::predicate::*;
|
|
275
|
+
use mockall::mock;
|
|
276
|
+
|
|
277
|
+
// ✅ Mock 생성
|
|
278
|
+
mock! {
|
|
279
|
+
pub UserRepo {}
|
|
280
|
+
|
|
281
|
+
#[async_trait]
|
|
282
|
+
impl UserRepository for UserRepo {
|
|
283
|
+
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, AppError>;
|
|
284
|
+
async fn create(&self, user: &User) -> Result<User, AppError>;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ✅ 단위 테스트
|
|
289
|
+
#[tokio::test]
|
|
290
|
+
async fn test_get_user_success() {
|
|
291
|
+
let mut mock_repo = MockUserRepo::new();
|
|
292
|
+
let user_id = Uuid::new_v4();
|
|
293
|
+
let expected_user = User::new("test@example.com".into(), "테스트".into());
|
|
294
|
+
|
|
295
|
+
mock_repo
|
|
296
|
+
.expect_find_by_id()
|
|
297
|
+
.with(eq(user_id))
|
|
298
|
+
.returning(move |_| Ok(Some(expected_user.clone())));
|
|
299
|
+
|
|
300
|
+
let service = UserService::new(Arc::new(mock_repo));
|
|
301
|
+
let result = service.get_user(user_id).await;
|
|
302
|
+
|
|
303
|
+
assert!(result.is_ok());
|
|
304
|
+
assert_eq!(result.unwrap().email, "test@example.com");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ✅ 에러 케이스 테스트
|
|
308
|
+
#[tokio::test]
|
|
309
|
+
async fn test_get_user_not_found() {
|
|
310
|
+
let mut mock_repo = MockUserRepo::new();
|
|
311
|
+
let user_id = Uuid::new_v4();
|
|
312
|
+
|
|
313
|
+
mock_repo
|
|
314
|
+
.expect_find_by_id()
|
|
315
|
+
.returning(|_| Ok(None));
|
|
316
|
+
|
|
317
|
+
let service = UserService::new(Arc::new(mock_repo));
|
|
318
|
+
let result = service.get_user(user_id).await;
|
|
319
|
+
|
|
320
|
+
assert!(matches!(result, Err(AppError::NotFound { .. })));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### 7. 의존성 주입
|
|
326
|
+
|
|
327
|
+
```rust
|
|
328
|
+
// ✅ 생성자 주입
|
|
329
|
+
pub struct UserService {
|
|
330
|
+
repo: Arc<dyn UserRepository>,
|
|
331
|
+
cache: Arc<dyn CacheRepository>,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
impl UserService {
|
|
335
|
+
pub fn new(
|
|
336
|
+
repo: Arc<dyn UserRepository>,
|
|
337
|
+
cache: Arc<dyn CacheRepository>,
|
|
338
|
+
) -> Self {
|
|
339
|
+
Self { repo, cache }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ✅ Builder 패턴
|
|
344
|
+
#[derive(Default)]
|
|
345
|
+
pub struct ServerBuilder {
|
|
346
|
+
port: Option<u16>,
|
|
347
|
+
host: Option<String>,
|
|
348
|
+
timeout: Option<Duration>,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
impl ServerBuilder {
|
|
352
|
+
pub fn new() -> Self {
|
|
353
|
+
Self::default()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
pub fn port(mut self, port: u16) -> Self {
|
|
357
|
+
self.port = Some(port);
|
|
358
|
+
self
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
pub fn host(mut self, host: impl Into<String>) -> Self {
|
|
362
|
+
self.host = Some(host.into());
|
|
363
|
+
self
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pub fn timeout(mut self, timeout: Duration) -> Self {
|
|
367
|
+
self.timeout = Some(timeout);
|
|
368
|
+
self
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
pub fn build(self) -> Server {
|
|
372
|
+
Server {
|
|
373
|
+
port: self.port.unwrap_or(8080),
|
|
374
|
+
host: self.host.unwrap_or_else(|| "127.0.0.1".into()),
|
|
375
|
+
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 사용
|
|
381
|
+
let server = ServerBuilder::new()
|
|
382
|
+
.port(3000)
|
|
383
|
+
.host("0.0.0.0")
|
|
384
|
+
.timeout(Duration::from_secs(60))
|
|
385
|
+
.build();
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## 파일 구조
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
project/
|
|
392
|
+
├── src/
|
|
393
|
+
│ ├── main.rs # 엔트리포인트
|
|
394
|
+
│ ├── lib.rs # 라이브러리 루트
|
|
395
|
+
│ ├── config.rs # 설정
|
|
396
|
+
│ ├── error.rs # 에러 정의
|
|
397
|
+
│ ├── domain/ # 도메인 모델
|
|
398
|
+
│ │ ├── mod.rs
|
|
399
|
+
│ │ └── user.rs
|
|
400
|
+
│ ├── handlers/ # HTTP 핸들러
|
|
401
|
+
│ │ ├── mod.rs
|
|
402
|
+
│ │ └── user.rs
|
|
403
|
+
│ ├── services/ # 비즈니스 로직
|
|
404
|
+
│ │ ├── mod.rs
|
|
405
|
+
│ │ └── user.rs
|
|
406
|
+
│ ├── repositories/ # 데이터 액세스
|
|
407
|
+
│ │ ├── mod.rs
|
|
408
|
+
│ │ └── user.rs
|
|
409
|
+
│ └── middleware/ # 미들웨어
|
|
410
|
+
├── tests/ # 통합 테스트
|
|
411
|
+
├── migrations/ # DB 마이그레이션
|
|
412
|
+
├── Cargo.toml
|
|
413
|
+
└── Cargo.lock
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## 체크리스트
|
|
417
|
+
|
|
418
|
+
- [ ] unwrap()/expect() 최소화, ? 연산자 활용
|
|
419
|
+
- [ ] thiserror/anyhow로 에러 처리
|
|
420
|
+
- [ ] 트레이트로 추상화, 의존성 주입
|
|
421
|
+
- [ ] Clone 최소화, 참조 활용
|
|
422
|
+
- [ ] async/await 적절히 사용
|
|
423
|
+
- [ ] clippy 경고 해결
|
|
424
|
+
- [ ] cargo fmt 적용
|
|
425
|
+
- [ ] #[cfg(test)] 모듈로 테스트 작성
|