@su-record/vibe 0.4.4 โ†’ 0.4.5

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.
@@ -0,0 +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)] ๋ชจ๋“ˆ๋กœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ