@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.
Files changed (98) hide show
  1. package/.claude/settings.json +35 -35
  2. package/.claude/settings.local.json +24 -25
  3. package/.claude/vibe/constitution.md +184 -184
  4. package/.claude/vibe/rules/core/communication-guide.md +104 -104
  5. package/.claude/vibe/rules/core/development-philosophy.md +52 -52
  6. package/.claude/vibe/rules/core/quick-start.md +120 -120
  7. package/.claude/vibe/rules/languages/dart-flutter.md +509 -509
  8. package/.claude/vibe/rules/languages/go.md +396 -396
  9. package/.claude/vibe/rules/languages/java-spring.md +586 -586
  10. package/.claude/vibe/rules/languages/kotlin-android.md +491 -491
  11. package/.claude/vibe/rules/languages/python-django.md +371 -371
  12. package/.claude/vibe/rules/languages/python-fastapi.md +386 -386
  13. package/.claude/vibe/rules/languages/rust.md +425 -425
  14. package/.claude/vibe/rules/languages/swift-ios.md +516 -516
  15. package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -441
  16. package/.claude/vibe/rules/languages/typescript-node.md +375 -375
  17. package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -521
  18. package/.claude/vibe/rules/languages/typescript-react-native.md +446 -446
  19. package/.claude/vibe/rules/languages/typescript-react.md +525 -525
  20. package/.claude/vibe/rules/languages/typescript-vue.md +353 -353
  21. package/.claude/vibe/rules/quality/bdd-contract-testing.md +388 -388
  22. package/.claude/vibe/rules/quality/checklist.md +276 -276
  23. package/.claude/vibe/rules/quality/testing-strategy.md +437 -437
  24. package/.claude/vibe/rules/standards/anti-patterns.md +369 -369
  25. package/.claude/vibe/rules/standards/code-structure.md +291 -291
  26. package/.claude/vibe/rules/standards/complexity-metrics.md +312 -312
  27. package/.claude/vibe/rules/standards/naming-conventions.md +198 -198
  28. package/.claude/vibe/setup.sh +31 -31
  29. package/.claude/vibe/templates/constitution-template.md +184 -184
  30. package/.claude/vibe/templates/contract-backend-template.md +517 -517
  31. package/.claude/vibe/templates/contract-frontend-template.md +594 -594
  32. package/.claude/vibe/templates/feature-template.md +96 -96
  33. package/.claude/vibe/templates/spec-template.md +199 -199
  34. package/CLAUDE.md +345 -323
  35. package/LICENSE +21 -21
  36. package/README.md +744 -724
  37. package/agents/compounder.md +261 -261
  38. package/agents/diagrammer.md +178 -178
  39. package/agents/e2e-tester.md +266 -266
  40. package/agents/explorer.md +48 -48
  41. package/agents/implementer.md +53 -53
  42. package/agents/research/best-practices-agent.md +139 -139
  43. package/agents/research/codebase-patterns-agent.md +147 -147
  44. package/agents/research/framework-docs-agent.md +181 -181
  45. package/agents/research/security-advisory-agent.md +167 -167
  46. package/agents/review/architecture-reviewer.md +107 -107
  47. package/agents/review/complexity-reviewer.md +116 -116
  48. package/agents/review/data-integrity-reviewer.md +88 -88
  49. package/agents/review/git-history-reviewer.md +103 -103
  50. package/agents/review/performance-reviewer.md +86 -86
  51. package/agents/review/python-reviewer.md +152 -152
  52. package/agents/review/rails-reviewer.md +139 -139
  53. package/agents/review/react-reviewer.md +144 -144
  54. package/agents/review/security-reviewer.md +80 -80
  55. package/agents/review/simplicity-reviewer.md +140 -140
  56. package/agents/review/test-coverage-reviewer.md +116 -116
  57. package/agents/review/typescript-reviewer.md +127 -127
  58. package/agents/searcher.md +54 -54
  59. package/agents/simplifier.md +119 -119
  60. package/agents/tester.md +49 -49
  61. package/agents/ui-previewer.md +137 -137
  62. package/commands/vibe.analyze.md +245 -180
  63. package/commands/vibe.reason.md +223 -183
  64. package/commands/vibe.review.md +200 -136
  65. package/commands/vibe.run.md +838 -836
  66. package/commands/vibe.spec.md +419 -383
  67. package/commands/vibe.utils.md +101 -101
  68. package/commands/vibe.verify.md +282 -241
  69. package/dist/cli/index.js +385 -385
  70. package/dist/lib/MemoryManager.d.ts.map +1 -1
  71. package/dist/lib/MemoryManager.js +119 -114
  72. package/dist/lib/MemoryManager.js.map +1 -1
  73. package/dist/lib/PythonParser.js +108 -108
  74. package/dist/lib/gemini-mcp.js +15 -15
  75. package/dist/lib/gemini-oauth.js +35 -35
  76. package/dist/lib/gpt-mcp.js +17 -17
  77. package/dist/lib/gpt-oauth.js +44 -44
  78. package/dist/tools/analytics/getUsageAnalytics.js +12 -12
  79. package/dist/tools/index.d.ts +50 -0
  80. package/dist/tools/index.d.ts.map +1 -0
  81. package/dist/tools/index.js +61 -0
  82. package/dist/tools/index.js.map +1 -0
  83. package/dist/tools/memory/createMemoryTimeline.js +10 -10
  84. package/dist/tools/memory/getMemoryGraph.js +12 -12
  85. package/dist/tools/memory/getSessionContext.js +9 -9
  86. package/dist/tools/memory/linkMemories.js +14 -14
  87. package/dist/tools/memory/listMemories.js +4 -4
  88. package/dist/tools/memory/recallMemory.js +4 -4
  89. package/dist/tools/memory/saveMemory.js +4 -4
  90. package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
  91. package/dist/tools/planning/generatePrd.js +46 -46
  92. package/dist/tools/prompt/enhancePromptGemini.js +160 -160
  93. package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
  94. package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
  95. package/hooks/hooks.json +121 -103
  96. package/package.json +73 -69
  97. package/skills/git-worktree.md +178 -178
  98. 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)] 모듈로 테스트 작성