@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,396 +1,396 @@
|
|
|
1
|
-
# 🐹 Go 품질 규칙
|
|
2
|
-
|
|
3
|
-
## 핵심 원칙 (core에서 상속)
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
✅ 단일 책임 (SRP)
|
|
7
|
-
✅ 중복 제거 (DRY)
|
|
8
|
-
✅ 재사용성
|
|
9
|
-
✅ 낮은 복잡도
|
|
10
|
-
✅ 함수 ≤ 30줄
|
|
11
|
-
✅ 중첩 ≤ 3단계
|
|
12
|
-
✅ Cyclomatic complexity ≤ 10
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Go 특화 규칙
|
|
16
|
-
|
|
17
|
-
### 1. 에러 처리
|
|
18
|
-
|
|
19
|
-
```go
|
|
20
|
-
// ❌ 에러 무시
|
|
21
|
-
data, _ := ioutil.ReadFile("config.json")
|
|
22
|
-
|
|
23
|
-
// ✅ 에러 항상 처리
|
|
24
|
-
data, err := ioutil.ReadFile("config.json")
|
|
25
|
-
if err != nil {
|
|
26
|
-
return fmt.Errorf("설정 파일 읽기 실패: %w", err)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ✅ 커스텀 에러 타입
|
|
30
|
-
type NotFoundError struct {
|
|
31
|
-
Resource string
|
|
32
|
-
ID string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func (e *NotFoundError) Error() string {
|
|
36
|
-
return fmt.Sprintf("%s (ID: %s)를 찾을 수 없습니다", e.Resource, e.ID)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 사용
|
|
40
|
-
func GetUser(id string) (*User, error) {
|
|
41
|
-
user, err := repo.FindByID(id)
|
|
42
|
-
if err != nil {
|
|
43
|
-
return nil, fmt.Errorf("사용자 조회 실패: %w", err)
|
|
44
|
-
}
|
|
45
|
-
if user == nil {
|
|
46
|
-
return nil, &NotFoundError{Resource: "사용자", ID: id}
|
|
47
|
-
}
|
|
48
|
-
return user, nil
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### 2. 구조체와 인터페이스
|
|
53
|
-
|
|
54
|
-
```go
|
|
55
|
-
// ✅ 구조체 정의
|
|
56
|
-
type User struct {
|
|
57
|
-
ID string `json:"id"`
|
|
58
|
-
Email string `json:"email"`
|
|
59
|
-
Name string `json:"name"`
|
|
60
|
-
CreatedAt time.Time `json:"created_at"`
|
|
61
|
-
UpdatedAt time.Time `json:"updated_at"`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ✅ 생성자 함수
|
|
65
|
-
func NewUser(email, name string) *User {
|
|
66
|
-
now := time.Now()
|
|
67
|
-
return &User{
|
|
68
|
-
ID: uuid.New().String(),
|
|
69
|
-
Email: email,
|
|
70
|
-
Name: name,
|
|
71
|
-
CreatedAt: now,
|
|
72
|
-
UpdatedAt: now,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ✅ 작은 인터페이스 (Go의 철학)
|
|
77
|
-
type Reader interface {
|
|
78
|
-
Read(p []byte) (n int, err error)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
type Writer interface {
|
|
82
|
-
Write(p []byte) (n int, err error)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ✅ 인터페이스 조합
|
|
86
|
-
type ReadWriter interface {
|
|
87
|
-
Reader
|
|
88
|
-
Writer
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ✅ Repository 인터페이스
|
|
92
|
-
type UserRepository interface {
|
|
93
|
-
FindByID(ctx context.Context, id string) (*User, error)
|
|
94
|
-
FindByEmail(ctx context.Context, email string) (*User, error)
|
|
95
|
-
Create(ctx context.Context, user *User) error
|
|
96
|
-
Update(ctx context.Context, user *User) error
|
|
97
|
-
Delete(ctx context.Context, id string) error
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 3. Context 사용
|
|
102
|
-
|
|
103
|
-
```go
|
|
104
|
-
// ✅ Context 전파
|
|
105
|
-
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
|
|
106
|
-
// Context를 하위 함수에 전달
|
|
107
|
-
user, err := s.repo.FindByID(ctx, id)
|
|
108
|
-
if err != nil {
|
|
109
|
-
return nil, err
|
|
110
|
-
}
|
|
111
|
-
return user, nil
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ✅ Context 타임아웃
|
|
115
|
-
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
|
|
116
|
-
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
117
|
-
defer cancel()
|
|
118
|
-
|
|
119
|
-
result, err := h.service.Process(ctx)
|
|
120
|
-
if err != nil {
|
|
121
|
-
if errors.Is(err, context.DeadlineExceeded) {
|
|
122
|
-
http.Error(w, "요청 시간 초과", http.StatusRequestTimeout)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
json.NewEncoder(w).Encode(result)
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### 4. HTTP 핸들러 (net/http, Gin, Echo)
|
|
134
|
-
|
|
135
|
-
```go
|
|
136
|
-
// ✅ net/http 핸들러
|
|
137
|
-
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
138
|
-
id := chi.URLParam(r, "id")
|
|
139
|
-
|
|
140
|
-
user, err := h.service.GetUser(r.Context(), id)
|
|
141
|
-
if err != nil {
|
|
142
|
-
var notFound *NotFoundError
|
|
143
|
-
if errors.As(err, ¬Found) {
|
|
144
|
-
http.Error(w, err.Error(), http.StatusNotFound)
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
http.Error(w, "서버 오류", http.StatusInternalServerError)
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
w.Header().Set("Content-Type", "application/json")
|
|
152
|
-
json.NewEncoder(w).Encode(user)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ✅ Gin 핸들러
|
|
156
|
-
func (h *UserHandler) GetUser(c *gin.Context) {
|
|
157
|
-
id := c.Param("id")
|
|
158
|
-
|
|
159
|
-
user, err := h.service.GetUser(c.Request.Context(), id)
|
|
160
|
-
if err != nil {
|
|
161
|
-
var notFound *NotFoundError
|
|
162
|
-
if errors.As(err, ¬Found) {
|
|
163
|
-
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
c.JSON(http.StatusInternalServerError, gin.H{"error": "서버 오류"})
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
c.JSON(http.StatusOK, user)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ✅ Echo 핸들러
|
|
174
|
-
func (h *UserHandler) GetUser(c echo.Context) error {
|
|
175
|
-
id := c.Param("id")
|
|
176
|
-
|
|
177
|
-
user, err := h.service.GetUser(c.Request().Context(), id)
|
|
178
|
-
if err != nil {
|
|
179
|
-
var notFound *NotFoundError
|
|
180
|
-
if errors.As(err, ¬Found) {
|
|
181
|
-
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
|
182
|
-
}
|
|
183
|
-
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "서버 오류"})
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return c.JSON(http.StatusOK, user)
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### 5. 의존성 주입
|
|
191
|
-
|
|
192
|
-
```go
|
|
193
|
-
// ✅ 구조체에 의존성 주입
|
|
194
|
-
type UserService struct {
|
|
195
|
-
repo UserRepository
|
|
196
|
-
cache CacheRepository
|
|
197
|
-
logger *slog.Logger
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
func NewUserService(
|
|
201
|
-
repo UserRepository,
|
|
202
|
-
cache CacheRepository,
|
|
203
|
-
logger *slog.Logger,
|
|
204
|
-
) *UserService {
|
|
205
|
-
return &UserService{
|
|
206
|
-
repo: repo,
|
|
207
|
-
cache: cache,
|
|
208
|
-
logger: logger,
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ✅ 옵션 패턴
|
|
213
|
-
type ServerOption func(*Server)
|
|
214
|
-
|
|
215
|
-
func WithPort(port int) ServerOption {
|
|
216
|
-
return func(s *Server) {
|
|
217
|
-
s.port = port
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
func WithTimeout(timeout time.Duration) ServerOption {
|
|
222
|
-
return func(s *Server) {
|
|
223
|
-
s.timeout = timeout
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
func NewServer(opts ...ServerOption) *Server {
|
|
228
|
-
s := &Server{
|
|
229
|
-
port: 8080, // 기본값
|
|
230
|
-
timeout: 30 * time.Second,
|
|
231
|
-
}
|
|
232
|
-
for _, opt := range opts {
|
|
233
|
-
opt(s)
|
|
234
|
-
}
|
|
235
|
-
return s
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 사용
|
|
239
|
-
server := NewServer(
|
|
240
|
-
WithPort(3000),
|
|
241
|
-
WithTimeout(60*time.Second),
|
|
242
|
-
)
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### 6. 동시성
|
|
246
|
-
|
|
247
|
-
```go
|
|
248
|
-
// ✅ Goroutine + Channel
|
|
249
|
-
func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
|
|
250
|
-
results := make(chan Result, len(items))
|
|
251
|
-
errs := make(chan error, len(items))
|
|
252
|
-
|
|
253
|
-
var wg sync.WaitGroup
|
|
254
|
-
for _, item := range items {
|
|
255
|
-
wg.Add(1)
|
|
256
|
-
go func(item Item) {
|
|
257
|
-
defer wg.Done()
|
|
258
|
-
result, err := processItem(ctx, item)
|
|
259
|
-
if err != nil {
|
|
260
|
-
errs <- err
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
results <- result
|
|
264
|
-
}(item)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 결과 수집
|
|
268
|
-
go func() {
|
|
269
|
-
wg.Wait()
|
|
270
|
-
close(results)
|
|
271
|
-
close(errs)
|
|
272
|
-
}()
|
|
273
|
-
|
|
274
|
-
var finalResults []Result
|
|
275
|
-
for result := range results {
|
|
276
|
-
finalResults = append(finalResults, result)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 첫 번째 에러 반환
|
|
280
|
-
select {
|
|
281
|
-
case err := <-errs:
|
|
282
|
-
return nil, err
|
|
283
|
-
default:
|
|
284
|
-
return finalResults, nil
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ✅ errgroup 사용 (권장)
|
|
289
|
-
import "golang.org/x/sync/errgroup"
|
|
290
|
-
|
|
291
|
-
func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
|
|
292
|
-
g, ctx := errgroup.WithContext(ctx)
|
|
293
|
-
results := make([]Result, len(items))
|
|
294
|
-
|
|
295
|
-
for i, item := range items {
|
|
296
|
-
i, item := i, item // 클로저 캡처
|
|
297
|
-
g.Go(func() error {
|
|
298
|
-
result, err := processItem(ctx, item)
|
|
299
|
-
if err != nil {
|
|
300
|
-
return err
|
|
301
|
-
}
|
|
302
|
-
results[i] = result
|
|
303
|
-
return nil
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if err := g.Wait(); err != nil {
|
|
308
|
-
return nil, err
|
|
309
|
-
}
|
|
310
|
-
return results, nil
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### 7. 테스트
|
|
315
|
-
|
|
316
|
-
```go
|
|
317
|
-
// ✅ 테이블 기반 테스트
|
|
318
|
-
func TestAdd(t *testing.T) {
|
|
319
|
-
tests := []struct {
|
|
320
|
-
name string
|
|
321
|
-
a, b int
|
|
322
|
-
expected int
|
|
323
|
-
}{
|
|
324
|
-
{"양수 덧셈", 2, 3, 5},
|
|
325
|
-
{"음수 덧셈", -1, -2, -3},
|
|
326
|
-
{"영과 덧셈", 0, 5, 5},
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
for _, tt := range tests {
|
|
330
|
-
t.Run(tt.name, func(t *testing.T) {
|
|
331
|
-
result := Add(tt.a, tt.b)
|
|
332
|
-
if result != tt.expected {
|
|
333
|
-
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ✅ Mock 사용 (testify)
|
|
340
|
-
type MockUserRepository struct {
|
|
341
|
-
mock.Mock
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
|
|
345
|
-
args := m.Called(ctx, id)
|
|
346
|
-
if args.Get(0) == nil {
|
|
347
|
-
return nil, args.Error(1)
|
|
348
|
-
}
|
|
349
|
-
return args.Get(0).(*User), args.Error(1)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
func TestUserService_GetUser(t *testing.T) {
|
|
353
|
-
mockRepo := new(MockUserRepository)
|
|
354
|
-
service := NewUserService(mockRepo, nil, slog.Default())
|
|
355
|
-
|
|
356
|
-
expectedUser := &User{ID: "123", Name: "테스트"}
|
|
357
|
-
mockRepo.On("FindByID", mock.Anything, "123").Return(expectedUser, nil)
|
|
358
|
-
|
|
359
|
-
user, err := service.GetUser(context.Background(), "123")
|
|
360
|
-
|
|
361
|
-
assert.NoError(t, err)
|
|
362
|
-
assert.Equal(t, expectedUser, user)
|
|
363
|
-
mockRepo.AssertExpectations(t)
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
## 파일 구조
|
|
368
|
-
|
|
369
|
-
```
|
|
370
|
-
project/
|
|
371
|
-
├── cmd/
|
|
372
|
-
│ └── server/
|
|
373
|
-
│ └── main.go # 엔트리포인트
|
|
374
|
-
├── internal/
|
|
375
|
-
│ ├── domain/ # 도메인 모델
|
|
376
|
-
│ ├── handler/ # HTTP 핸들러
|
|
377
|
-
│ ├── service/ # 비즈니스 로직
|
|
378
|
-
│ ├── repository/ # 데이터 액세스
|
|
379
|
-
│ └── middleware/ # 미들웨어
|
|
380
|
-
├── pkg/ # 외부 공개 패키지
|
|
381
|
-
├── config/ # 설정
|
|
382
|
-
├── migrations/ # DB 마이그레이션
|
|
383
|
-
├── go.mod
|
|
384
|
-
└── go.sum
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
## 체크리스트
|
|
388
|
-
|
|
389
|
-
- [ ] 에러 항상 처리 (_, err 금지)
|
|
390
|
-
- [ ] fmt.Errorf("%w", err)로 에러 래핑
|
|
391
|
-
- [ ] Context 첫 번째 인자로 전달
|
|
392
|
-
- [ ] 작은 인터페이스 정의
|
|
393
|
-
- [ ] 생성자 함수 (NewXxx) 사용
|
|
394
|
-
- [ ] 테이블 기반 테스트
|
|
395
|
-
- [ ] gofmt, golint, go vet 통과
|
|
396
|
-
- [ ] 동시성에서 race condition 주의
|
|
1
|
+
# 🐹 Go 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Go 특화 규칙
|
|
16
|
+
|
|
17
|
+
### 1. 에러 처리
|
|
18
|
+
|
|
19
|
+
```go
|
|
20
|
+
// ❌ 에러 무시
|
|
21
|
+
data, _ := ioutil.ReadFile("config.json")
|
|
22
|
+
|
|
23
|
+
// ✅ 에러 항상 처리
|
|
24
|
+
data, err := ioutil.ReadFile("config.json")
|
|
25
|
+
if err != nil {
|
|
26
|
+
return fmt.Errorf("설정 파일 읽기 실패: %w", err)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ✅ 커스텀 에러 타입
|
|
30
|
+
type NotFoundError struct {
|
|
31
|
+
Resource string
|
|
32
|
+
ID string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func (e *NotFoundError) Error() string {
|
|
36
|
+
return fmt.Sprintf("%s (ID: %s)를 찾을 수 없습니다", e.Resource, e.ID)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 사용
|
|
40
|
+
func GetUser(id string) (*User, error) {
|
|
41
|
+
user, err := repo.FindByID(id)
|
|
42
|
+
if err != nil {
|
|
43
|
+
return nil, fmt.Errorf("사용자 조회 실패: %w", err)
|
|
44
|
+
}
|
|
45
|
+
if user == nil {
|
|
46
|
+
return nil, &NotFoundError{Resource: "사용자", ID: id}
|
|
47
|
+
}
|
|
48
|
+
return user, nil
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. 구조체와 인터페이스
|
|
53
|
+
|
|
54
|
+
```go
|
|
55
|
+
// ✅ 구조체 정의
|
|
56
|
+
type User struct {
|
|
57
|
+
ID string `json:"id"`
|
|
58
|
+
Email string `json:"email"`
|
|
59
|
+
Name string `json:"name"`
|
|
60
|
+
CreatedAt time.Time `json:"created_at"`
|
|
61
|
+
UpdatedAt time.Time `json:"updated_at"`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ✅ 생성자 함수
|
|
65
|
+
func NewUser(email, name string) *User {
|
|
66
|
+
now := time.Now()
|
|
67
|
+
return &User{
|
|
68
|
+
ID: uuid.New().String(),
|
|
69
|
+
Email: email,
|
|
70
|
+
Name: name,
|
|
71
|
+
CreatedAt: now,
|
|
72
|
+
UpdatedAt: now,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ✅ 작은 인터페이스 (Go의 철학)
|
|
77
|
+
type Reader interface {
|
|
78
|
+
Read(p []byte) (n int, err error)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Writer interface {
|
|
82
|
+
Write(p []byte) (n int, err error)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ✅ 인터페이스 조합
|
|
86
|
+
type ReadWriter interface {
|
|
87
|
+
Reader
|
|
88
|
+
Writer
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ✅ Repository 인터페이스
|
|
92
|
+
type UserRepository interface {
|
|
93
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
94
|
+
FindByEmail(ctx context.Context, email string) (*User, error)
|
|
95
|
+
Create(ctx context.Context, user *User) error
|
|
96
|
+
Update(ctx context.Context, user *User) error
|
|
97
|
+
Delete(ctx context.Context, id string) error
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Context 사용
|
|
102
|
+
|
|
103
|
+
```go
|
|
104
|
+
// ✅ Context 전파
|
|
105
|
+
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
|
|
106
|
+
// Context를 하위 함수에 전달
|
|
107
|
+
user, err := s.repo.FindByID(ctx, id)
|
|
108
|
+
if err != nil {
|
|
109
|
+
return nil, err
|
|
110
|
+
}
|
|
111
|
+
return user, nil
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ✅ Context 타임아웃
|
|
115
|
+
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
|
|
116
|
+
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
117
|
+
defer cancel()
|
|
118
|
+
|
|
119
|
+
result, err := h.service.Process(ctx)
|
|
120
|
+
if err != nil {
|
|
121
|
+
if errors.Is(err, context.DeadlineExceeded) {
|
|
122
|
+
http.Error(w, "요청 시간 초과", http.StatusRequestTimeout)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
json.NewEncoder(w).Encode(result)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 4. HTTP 핸들러 (net/http, Gin, Echo)
|
|
134
|
+
|
|
135
|
+
```go
|
|
136
|
+
// ✅ net/http 핸들러
|
|
137
|
+
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
138
|
+
id := chi.URLParam(r, "id")
|
|
139
|
+
|
|
140
|
+
user, err := h.service.GetUser(r.Context(), id)
|
|
141
|
+
if err != nil {
|
|
142
|
+
var notFound *NotFoundError
|
|
143
|
+
if errors.As(err, ¬Found) {
|
|
144
|
+
http.Error(w, err.Error(), http.StatusNotFound)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
http.Error(w, "서버 오류", http.StatusInternalServerError)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
w.Header().Set("Content-Type", "application/json")
|
|
152
|
+
json.NewEncoder(w).Encode(user)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ✅ Gin 핸들러
|
|
156
|
+
func (h *UserHandler) GetUser(c *gin.Context) {
|
|
157
|
+
id := c.Param("id")
|
|
158
|
+
|
|
159
|
+
user, err := h.service.GetUser(c.Request.Context(), id)
|
|
160
|
+
if err != nil {
|
|
161
|
+
var notFound *NotFoundError
|
|
162
|
+
if errors.As(err, ¬Found) {
|
|
163
|
+
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "서버 오류"})
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
c.JSON(http.StatusOK, user)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ✅ Echo 핸들러
|
|
174
|
+
func (h *UserHandler) GetUser(c echo.Context) error {
|
|
175
|
+
id := c.Param("id")
|
|
176
|
+
|
|
177
|
+
user, err := h.service.GetUser(c.Request().Context(), id)
|
|
178
|
+
if err != nil {
|
|
179
|
+
var notFound *NotFoundError
|
|
180
|
+
if errors.As(err, ¬Found) {
|
|
181
|
+
return c.JSON(http.StatusNotFound, map[string]string{"error": err.Error()})
|
|
182
|
+
}
|
|
183
|
+
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "서버 오류"})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return c.JSON(http.StatusOK, user)
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 5. 의존성 주입
|
|
191
|
+
|
|
192
|
+
```go
|
|
193
|
+
// ✅ 구조체에 의존성 주입
|
|
194
|
+
type UserService struct {
|
|
195
|
+
repo UserRepository
|
|
196
|
+
cache CacheRepository
|
|
197
|
+
logger *slog.Logger
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func NewUserService(
|
|
201
|
+
repo UserRepository,
|
|
202
|
+
cache CacheRepository,
|
|
203
|
+
logger *slog.Logger,
|
|
204
|
+
) *UserService {
|
|
205
|
+
return &UserService{
|
|
206
|
+
repo: repo,
|
|
207
|
+
cache: cache,
|
|
208
|
+
logger: logger,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ✅ 옵션 패턴
|
|
213
|
+
type ServerOption func(*Server)
|
|
214
|
+
|
|
215
|
+
func WithPort(port int) ServerOption {
|
|
216
|
+
return func(s *Server) {
|
|
217
|
+
s.port = port
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func WithTimeout(timeout time.Duration) ServerOption {
|
|
222
|
+
return func(s *Server) {
|
|
223
|
+
s.timeout = timeout
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func NewServer(opts ...ServerOption) *Server {
|
|
228
|
+
s := &Server{
|
|
229
|
+
port: 8080, // 기본값
|
|
230
|
+
timeout: 30 * time.Second,
|
|
231
|
+
}
|
|
232
|
+
for _, opt := range opts {
|
|
233
|
+
opt(s)
|
|
234
|
+
}
|
|
235
|
+
return s
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 사용
|
|
239
|
+
server := NewServer(
|
|
240
|
+
WithPort(3000),
|
|
241
|
+
WithTimeout(60*time.Second),
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 6. 동시성
|
|
246
|
+
|
|
247
|
+
```go
|
|
248
|
+
// ✅ Goroutine + Channel
|
|
249
|
+
func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
|
|
250
|
+
results := make(chan Result, len(items))
|
|
251
|
+
errs := make(chan error, len(items))
|
|
252
|
+
|
|
253
|
+
var wg sync.WaitGroup
|
|
254
|
+
for _, item := range items {
|
|
255
|
+
wg.Add(1)
|
|
256
|
+
go func(item Item) {
|
|
257
|
+
defer wg.Done()
|
|
258
|
+
result, err := processItem(ctx, item)
|
|
259
|
+
if err != nil {
|
|
260
|
+
errs <- err
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
results <- result
|
|
264
|
+
}(item)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 결과 수집
|
|
268
|
+
go func() {
|
|
269
|
+
wg.Wait()
|
|
270
|
+
close(results)
|
|
271
|
+
close(errs)
|
|
272
|
+
}()
|
|
273
|
+
|
|
274
|
+
var finalResults []Result
|
|
275
|
+
for result := range results {
|
|
276
|
+
finalResults = append(finalResults, result)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 첫 번째 에러 반환
|
|
280
|
+
select {
|
|
281
|
+
case err := <-errs:
|
|
282
|
+
return nil, err
|
|
283
|
+
default:
|
|
284
|
+
return finalResults, nil
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ✅ errgroup 사용 (권장)
|
|
289
|
+
import "golang.org/x/sync/errgroup"
|
|
290
|
+
|
|
291
|
+
func ProcessItems(ctx context.Context, items []Item) ([]Result, error) {
|
|
292
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
293
|
+
results := make([]Result, len(items))
|
|
294
|
+
|
|
295
|
+
for i, item := range items {
|
|
296
|
+
i, item := i, item // 클로저 캡처
|
|
297
|
+
g.Go(func() error {
|
|
298
|
+
result, err := processItem(ctx, item)
|
|
299
|
+
if err != nil {
|
|
300
|
+
return err
|
|
301
|
+
}
|
|
302
|
+
results[i] = result
|
|
303
|
+
return nil
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if err := g.Wait(); err != nil {
|
|
308
|
+
return nil, err
|
|
309
|
+
}
|
|
310
|
+
return results, nil
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### 7. 테스트
|
|
315
|
+
|
|
316
|
+
```go
|
|
317
|
+
// ✅ 테이블 기반 테스트
|
|
318
|
+
func TestAdd(t *testing.T) {
|
|
319
|
+
tests := []struct {
|
|
320
|
+
name string
|
|
321
|
+
a, b int
|
|
322
|
+
expected int
|
|
323
|
+
}{
|
|
324
|
+
{"양수 덧셈", 2, 3, 5},
|
|
325
|
+
{"음수 덧셈", -1, -2, -3},
|
|
326
|
+
{"영과 덧셈", 0, 5, 5},
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for _, tt := range tests {
|
|
330
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
331
|
+
result := Add(tt.a, tt.b)
|
|
332
|
+
if result != tt.expected {
|
|
333
|
+
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ✅ Mock 사용 (testify)
|
|
340
|
+
type MockUserRepository struct {
|
|
341
|
+
mock.Mock
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
|
|
345
|
+
args := m.Called(ctx, id)
|
|
346
|
+
if args.Get(0) == nil {
|
|
347
|
+
return nil, args.Error(1)
|
|
348
|
+
}
|
|
349
|
+
return args.Get(0).(*User), args.Error(1)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func TestUserService_GetUser(t *testing.T) {
|
|
353
|
+
mockRepo := new(MockUserRepository)
|
|
354
|
+
service := NewUserService(mockRepo, nil, slog.Default())
|
|
355
|
+
|
|
356
|
+
expectedUser := &User{ID: "123", Name: "테스트"}
|
|
357
|
+
mockRepo.On("FindByID", mock.Anything, "123").Return(expectedUser, nil)
|
|
358
|
+
|
|
359
|
+
user, err := service.GetUser(context.Background(), "123")
|
|
360
|
+
|
|
361
|
+
assert.NoError(t, err)
|
|
362
|
+
assert.Equal(t, expectedUser, user)
|
|
363
|
+
mockRepo.AssertExpectations(t)
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## 파일 구조
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
project/
|
|
371
|
+
├── cmd/
|
|
372
|
+
│ └── server/
|
|
373
|
+
│ └── main.go # 엔트리포인트
|
|
374
|
+
├── internal/
|
|
375
|
+
│ ├── domain/ # 도메인 모델
|
|
376
|
+
│ ├── handler/ # HTTP 핸들러
|
|
377
|
+
│ ├── service/ # 비즈니스 로직
|
|
378
|
+
│ ├── repository/ # 데이터 액세스
|
|
379
|
+
│ └── middleware/ # 미들웨어
|
|
380
|
+
├── pkg/ # 외부 공개 패키지
|
|
381
|
+
├── config/ # 설정
|
|
382
|
+
├── migrations/ # DB 마이그레이션
|
|
383
|
+
├── go.mod
|
|
384
|
+
└── go.sum
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## 체크리스트
|
|
388
|
+
|
|
389
|
+
- [ ] 에러 항상 처리 (_, err 금지)
|
|
390
|
+
- [ ] fmt.Errorf("%w", err)로 에러 래핑
|
|
391
|
+
- [ ] Context 첫 번째 인자로 전달
|
|
392
|
+
- [ ] 작은 인터페이스 정의
|
|
393
|
+
- [ ] 생성자 함수 (NewXxx) 사용
|
|
394
|
+
- [ ] 테이블 기반 테스트
|
|
395
|
+
- [ ] gofmt, golint, go vet 통과
|
|
396
|
+
- [ ] 동시성에서 race condition 주의
|