@wooojin/forgen 0.4.8 → 0.4.9
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-plugin/plugin.json +1 -1
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/cli.js +42 -0
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/init.js +53 -0
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/session-store.js +4 -0
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +179 -2
- package/dist/core/statusline-cli.js +34 -1
- package/dist/engine/compound-extractor.js +39 -0
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/solution-injector.js +93 -0
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +71 -0
- package/dist/host/install-orchestrator.js +1 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Go 원칙
|
|
3
|
+
version: 2026-05-18
|
|
4
|
+
sources:
|
|
5
|
+
- sources/go-runtime/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Go 원칙
|
|
9
|
+
|
|
10
|
+
> [공통 원칙](./common.md)을 먼저 따르고, 아래는 Go 특화.
|
|
11
|
+
|
|
12
|
+
## G0. 의사결정 우선순위
|
|
13
|
+
|
|
14
|
+
1. **에러는 값** — panic은 진짜 예외(복구 불가)에만
|
|
15
|
+
2. **context.Context 전파** — cancellation/deadline은 모든 I/O에 전달
|
|
16
|
+
3. **goroutine 생명주기 명확화** — 시작한 곳이 종료 책임 보유
|
|
17
|
+
4. **interface는 작게** — consumer 측 정의, 1~3 메서드
|
|
18
|
+
5. **lint 자동화** — golangci-lint + go vet + staticcheck CI 필수
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## G1. 에러는 값으로 반환
|
|
23
|
+
|
|
24
|
+
근거: `sources/go-runtime/` (Effective Go, Rob Pike "Errors are values")
|
|
25
|
+
|
|
26
|
+
**panic은 "진짜 프로그래밍 오류" 또는 "복구 불가 상황"에만. 일반 에러는 반환값.**
|
|
27
|
+
|
|
28
|
+
### G1.1 에러 반환 패턴
|
|
29
|
+
|
|
30
|
+
```go
|
|
31
|
+
// 에러를 항상 마지막 반환값으로
|
|
32
|
+
func fetchUser(ctx context.Context, id string) (*User, error) {
|
|
33
|
+
row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
|
|
34
|
+
var user User
|
|
35
|
+
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
|
36
|
+
if errors.Is(err, sql.ErrNoRows) {
|
|
37
|
+
return nil, ErrUserNotFound // sentinel error
|
|
38
|
+
}
|
|
39
|
+
return nil, fmt.Errorf("fetchUser %s: %w", id, err) // 래핑
|
|
40
|
+
}
|
|
41
|
+
return &user, nil
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### G1.2 에러 래핑과 검사
|
|
46
|
+
|
|
47
|
+
```go
|
|
48
|
+
// 에러 래핑: %w 사용 (Go 1.13+)
|
|
49
|
+
return fmt.Errorf("processPayment: %w", ErrInsufficientFunds)
|
|
50
|
+
|
|
51
|
+
// 에러 검사: errors.Is (sentinel), errors.As (타입)
|
|
52
|
+
if errors.Is(err, ErrUserNotFound) {
|
|
53
|
+
return http.StatusNotFound, nil
|
|
54
|
+
}
|
|
55
|
+
var validationErr *ValidationError
|
|
56
|
+
if errors.As(err, &validationErr) {
|
|
57
|
+
return http.StatusBadRequest, validationErr.Fields
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### G1.3 Sentinel Error 정의
|
|
62
|
+
|
|
63
|
+
```go
|
|
64
|
+
// 패키지 레벨 sentinel errors
|
|
65
|
+
var (
|
|
66
|
+
ErrUserNotFound = errors.New("user not found")
|
|
67
|
+
ErrInsufficientFunds = errors.New("insufficient funds")
|
|
68
|
+
ErrDuplicateEmail = errors.New("duplicate email")
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// 필드 정보가 필요한 구조체 에러
|
|
72
|
+
type ValidationError struct {
|
|
73
|
+
Fields map[string]string
|
|
74
|
+
}
|
|
75
|
+
func (e *ValidationError) Error() string {
|
|
76
|
+
return fmt.Sprintf("validation failed: %v", e.Fields)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### G1.4 panic 허용 기준
|
|
81
|
+
|
|
82
|
+
```go
|
|
83
|
+
// OK: 프로그래밍 오류 — nil 포인터, 인덱스 초과 등 (런타임 패닉)
|
|
84
|
+
// OK: 초기화 실패 — 앱이 시작될 수 없는 상태
|
|
85
|
+
func mustParseTemplate(s string) *template.Template {
|
|
86
|
+
t, err := template.New("").Parse(s)
|
|
87
|
+
if err != nil {
|
|
88
|
+
panic(fmt.Sprintf("failed to parse template: %v", err)) // 컴파일 타임 알아야 할 오류
|
|
89
|
+
}
|
|
90
|
+
return t
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// NOT OK: 일반 런타임 에러 (DB 오류, 네트워크 오류, 검증 실패)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## G2. context.Context 전파
|
|
99
|
+
|
|
100
|
+
**모든 I/O 함수의 첫 번째 인자는 context.Context. cancellation/deadline이 반드시 전파되어야 한다.**
|
|
101
|
+
|
|
102
|
+
### G2.1 함수 시그니처 규칙
|
|
103
|
+
|
|
104
|
+
```go
|
|
105
|
+
// WRONG: context 없는 DB 호출
|
|
106
|
+
func (r *UserRepo) FindByID(id string) (*User, error) {
|
|
107
|
+
return r.db.QueryRow("SELECT ...", id).Scan(...)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// RIGHT: context 전파
|
|
111
|
+
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
|
|
112
|
+
return r.db.QueryRowContext(ctx, "SELECT ...", id).Scan(...)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### G2.2 context 값 사용 기준
|
|
117
|
+
|
|
118
|
+
```go
|
|
119
|
+
// context.Value: 요청 범위 메타데이터만 (request ID, auth user, traceID)
|
|
120
|
+
// 비즈니스 로직 파라미터를 context에 넣지 마라
|
|
121
|
+
|
|
122
|
+
// WRONG
|
|
123
|
+
ctx = context.WithValue(ctx, "userID", userID)
|
|
124
|
+
|
|
125
|
+
// RIGHT: 명시적 파라미터
|
|
126
|
+
func processOrder(ctx context.Context, userID string, items []Item) error
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### G2.3 context 취소 처리
|
|
130
|
+
|
|
131
|
+
```go
|
|
132
|
+
func longOperation(ctx context.Context) error {
|
|
133
|
+
for {
|
|
134
|
+
select {
|
|
135
|
+
case <-ctx.Done():
|
|
136
|
+
return ctx.Err() // context.Canceled 또는 context.DeadlineExceeded
|
|
137
|
+
default:
|
|
138
|
+
// 작업 계속
|
|
139
|
+
}
|
|
140
|
+
// 또는 I/O 함수에 ctx 전달 시 자동 처리
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### G2.4 Timeout 설정
|
|
146
|
+
|
|
147
|
+
```go
|
|
148
|
+
// HTTP 핸들러: 요청 timeout
|
|
149
|
+
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
150
|
+
defer cancel() // 반드시 defer cancel() — goroutine/timer 누수 방지
|
|
151
|
+
|
|
152
|
+
result, err := svc.Process(ctx, input)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## G3. Goroutine 생명주기
|
|
158
|
+
|
|
159
|
+
**goroutine을 시작한 쪽이 종료 책임을 진다. goroutine leak은 메모리 누수와 동일하다.**
|
|
160
|
+
|
|
161
|
+
### G3.1 goroutine leak 패턴과 픽스
|
|
162
|
+
|
|
163
|
+
```go
|
|
164
|
+
// WRONG: 종료 조건 없는 goroutine
|
|
165
|
+
func start() {
|
|
166
|
+
go func() {
|
|
167
|
+
for {
|
|
168
|
+
processMessage() // 영원히 실행
|
|
169
|
+
}
|
|
170
|
+
}()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// RIGHT: context 기반 종료
|
|
174
|
+
func start(ctx context.Context) {
|
|
175
|
+
go func() {
|
|
176
|
+
for {
|
|
177
|
+
select {
|
|
178
|
+
case <-ctx.Done():
|
|
179
|
+
return
|
|
180
|
+
default:
|
|
181
|
+
processMessage(ctx)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}()
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### G3.2 WaitGroup으로 완료 대기
|
|
189
|
+
|
|
190
|
+
```go
|
|
191
|
+
func processAll(ctx context.Context, items []Item) error {
|
|
192
|
+
var wg sync.WaitGroup
|
|
193
|
+
errCh := make(chan error, len(items)) // 버퍼드 채널 (모든 에러 수집)
|
|
194
|
+
|
|
195
|
+
for _, item := range items {
|
|
196
|
+
wg.Add(1)
|
|
197
|
+
go func(item Item) {
|
|
198
|
+
defer wg.Done()
|
|
199
|
+
if err := process(ctx, item); err != nil {
|
|
200
|
+
errCh <- err
|
|
201
|
+
}
|
|
202
|
+
}(item) // 루프 변수 캡처 — Go 1.22부터 자동이지만 명시적 인자가 명확
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
wg.Wait()
|
|
206
|
+
close(errCh)
|
|
207
|
+
|
|
208
|
+
for err := range errCh {
|
|
209
|
+
return err // 첫 번째 에러 반환 (또는 multierr.Combine)
|
|
210
|
+
}
|
|
211
|
+
return nil
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### G3.3 errgroup 패턴
|
|
216
|
+
|
|
217
|
+
```go
|
|
218
|
+
import "golang.org/x/sync/errgroup"
|
|
219
|
+
|
|
220
|
+
func fanOut(ctx context.Context) error {
|
|
221
|
+
g, ctx := errgroup.WithContext(ctx) // 한 goroutine 실패 시 나머지 ctx 취소
|
|
222
|
+
|
|
223
|
+
g.Go(func() error { return fetchUsers(ctx) })
|
|
224
|
+
g.Go(func() error { return fetchOrders(ctx) })
|
|
225
|
+
g.Go(func() error { return fetchInventory(ctx) })
|
|
226
|
+
|
|
227
|
+
return g.Wait() // 모든 goroutine 완료 대기, 첫 번째 non-nil 에러 반환
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## G4. Channel 단일 책임
|
|
234
|
+
|
|
235
|
+
**channel은 한 가지 목적으로만. close는 항상 sender가.**
|
|
236
|
+
|
|
237
|
+
### G4.1 channel 패턴
|
|
238
|
+
|
|
239
|
+
```go
|
|
240
|
+
// 생산자-소비자: sender가 close
|
|
241
|
+
func producer(ctx context.Context) <-chan Item {
|
|
242
|
+
ch := make(chan Item)
|
|
243
|
+
go func() {
|
|
244
|
+
defer close(ch) // sender가 close
|
|
245
|
+
for _, item := range items {
|
|
246
|
+
select {
|
|
247
|
+
case ch <- item:
|
|
248
|
+
case <-ctx.Done():
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}()
|
|
253
|
+
return ch
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func consumer(ctx context.Context, ch <-chan Item) {
|
|
257
|
+
for item := range ch { // close 시 자동 종료
|
|
258
|
+
process(ctx, item)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### G4.2 channel vs sync.Mutex 선택 기준
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
goroutine 간 데이터 전달 → channel
|
|
267
|
+
공유 상태 보호 (캐시, 카운터) → sync.Mutex / sync.RWMutex
|
|
268
|
+
단발성 신호 (완료 알림) → chan struct{} (또는 context)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## G5. Interface는 작게
|
|
274
|
+
|
|
275
|
+
**interface는 큰 것을 정의하지 마라. consumer 측에서, 1~3 메서드로.**
|
|
276
|
+
|
|
277
|
+
### G5.1 인터페이스 크기 원칙
|
|
278
|
+
|
|
279
|
+
```go
|
|
280
|
+
// WRONG: 모든 것을 포함한 큰 인터페이스
|
|
281
|
+
type UserRepository interface {
|
|
282
|
+
Create(ctx context.Context, user *User) error
|
|
283
|
+
Update(ctx context.Context, user *User) error
|
|
284
|
+
Delete(ctx context.Context, id string) error
|
|
285
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
286
|
+
FindByEmail(ctx context.Context, email string) (*User, error)
|
|
287
|
+
List(ctx context.Context, filter UserFilter) ([]*User, error)
|
|
288
|
+
Count(ctx context.Context, filter UserFilter) (int, error)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// RIGHT: 사용 측이 필요한 것만
|
|
292
|
+
type UserFinder interface {
|
|
293
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type UserCreator interface {
|
|
297
|
+
Create(ctx context.Context, user *User) error
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// UserService는 필요한 인터페이스만 조합
|
|
301
|
+
type UserService struct {
|
|
302
|
+
finder UserFinder
|
|
303
|
+
creator UserCreator
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### G5.2 Consumer 측 정의
|
|
308
|
+
|
|
309
|
+
```go
|
|
310
|
+
// WRONG: 구현 패키지에서 인터페이스 정의 (Go 안티패턴)
|
|
311
|
+
// user/repository.go
|
|
312
|
+
type Repository interface { ... } // 구현 패키지가 자기 인터페이스를 선언
|
|
313
|
+
|
|
314
|
+
// RIGHT: consumer 패키지에서 필요한 것만 정의
|
|
315
|
+
// order/service.go — order 서비스가 필요한 user 기능만 정의
|
|
316
|
+
type userProvider interface {
|
|
317
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### G5.3 표준 라이브러리 인터페이스 우선 활용
|
|
322
|
+
|
|
323
|
+
```go
|
|
324
|
+
io.Reader, io.Writer, io.Closer — 스트림
|
|
325
|
+
fmt.Stringer — 문자열 표현
|
|
326
|
+
error — 에러
|
|
327
|
+
http.Handler — HTTP 처리
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## G6. 메모리 재사용 — sync.Pool
|
|
333
|
+
|
|
334
|
+
**sync.Pool은 신중하게. 올바르게 쓰지 않으면 미묘한 버그 원인.**
|
|
335
|
+
|
|
336
|
+
### G6.1 sync.Pool 적절한 사용
|
|
337
|
+
|
|
338
|
+
```go
|
|
339
|
+
// 적합: 버퍼, bytes.Buffer, 인코더 같은 임시 객체
|
|
340
|
+
var bufPool = sync.Pool{
|
|
341
|
+
New: func() interface{} {
|
|
342
|
+
return new(bytes.Buffer)
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func encode(v interface{}) ([]byte, error) {
|
|
347
|
+
buf := bufPool.Get().(*bytes.Buffer)
|
|
348
|
+
defer func() {
|
|
349
|
+
buf.Reset() // 상태 초기화 필수
|
|
350
|
+
bufPool.Put(buf) // 반환
|
|
351
|
+
}()
|
|
352
|
+
if err := json.NewEncoder(buf).Encode(v); err != nil {
|
|
353
|
+
return nil, err
|
|
354
|
+
}
|
|
355
|
+
return append([]byte{}, buf.Bytes()...), nil // copy — buf는 pool에 반환되므로
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### G6.2 sync.Pool 주의사항
|
|
360
|
+
|
|
361
|
+
- Pool에서 꺼낸 객체의 **이전 상태가 남아있을 수 있다** → `Reset()` / `Zero` 필수.
|
|
362
|
+
- GC가 Pool을 비울 수 있다 — 영구 캐시 용도로 사용 금지.
|
|
363
|
+
- escape analysis 확인: `go build -gcflags='-m'` 으로 heap 할당 여부 확인.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## G7. 정적 분석 필수
|
|
368
|
+
|
|
369
|
+
**린터 없이 코드 리뷰는 없다. CI에서 통과 못하면 merge 불가.**
|
|
370
|
+
|
|
371
|
+
### G7.1 golangci-lint 설정
|
|
372
|
+
|
|
373
|
+
```yaml
|
|
374
|
+
# .golangci.yml
|
|
375
|
+
linters:
|
|
376
|
+
enable:
|
|
377
|
+
- govet # go vet (공식)
|
|
378
|
+
- staticcheck # SA/S 룰 (버그 탐지)
|
|
379
|
+
- errcheck # 에러 무시 탐지
|
|
380
|
+
- gosimple # 코드 단순화
|
|
381
|
+
- ineffassign # 불필요한 할당
|
|
382
|
+
- unused # 미사용 코드
|
|
383
|
+
- gosec # 보안 룰 (G-시리즈)
|
|
384
|
+
- noctx # context 없는 HTTP 요청 탐지
|
|
385
|
+
- bodyclose # http.Response.Body.Close() 확인
|
|
386
|
+
- nilerr # nil 에러 반환 패턴 탐지
|
|
387
|
+
- wrapcheck # 외부 패키지 에러 wrap 강제
|
|
388
|
+
- cyclop # 순환 복잡도 제한
|
|
389
|
+
|
|
390
|
+
linters-settings:
|
|
391
|
+
cyclop:
|
|
392
|
+
max-complexity: 10
|
|
393
|
+
gosec:
|
|
394
|
+
excludes: ["G304"] # 파일 경로는 의도적으로 제외 가능
|
|
395
|
+
|
|
396
|
+
issues:
|
|
397
|
+
exclude-rules:
|
|
398
|
+
- path: "_test.go"
|
|
399
|
+
linters: [wrapcheck, gosec]
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### G7.2 CI 파이프라인
|
|
403
|
+
|
|
404
|
+
```yaml
|
|
405
|
+
# .github/workflows/go.yml
|
|
406
|
+
- name: golangci-lint
|
|
407
|
+
uses: golangci/golangci-lint-action@v4
|
|
408
|
+
with:
|
|
409
|
+
version: latest
|
|
410
|
+
|
|
411
|
+
- name: go test
|
|
412
|
+
run: go test -race -coverprofile=coverage.out ./...
|
|
413
|
+
|
|
414
|
+
- name: go vet
|
|
415
|
+
run: go vet ./...
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
- `-race` 플래그 필수 (data race 탐지).
|
|
419
|
+
- 커버리지: `go tool cover -func=coverage.out` 으로 함수별 커버리지.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## G8. 구조화 로그 (slog)
|
|
424
|
+
|
|
425
|
+
```go
|
|
426
|
+
import "log/slog" // Go 1.21+
|
|
427
|
+
|
|
428
|
+
// 기본 설정
|
|
429
|
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
430
|
+
Level: slog.LevelInfo,
|
|
431
|
+
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
|
432
|
+
// 민감 정보 리댁션
|
|
433
|
+
if a.Key == "password" || a.Key == "token" {
|
|
434
|
+
return slog.String(a.Key, "[REDACTED]")
|
|
435
|
+
}
|
|
436
|
+
return a
|
|
437
|
+
},
|
|
438
|
+
}))
|
|
439
|
+
slog.SetDefault(logger)
|
|
440
|
+
|
|
441
|
+
// 사용
|
|
442
|
+
slog.InfoContext(ctx, "order created",
|
|
443
|
+
slog.String("orderId", order.ID),
|
|
444
|
+
slog.String("userId", order.UserID),
|
|
445
|
+
slog.Float64("amount", order.Amount),
|
|
446
|
+
slog.Duration("duration", time.Since(start)),
|
|
447
|
+
)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## G9. Go 안티패턴 카탈로그
|
|
453
|
+
|
|
454
|
+
| 안티패턴 | 픽스 |
|
|
455
|
+
|----------|------|
|
|
456
|
+
| 에러 무시 (`_ = err`) | 에러 처리 또는 이유 주석 |
|
|
457
|
+
| `panic` in 일반 에러 경로 | `error` 반환값 사용 |
|
|
458
|
+
| context 없는 DB/HTTP 호출 | `xxxContext(ctx, ...)` 버전 사용 |
|
|
459
|
+
| goroutine 종료 조건 없음 | context 취소 또는 done 채널 |
|
|
460
|
+
| `defer cancel()` 누락 | `ctx, cancel := context.WithTimeout(...)` → `defer cancel()` |
|
|
461
|
+
| receiver에서 직접 인터페이스 정의 | consumer 측에서 필요한 것만 |
|
|
462
|
+
| 큰 인터페이스 (5+ 메서드) | 목적별 소형 인터페이스 분리 |
|
|
463
|
+
| `sync.Mutex` Lock 후 defer Unlock 누락 | `defer mu.Unlock()` 패턴 |
|
|
464
|
+
| goroutine 내 루프 변수 캡처 (Go 1.22 미만) | `item := item` 또는 인자로 전달 |
|
|
465
|
+
| `http.Get` (context 없음) | `http.NewRequestWithContext(ctx, ...)` |
|
|
466
|
+
| `json.Unmarshal` 에러 무시 | 에러 처리 |
|
|
467
|
+
| 채널 receiver가 close | sender가 close |
|
|
468
|
+
| `fmt.Println` in 프로덕션 코드 | `slog` 구조화 로그 |
|
|
469
|
+
| `golangci-lint` CI 미설정 | `.golangci.yml` + CI 단계 추가 |
|