Skip to content

Commit a2b9c05

Browse files
committed
feat: 이벤트 디스패칭 및 WebSocket 런타임에서의 에러 처리와 동시성 개선
• Kafka 및 RabbitMQ 퍼블리셔에서 nil 핸들러와 write 옵션에 대한 에러 체크 추가 • 고루틴과 동기화를 활용해 이벤트 디스패칭을 병렬 처리하도록 개선 • 바이너리 및 리다이렉트 응답에서 포인터 타입을 지원하도록 반환값 핸들러 확장 • 성능 향상을 위해 실행 계획을 컴파일하고 캐싱하도록 파이프라인 실행 로직 리팩토링 • WebSocket 런타임에서 동시 쓰기를 안전하게 처리하기 위해 mutex 기반 연결 추적 추가 • 새로운 에러 처리 시나리오와 WebSocket 컨텍스트 파라미터 노출에 대한 테스트 추가
1 parent 70c816e commit a2b9c05

31 files changed

Lines changed: 897 additions & 278 deletions

cmd/demo/main.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,23 @@ func main() {
7272
}),
7373
)
7474

75-
app.Consumers().Register(
75+
if err := app.Consumers().Register(
7676
"order.created",
7777
(*OrderConsumer).OnCreatedKafka,
78-
)
78+
); err != nil {
79+
panic(err)
80+
}
7981

80-
app.Consumers().Register(
82+
if err := app.Consumers().Register(
8183
"stock.created",
8284
(*OrderConsumer).OnCreatedRabbitMQ,
83-
)
85+
); err != nil {
86+
panic(err)
87+
}
8488

85-
app.WebSocket().Register("/ws/chat", (*ChatController).OnMessage)
89+
if err := app.WebSocket().Register("/ws/chat", (*ChatController).OnMessage); err != nil {
90+
panic(err)
91+
}
8692

8793
// EnableGracefulShutdown & ShutdownTimeout은 선택사항입니다.
8894
app.Run(boot.Options{

core/handler_meta.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ type HandlerMeta struct {
1010
ControllerType reflect.Type
1111
// 호출할 메서드 이름
1212
Method reflect.Method
13+
// 라우트에 선언된 path parameter key 순서
14+
PathKeys []string
1315
// 핸들러에 적용된 인터셉터
1416
Interceptors []Interceptor
1517
}

internal/adapter/echo/adapter.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ type Server struct {
4242
func NewServer(pipeline *pipeline.Pipeline, address string, transportHooks []func(any), opts boot.HTTPOptions) *Server {
4343
normalized := normalizeHTTPOptions(opts)
4444
e := newEcho(normalized)
45-
for _, hook := range transportHooks {
46-
hook(e)
47-
}
4845

4946
httpServer := &http.Server{
5047
Addr: address,
@@ -129,6 +126,9 @@ func simpleRecover() echo.MiddlewareFunc {
129126

130127
func (s *Server) Mount() {
131128
s.echo.Any("/*", s.handle)
129+
for _, hook := range s.transportHooks {
130+
hook(s.echo)
131+
}
132132
}
133133

134134
func (s *Server) Start() error {

internal/adapter/echo/context_impl.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ type echoContext struct {
1919

2020
func NewContext(c echo.Context) core.ExecutionContext {
2121
return &echoContext{
22-
echo: c,
23-
reqCtx: c.Request().Context(), // 요청시 생성되는 Context
24-
store: make(map[string]any),
25-
eventBus: publish.NewEventBus(),
22+
echo: c,
23+
reqCtx: c.Request().Context(), // 요청시 생성되는 Context
2624
}
2725
}
2826

@@ -35,6 +33,9 @@ func (e *echoContext) Bind(out any) error {
3533
}
3634

3735
func (e *echoContext) Get(key string) (any, bool) {
36+
if e.store == nil {
37+
return nil, false
38+
}
3839
value, ok := e.store[key]
3940
return value, ok
4041
}
@@ -64,6 +65,9 @@ func (e *echoContext) Query(name string) string {
6465
}
6566

6667
func (e *echoContext) Set(key string, value any) {
68+
if e.store == nil {
69+
e.store = make(map[string]any)
70+
}
6771
e.store[key] = value
6872
}
6973

@@ -125,5 +129,8 @@ func (e *echoContext) MultipartForm() (*multipart.Form, error) {
125129
}
126130

127131
func (c *echoContext) EventBus() publish.EventBus {
132+
if c.eventBus == nil {
133+
c.eventBus = publish.NewEventBus()
134+
}
128135
return c.eventBus
129136
}

internal/bootstrap/bootstrap.go

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func Run(config Config) error {
8787
},
8888
})
8989
if err != nil {
90-
panic(err)
90+
return fmt.Errorf("[Bootstrap] Kafka publisher 초기화 실패: %w", err)
9191
}
9292
eventPublishers = append(eventPublishers, kafkaPublisher)
9393
defer func() {
@@ -108,7 +108,7 @@ func Run(config Config) error {
108108
},
109109
})
110110
if err != nil {
111-
panic(err)
111+
return fmt.Errorf("[Bootstrap] RabbitMQ writer 초기화 실패: %w", err)
112112
}
113113
eventPublishers = append(eventPublishers, rabbitmqWriter)
114114
defer func() {
@@ -183,13 +183,13 @@ func Run(config Config) error {
183183
prefix := config.HTTP.GlobalPrefix
184184
if prefix != "" {
185185
if !strings.HasPrefix(prefix, "/") {
186-
panic("HTTP GlobalPrefix는 '/'로 시작해야 합니다")
186+
return fmt.Errorf("HTTP GlobalPrefix는 '/'로 시작해야 합니다")
187187
}
188188
if strings.Contains(prefix, ":") {
189-
panic("Path 파라미터는 HTTP GlobalPrefix에서 사용될 수 없습니다")
189+
return fmt.Errorf("Path 파라미터는 HTTP GlobalPrefix에서 사용될 수 없습니다")
190190
}
191191
if strings.Contains(prefix, "*") {
192-
panic("와일드카드는 HTTP GlobalPrefix에서 사용될 수 없습니다")
192+
return fmt.Errorf("와일드카드는 HTTP GlobalPrefix에서 사용될 수 없습니다")
193193
}
194194
prefix = strings.TrimSuffix(prefix, "/")
195195
log.Printf("[Bootstrap] HTTP GlobalPrefix 적용: %s", prefix)
@@ -212,6 +212,9 @@ func Run(config Config) error {
212212
resolved := make([]core.Interceptor, len(route.Interceptors))
213213
for i, interceptor := range route.Interceptors {
214214
interceptorType := reflect.TypeOf(interceptor)
215+
if interceptorType == nil {
216+
return fmt.Errorf("[Bootstrap] Route Interceptor[%d]가 nil입니다", i)
217+
}
215218
value := reflect.ValueOf(interceptor)
216219

217220
// 같은 타입의 인터셉터 로깅은 한 번만 남긴다.
@@ -225,7 +228,7 @@ func Run(config Config) error {
225228

226229
inst, err := container.Resolve(interceptorType)
227230
if err != nil {
228-
panic(err)
231+
return fmt.Errorf("[Bootstrap] Route Interceptor 생성 실패: %w", err)
229232
}
230233
resolved[i] = inst.(core.Interceptor)
231234
} else {
@@ -238,10 +241,15 @@ func Run(config Config) error {
238241
}
239242

240243
meta.Interceptors = resolved
241-
fullPath := joinPath(prefix, route.Path)
244+
fullPath, err := joinPath(prefix, route.Path)
245+
if err != nil {
246+
return err
247+
}
242248
log.Printf("[Bootstrap] HTTP 라우트 등록 : (%s) %s", route.Method, fullPath)
243249

244-
assertNoAmbiguousRoute(route.Method, fullPath, registeredPathsByMethod[route.Method])
250+
if err := assertNoAmbiguousRoute(route.Method, fullPath, registeredPathsByMethod[route.Method]); err != nil {
251+
return err
252+
}
245253
registeredPathsByMethod[route.Method] = append(registeredPathsByMethod[route.Method], fullPath)
246254

247255
router.Register(route.Method, fullPath, meta)
@@ -250,8 +258,7 @@ func Run(config Config) error {
250258
log.Println("[Bootstrap] 컨트롤러 의존성 Warm-up 시작")
251259
// Warm-Up Component
252260
if err := container.WarmUp(router.ControllerTypes()); err != nil {
253-
// Warm-up 실패시 panic
254-
panic(err)
261+
return fmt.Errorf("[Bootstrap] HTTP 컨트롤러 Warm-up 실패: %w", err)
255262
}
256263

257264
log.Println("[Bootstrap] 실행 파이프라인 구성")
@@ -319,13 +326,16 @@ func Run(config Config) error {
319326
for _, interceptor := range ordered {
320327
v := reflect.ValueOf(interceptor)
321328
t := reflect.TypeOf(interceptor)
329+
if t == nil {
330+
return fmt.Errorf("[Bootstrap] Interceptor가 nil입니다")
331+
}
322332

323333
if t.Kind() == reflect.Pointer && v.IsNil() {
324334
log.Printf("[Bootstrap] Interceptor %s가 컨테이너에서 생성됐습니다.", t.Elem().Name())
325335

326336
inst, err := container.Resolve(t)
327337
if err != nil {
328-
panic(err)
338+
return fmt.Errorf("[Bootstrap] Interceptor 생성 실패: %w", err)
329339
}
330340

331341
httpPipeline.AddInterceptor(inst.(core.Interceptor))
@@ -351,7 +361,7 @@ func Run(config Config) error {
351361
defer wsRuntime.Stop()
352362

353363
// Echo Transport Hook으로 마운트
354-
config.TransportHooks = append(config.TransportHooks, func(e any) {
364+
wsMountHook := func(e any) {
355365
echoInstance, ok := e.(*echo.Echo)
356366
if !ok {
357367
return
@@ -363,7 +373,8 @@ func Run(config Config) error {
363373
return nil
364374
})
365375
}
366-
})
376+
}
377+
config.TransportHooks = append([]func(any){wsMountHook}, config.TransportHooks...)
367378
}
368379

369380
// Echo Adapter
@@ -390,7 +401,7 @@ func Run(config Config) error {
390401
consumerTypes = append(consumerTypes, reg.Meta.ControllerType)
391402
}
392403
if err := container.WarmUp(consumerTypes); err != nil {
393-
panic(err)
404+
return fmt.Errorf("[Bootstrap] Consumer 컨트롤러 Warm-up 실패: %w", err)
394405
}
395406
}
396407

@@ -419,7 +430,7 @@ func Run(config Config) error {
419430
)
420431

421432
if err := runtime.Validate(); err != nil {
422-
panic(err)
433+
return fmt.Errorf("[Bootstrap] Kafka consumer 검증 실패: %w", err)
423434
}
424435

425436
forwardConsumerErrors("Kafka", runtime, consumerErrCh)
@@ -451,7 +462,7 @@ func Run(config Config) error {
451462
)
452463

453464
if err := runtime.Validate(); err != nil {
454-
panic(err)
465+
return fmt.Errorf("[Bootstrap] RabbitMQ consumer 검증 실패: %w", err)
455466
}
456467

457468
forwardConsumerErrors("RabbitMQ", runtime, consumerErrCh)
@@ -539,23 +550,23 @@ func Run(config Config) error {
539550
return nil
540551
}
541552

542-
func joinPath(prefix, path string) string {
553+
func joinPath(prefix, path string) (string, error) {
543554
if path == "" {
544-
panic("라우트 Path는 비어있을 수 없습니다")
555+
return "", fmt.Errorf("라우트 Path는 비어있을 수 없습니다")
545556
}
546557

547558
if !strings.HasPrefix(path, "/") {
548559
path = "/" + path
549560
}
550561

551562
if prefix == "" {
552-
return path
563+
return path, nil
553564
}
554565

555-
return prefix + path
566+
return prefix + path, nil
556567
}
557568

558-
func assertNoAmbiguousRoute(method, newPath string, existing []string) {
569+
func assertNoAmbiguousRoute(method, newPath string, existing []string) error {
559570
newSegs := splitPathForValidation(newPath)
560571

561572
for _, oldPath := range existing {
@@ -583,12 +594,13 @@ func assertNoAmbiguousRoute(method, newPath string, existing []string) {
583594
}
584595

585596
if overlaps {
586-
panic(fmt.Sprintf(
597+
return fmt.Errorf(
587598
"[Router] 모호한 라우트가 감지되었습니다 (부트 타임): method=%s, 신규=%s 가 기존=%s 와 충돌합니다",
588599
method, newPath, oldPath,
589-
))
600+
)
590601
}
591602
}
603+
return nil
592604
}
593605

594606
func splitPathForValidation(path string) []string {
@@ -613,13 +625,19 @@ func isPathParam(seg string) bool {
613625
// forwardConsumerErrors는 특정 런타임의 치명적 에러를 공용 채널로 전달한다.
614626
func forwardConsumerErrors(name string, runtime *consumer.Runtime, out chan<- error) {
615627
go func() {
616-
if err := <-runtime.Errors(); err != nil {
628+
select {
629+
case err := <-runtime.Errors():
630+
if err == nil {
631+
return
632+
}
617633
wrapped := fmt.Errorf("[Bootstrap] %s consumer runtime error: %w", name, err)
618634
select {
619635
case out <- wrapped:
620636
default:
621637
log.Printf("%v (consumerErrCh가 가득 차 전파하지 못했습니다)", wrapped)
622638
}
639+
case <-runtime.Done():
640+
return
623641
}
624642
}()
625643
}

0 commit comments

Comments
 (0)