Skip to content

실행 컨텍스트 (ExecutionContext)

Spine 요청의 핵심.

개요

ExecutionContext는 Spine 파이프라인 전체에서 공유되는 요청 스코프 컨텍스트입니다. HTTP 요청이 도착하면 Transport 어댑터가 ExecutionContext를 생성하고, 이 컨텍스트는 파이프라인의 모든 단계를 통과하며 요청 정보와 실행 상태를 전달합니다.

Context 계층 구조

Spine은 Context를 계층적으로 분리합니다. 이는 HTTP, Event Consumer, WebSocket을 동일한 파이프라인 모델로 처리하기 위한 설계입니다.

왜 이렇게 분리하는가?

계층담당사용 위치
ContextCarrierGo 표준 context 전달모든 곳
EventBusCarrier도메인 이벤트 발행 (core.EventBus)Controller, Consumer
ExecutionContext실행 흐름 제어Router, Pipeline, Interceptor
ControllerContextExecutionContext의 읽기 전용 FacadeController (Interceptor 주입 값 참조)
HttpRequestContextHTTP 입력 해석HTTP ArgumentResolver
ConsumerRequestContext이벤트 입력 해석Consumer ArgumentResolver
WebSocketContextWebSocket 입력 해석WebSocket ArgumentResolver

목표: HTTP, Event Consumer, WebSocket이 동일한 파이프라인 모델을 공유하면서, 각 프로토콜의 특성에 맞는 입력 해석이 가능하게 합니다.

기반 인터페이스

ContextCarrier

Go 표준 context.Context를 전달하는 최소 계약입니다.

go
// core/context.go
type ContextCarrier interface {
    Context() context.Context
}

EventBusCarrier

도메인 이벤트 발행을 위한 EventBus 접근 계약입니다. 반환 타입은 core.EventBus입니다.

go
// core/context.go
type EventBusCarrier interface {
    EventBus() EventBus
}

core.EventBus는 도메인 이벤트를 수집했다가 실행 후 한 번에 방출하기 위한 최소 계약입니다.

go
// core/event_bus.go
type EventBus interface {
    Publish(events ...publish.DomainEvent)
    Drain() []publish.DomainEvent
}

참고: internal/event/publish.EventBuscore.EventBus의 타입 별칭(type EventBus = core.EventBus)으로, 내부 구현체가 이 타입을 만족하도록 구성됩니다.

ExecutionContext 인터페이스

파이프라인 전체에서 사용되는 실행 흐름 제어용 인터페이스입니다.

go
// core/context.go
type ExecutionContext interface {
    ContextCarrier
    EventBusCarrier

    // HTTP 요청 정보 (Consumer/WebSocket에서는 의미가 다름)
    Method() string                    // HTTP: GET, POST... / Consumer: "EVENT" / WS: "WS"
    Path() string                      // HTTP: /users/123 / Consumer: EventName / WS: path
    Header(name string) string         // HTTP 헤더 (Consumer, WS는 빈 문자열)
    
    // 파라미터 접근
    Params() map[string]string         // Path parameters
    PathKeys() []string                // Path key 순서
    Queries() map[string][]string      // Query parameters
    
    // 내부 저장소
    Set(key string, value any)         // 값 저장
    Get(key string) (any, bool)        // 값 조회
}

메서드 상세

Context()

Go 표준 context.Context를 반환합니다. 요청 취소, 타임아웃, 값 전달에 사용됩니다.

go
func (e *echoContext) Context() context.Context {
    return e.reqCtx  // HTTP 요청의 context
}

EventBus()

요청 스코프의 EventBus를 반환합니다. Controller에서 도메인 이벤트를 발행할 때 사용됩니다.

go
func (c *echoContext) EventBus() publish.EventBus {
    return c.eventBus
}

Method() / Path()

HTTP 요청의 메서드와 경로를 반환합니다. Consumer와 WebSocket에서는 다른 의미로 사용됩니다.

go
// HTTP
ctx.Method()  // "GET"
ctx.Path()    // "/users/123/posts/456"

// Consumer
ctx.Method()  // "EVENT"
ctx.Path()    // "order.created" (EventName)

// WebSocket
ctx.Method()  // "WS"
ctx.Path()    // WebSocket 경로

Params() / PathKeys()

Path parameter 정보를 제공합니다.

go
// Route: /users/:userId/posts/:postId
// Request: /users/123/posts/456

ctx.Params()    // {"userId": "123", "postId": "456"}
ctx.PathKeys()  // ["userId", "postId"]

PathKeys()는 파라미터의 선언 순서를 보장합니다. Spine의 순서 기반 바인딩에 필수적입니다.

Queries()

Query parameter를 다중 값 형태로 반환합니다.

go
// Request: /users?status=active&tag=go&tag=web

ctx.Queries()  // {"status": ["active"], "tag": ["go", "web"]}

Set() / Get()

파이프라인 내부에서 값을 공유하는 저장소입니다.

go
// Router에서 path params 저장
ctx.Set("spine.params", params)
ctx.Set("spine.pathKeys", keys)

// Adapter에서 ResponseWriter 저장
ctx.Set("spine.response_writer", NewEchoResponseWriter(c))

// Interceptor에서 조회
rw, ok := ctx.Get("spine.response_writer")

ControllerContext 인터페이스

Controller 전용 Context View입니다. ExecutionContext의 읽기 전용 Facade로, Interceptor에서 주입한 값을 Controller에서 참조하기 위한 공식 통로입니다.

go
// core/context.go
type ControllerContext interface {
    Get(key string) (any, bool)
}

구현

go
// internal/runtime/controller_ctx.go
type controllerCtxView struct {
    ec core.ExecutionContext
}

func NewControllerContext(ec core.ExecutionContext) core.ControllerContext {
    return controllerCtxView{ec: ec}
}

func (v controllerCtxView) Get(key string) (any, bool) {
    return v.ec.Get(key)
}

사용 예시

go
// Controller에서 Interceptor가 주입한 값 참조
func (c *UserController) GetUser(ctx context.Context, cc core.ControllerContext, userId path.Int) User {
    authInfo, _ := cc.Get("auth.user")
    // ...
}

참고: pkg/spine/types.go에는 Ctx 인터페이스(Get(key string) (any, bool))가 정의되어 있어, 사용자 코드에서 spine.Ctx로도 접근 가능합니다.

HttpRequestContext 인터페이스

HTTP 전용 확장 인터페이스입니다. HTTP ArgumentResolver에서 사용됩니다.

go
// core/context.go
type HttpRequestContext interface {
    ContextCarrier
    EventBusCarrier

    // 개별 파라미터 접근
    Param(name string) string          // 특정 path param
    Query(name string) string          // 특정 query param (첫 번째 값)
    Header(name string) string         // 특정 헤더
    
    // 전체 뷰 접근
    Params() map[string]string         // 모든 path params
    Queries() map[string][]string      // 모든 query params
    Headers() map[string][]string      // 모든 헤더
    
    // Body 바인딩
    Bind(out any) error                // JSON body → struct
    
    // Multipart
    MultipartForm() (*multipart.Form, error)
}

참고: HttpRequestContextRequestContext를 포함하지 않습니다. ContextCarrierEventBusCarrier를 직접 임베딩합니다. 또한 Headers() map[string][]string 메서드가 추가되어 전체 헤더 맵에 접근할 수 있습니다.

메서드 상세

Param() / Query()

개별 파라미터에 편리하게 접근합니다.

go
// Route: /users/:id?page=1&size=20

ctx.Param("id")      // "123"
ctx.Query("page")    // "1"
ctx.Query("size")    // "20"
ctx.Query("missing") // "" (없으면 빈 문자열)

Bind()

HTTP body를 구조체로 바인딩합니다.

go
// internal/resolver/dto_resolver.go
func (r *DTOResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
    httpCtx, ok := ctx.(core.HttpRequestContext)
    if !ok {
        return nil, fmt.Errorf("HTTP 요청 컨텍스트가 아닙니다")
    }

    valuePtr := reflect.New(parameterMeta.Type)

    if err := httpCtx.Bind(valuePtr.Interface()); err != nil {
        return nil, fmt.Errorf("DTO 바인딩 실패 (%s): %w", parameterMeta.Type.Name(), err)
    }

    return valuePtr.Elem().Interface(), nil
}

MultipartForm()

Multipart form 데이터에 접근합니다. 파일 업로드 처리에 사용됩니다.

go
// internal/resolver/uploaded_files_resolver.go
func (r *UploadedFilesResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
    httpCtx, ok := ctx.(core.HttpRequestContext)
    if !ok {
        return nil, fmt.Errorf("HTTP 요청 컨텍스트가 아닙니다")
    }

    form, err := httpCtx.MultipartForm()
    if err != nil {
        return nil, err
    }
    // ...
}

ConsumerRequestContext 인터페이스

Event Consumer 전용 확장 인터페이스입니다.

go
// core/context.go
type ConsumerRequestContext interface {
    ContextCarrier
    EventBusCarrier

    EventName() string    // 이벤트 이름 (예: "order.created")
    Payload() []byte      // 이벤트 페이로드 (JSON 등)
}

메서드 상세

EventName()

수신한 이벤트의 이름을 반환합니다.

go
ctx.EventName()  // "order.created"

Payload()

이벤트의 원시 페이로드를 반환합니다.

go
payload := ctx.Payload()  // []byte (JSON)

Consumer Resolver 예시

go
// internal/event/consumer/resolver/dto_resolver.go
func (r *DTOResolver) Resolve(ctx core.ExecutionContext, meta resolver.ParameterMeta) (any, error) {
    consumerCtx, ok := ctx.(core.ConsumerRequestContext)
    if !ok {
        return nil, fmt.Errorf("ConsumerRequestContext가 아닙니다")
    }

    payload := consumerCtx.Payload()
    if payload == nil {
        return nil, fmt.Errorf("Payload가 비어있어 DTO를 생성할 수 없습니다")
    }

    dtoPtr := reflect.New(meta.Type)
    if err := json.Unmarshal(payload, dtoPtr.Interface()); err != nil {
        return nil, fmt.Errorf("DTO 역직렬화에 실패했습니다: %w", err)
    }

    return dtoPtr.Elem().Interface(), nil
}

WebSocketContext 인터페이스

WebSocket 전용 ExecutionContext 확장입니다. ExecutionContext를 임베딩하여 파이프라인 호환성을 유지합니다.

go
// core/context.go
type WebSocketContext interface {
    ExecutionContext

    ConnID() string       // 연결 ID
    MessageType() int     // 메시지 타입 (Text, Binary 등)
    Payload() []byte      // 메시지 페이로드
}

WebSocket Resolver 예시

go
// internal/ws/resolver/dto_resolver.go
func (r *DTOResolver) Resolve(ctx core.ExecutionContext, meta resolver.ParameterMeta) (any, error) {
    wsCtx, ok := ctx.(core.WebSocketContext)
    if !ok {
        return nil, fmt.Errorf("WebSocketContext가 아닙니다")
    }

    payload := wsCtx.Payload()
    if payload == nil {
        return nil, fmt.Errorf("Payload가 비어있어 DTO를 생성할 수 없습니다")
    }

    dtoPtr := reflect.New(meta.Type)
    if err := json.Unmarshal(payload, dtoPtr.Interface()); err != nil {
        return nil, fmt.Errorf("DTO 역직렬화 실패: %w", err)
    }

    return dtoPtr.Elem().Interface(), nil
}

Echo 어댑터 구현

Spine은 Echo를 HTTP Transport 레이어로 사용합니다. echoContextExecutionContextHttpRequestContext를 모두 구현합니다.

go
// internal/adapter/echo/context_impl.go
type echoContext struct {
    echo     echo.Context           // Echo의 원본 컨텍스트
    reqCtx   context.Context        // 요청 스코프 컨텍스트
    store    map[string]any         // 내부 저장소
    eventBus publish.EventBus       // 이벤트 버스
}

func NewContext(c echo.Context) core.ExecutionContext {
    return &echoContext{
        echo:     c,
        reqCtx:   c.Request().Context(),
        store:    make(map[string]any),
        eventBus: publish.NewEventBus(),
    }
}

주요 구현

Path Parameters

Router가 매칭한 결과를 우선 사용하고, 없으면 Echo의 값을 사용합니다.

go
func (e *echoContext) Param(name string) string {
    // Spine Router가 저장한 값 우선
    if raw, ok := e.store["spine.params"]; ok {
        if m, ok := raw.(map[string]string); ok {
            if v, ok := m[name]; ok {
                return v
            }
        }
    }
    // Fallback to Echo
    return e.echo.Param(name)
}

Params() - 방어적 복사

외부에서 원본 맵을 변경하지 못하도록 복사본을 반환합니다. maps.Copy를 사용합니다.

go
func (e *echoContext) Params() map[string]string {
    if raw, ok := e.store["spine.params"]; ok {
        if m, ok := raw.(map[string]string); ok {
            // return a shallow copy to avoid mutation
            copyMap := make(map[string]string, len(m))
            maps.Copy(copyMap, m)
            return copyMap
        }
    }
    // Echo에서 직접 구성
    names := e.echo.ParamNames()
    values := e.echo.ParamValues()
    params := make(map[string]string, len(names))
    for i, name := range names {
        if i < len(values) {
            params[name] = values[i]
        }
    }
    return params
}

Headers()

모든 HTTP 헤더를 맵으로 반환합니다.

go
func (e *echoContext) Headers() map[string][]string {
    return e.echo.Request().Header
}

EventBus

요청 스코프의 EventBus를 반환합니다.

go
func (c *echoContext) EventBus() publish.EventBus {
    return c.eventBus
}

Consumer 어댑터 구현

Event Consumer용 Context 구현입니다.

go
// internal/event/consumer/request_context_impl.go
type ConsumerRequestContextImpl struct {
    ctx      context.Context
    msg      *Message
    eventBus publish.EventBus
    store    map[string]any
}

func NewRequestContext(
    ctx context.Context,
    msg *Message,
    eventBus publish.EventBus,
) core.ExecutionContext {
    return &ConsumerRequestContextImpl{
        ctx:      ctx,
        msg:      msg,
        eventBus: eventBus,
        store:    make(map[string]any),
    }
}

Consumer Context의 특수 동작

Consumer는 HTTP가 아니므로 일부 메서드가 다르게 동작합니다.

go
func (c *ConsumerRequestContextImpl) Method() string {
    // Consumer 실행은 HTTP Method 개념이 없으며, 라우팅 구분을 위해 "EVENT" 사용
    return "EVENT"
}

func (c *ConsumerRequestContextImpl) Path() string {
    // Consumer 라우팅에서 Path는 EventName을 그대로 사용
    return c.msg.EventName
}

func (c *ConsumerRequestContextImpl) Header(key string) string {
    // Consumer에는 HTTP Header 개념이 없음
    return ""
}

func (c *ConsumerRequestContextImpl) Params() map[string]string {
    // Consumer에는 Path Parameter 개념이 없음
    return map[string]string{}
}

func (c *ConsumerRequestContextImpl) PathKeys() []string {
    // Consumer에는 Path Key 개념이 없음
    return []string{}
}

func (c *ConsumerRequestContextImpl) Queries() map[string][]string {
    // Consumer에는 Query Parameter 개념이 없음
    return map[string][]string{}
}

WebSocket 어댑터 구현

WebSocket용 Context 구현입니다. core.WebSocketContext를 구현합니다.

go
// internal/ws/context_impl.go
type WSExecutionContext struct {
    ctx         context.Context
    connID      string
    path        string
    messageType int
    payload     []byte
    eventBus    publish.EventBus
    store       map[string]any
}

func NewWSExecutionContext(
    ctx context.Context,
    connID string,
    path string,
    messageType int,
    payload []byte,
    eventBus publish.EventBus,
    sendFn func(int, []byte) error,
) core.WebSocketContext {
    ctx = context.WithValue(ctx, pkgws.SenderKey, &connSender{send: sendFn})

    return &WSExecutionContext{
        ctx:         ctx,
        connID:      connID,
        path:        path,
        messageType: messageType,
        payload:     payload,
        eventBus:    eventBus,
        store:       make(map[string]any),
    }
}

WebSocket Context의 특수 동작

go
func (w *WSExecutionContext) Method() string {
    return "WS"
}

func (w *WSExecutionContext) ConnID() string {
    return w.connID
}

func (w *WSExecutionContext) MessageType() int {
    return w.messageType
}

func (w *WSExecutionContext) Payload() []byte {
    return w.payload
}

func (w *WSExecutionContext) EventBus() core.EventBus {
    return w.eventBus
}

ArgumentResolver와 Context

ArgumentResolver는 ExecutionContext를 받고, 필요에 따라 프로토콜별 Context로 타입 단언합니다.

go
// internal/resolver/argument.go
type ArgumentResolver interface {
    Supports(parameterMeta ParameterMeta) bool
    Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error)
}

HTTP Resolver 예시

go
// internal/resolver/path_int_resolver.go
func (r *PathIntResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
    // HttpRequestContext로 타입 단언
    httpCtx, ok := ctx.(core.HttpRequestContext)
    if !ok {
        return nil, fmt.Errorf("HTTP 요청 컨텍스트가 아닙니다")
    }

    raw, ok := httpCtx.Params()[parameterMeta.PathKey]
    if !ok {
        return nil, fmt.Errorf("path param을 찾을 수 없습니다. %s", parameterMeta.PathKey)
    }

    value, err := strconv.ParseInt(raw, 10, 64)
    if err != nil {
        return nil, err
    }

    return path.Int{Value: value}, nil
}

Consumer Resolver 예시

go
// internal/event/consumer/resolver/event_name_resolver.go
func (r *EventNameResolver) Resolve(ctx core.ExecutionContext, meta resolver.ParameterMeta) (any, error) {
    // ConsumerRequestContext로 타입 단언
    consumerCtx, ok := ctx.(core.ConsumerRequestContext)
    if !ok {
        return nil, fmt.Errorf("ConsumerRequestContext가 아닙니다")
    }

    name := consumerCtx.EventName()
    if name == "" {
        return nil, fmt.Errorf("EventName을 RequestContext에서 찾을 수 없습니다")
    }

    return name, nil
}

WebSocket Resolver 예시

go
// internal/ws/resolver/dto_resolver.go
func (r *DTOResolver) Resolve(ctx core.ExecutionContext, meta resolver.ParameterMeta) (any, error) {
    wsCtx, ok := ctx.(core.WebSocketContext)
    if !ok {
        return nil, fmt.Errorf("WebSocketContext가 아닙니다")
    }

    payload := wsCtx.Payload()
    if payload == nil {
        return nil, fmt.Errorf("Payload가 비어있어 DTO를 생성할 수 없습니다")
    }

    dtoPtr := reflect.New(meta.Type)
    if err := json.Unmarshal(payload, dtoPtr.Interface()); err != nil {
        return nil, fmt.Errorf("DTO 역직렬화 실패: %w", err)
    }

    return dtoPtr.Elem().Interface(), nil
}

공통 Resolver 예시

StdContextResolver는 HTTP, Consumer, WebSocket 모두에서 동작합니다.

go
// internal/resolver/std_context_resolver.go
func (r *StdContextResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
    baseCtx := ctx.Context()
    bus := ctx.EventBus()
    if bus != nil {
        return context.WithValue(baseCtx, publish.PublisherKey, bus), nil
    }
    return baseCtx, nil
}

ControllerContext Resolver

ControllerContextResolverExecutionContext를 읽기 전용 ControllerContext로 래핑합니다.

go
// internal/resolver/controller_context_resolver.go
func (r *ControllerContextResolver) Resolve(ctx core.ExecutionContext, _ ParameterMeta) (any, error) {
    return runtime.NewControllerContext(ctx), nil
}

파이프라인에서의 사용

Router

go
// internal/router/router.go
func (r *DefaultRouter) Route(ctx core.ExecutionContext) (core.HandlerMeta, error) {
    for _, route := range r.routes {
        if route.Method != ctx.Method() {
            continue
        }
        
        ok, params, keys := matchPath(route.Path, ctx.Path())
        if !ok {
            continue
        }
        
        // 매칭된 정보를 Context에 저장
        ctx.Set("spine.params", params)
        ctx.Set("spine.pathKeys", keys)
        
        return route.Meta, nil
    }
    return core.HandlerMeta{}, httperr.NotFound("핸들러가 없습니다.")
}

Pipeline - Execute 흐름

go
// internal/pipeline/pipeline.go
func (p *Pipeline) Execute(ctx core.ExecutionContext) (finalErr error) {
    // 1. 글로벌 Interceptor PreHandle (라우팅 전)
    // 2. Router가 실행 대상을 결정
    // 3. 라우트 Interceptor PreHandle
    // 4. ArgumentResolver 체인 실행
    // 5. Controller Method 호출 (Invoker)
    // 6. ReturnValueHandler 처리
    // 7. PostExecutionHook (이벤트 발행 등)
    // 8. 라우트 Interceptor PostHandle (역순)
    // 9. 글로벌 Interceptor PostHandle (역순)
    // 10. AfterCompletion (성공/실패 무관, 역순)
}

Pipeline - ArgumentResolver 호출

go
// internal/pipeline/pipeline.go
func (p *Pipeline) resolveArguments(ctx core.ExecutionContext, paramMetas []resolver.ParameterMeta) ([]any, error) {
    args := make([]any, 0, len(paramMetas))

    for _, paramMeta := range paramMetas {
        resolved := false

        for _, r := range p.argumentResolvers {
            if !r.Supports(paramMeta) {
                continue
            }

            // ExecutionContext를 직접 전달
            // Resolver 내부에서 필요한 타입으로 단언
            val, err := r.Resolve(ctx, paramMeta)
            if err != nil {
                return nil, err
            }

            args = append(args, val)
            resolved = true
            break
        }

        if !resolved {
            return nil, fmt.Errorf(
                "ArgumentResolver에 parameter가 없습니다. %d (%s)",
                paramMeta.Index,
                paramMeta.Type.String(),
            )
        }
    }
    return args, nil
}

Interceptor

go
// interceptor/cors/cors.go
func (i *CORSInterceptor) PreHandle(ctx core.ExecutionContext, meta core.HandlerMeta) error {
    // ResponseWriter 획득
    rwAny, ok := ctx.Get("spine.response_writer")
    if !ok {
        return nil
    }
    rw := rwAny.(core.ResponseWriter)
    
    // 요청 정보 확인
    origin := ctx.Header("Origin")
    if origin != "" && i.isAllowedOrigin(origin) {
        rw.SetHeader("Access-Control-Allow-Origin", origin)
    }
    
    // Preflight 처리
    if ctx.Method() == "OPTIONS" {
        rw.WriteStatus(204)
        return core.ErrAbortPipeline
    }
    
    return nil
}

내부 저장소 규약

Set()/Get()으로 사용하는 키에는 명확한 규약이 있습니다.

Spine 예약 키

타입설정 위치용도
spine.paramsmap[string]stringRouterPath parameter 값
spine.pathKeys[]stringRouterPath key 순서
spine.response_writercore.ResponseWriterAdapter응답 출력

사용 예시

go
// ReturnValueHandler에서 ResponseWriter 사용
func (h *JSONReturnHandler) Handle(value any, ctx core.ExecutionContext) error {
    rwAny, ok := ctx.Get("spine.response_writer")
    if !ok {
        return fmt.Errorf("ExecutionContext 안에서 ResponseWriter를 찾을 수 없습니다.")
    }
    
    rw, ok := rwAny.(core.ResponseWriter)
    if !ok {
        return fmt.Errorf("ResponseWriter 타입이 올바르지 않습니다.")
    }
    
    return rw.WriteJSON(200, value)
}

EventBus 통합

ExecutionContextcore.EventBus가 통합되어 있습니다.

Controller에서 이벤트 발행

go
// cmd/demo/controller.go
func (c *UserController) CreateOrder(ctx context.Context, orderId path.Int) string {
    // context.Context에서 EventBus를 꺼내 이벤트 발행
    publish.Event(ctx, OrderCreated{
        OrderID: orderId.Value,
        At:      time.Now(),
    })

    return "OK"
}

EventBus 주입 흐름

go
// internal/resolver/std_context_resolver.go
func (r *StdContextResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
    baseCtx := ctx.Context()
    bus := ctx.EventBus()
    if bus != nil {
        // EventBus를 context.Context에 주입
        return context.WithValue(baseCtx, publish.PublisherKey, bus), nil
    }
    return baseCtx, nil
}

PostExecutionHook에서 이벤트 방출

Pipeline 실행 완료 후 수집된 이벤트를 한 번에 방출합니다.

go
// internal/event/hook/post_execution.go
func (h *EventDispatchHook) AfterExecution(ctx core.ExecutionContext, results []any, err error) {
    if err != nil {
        return
    }

    events := ctx.EventBus().Drain()
    if len(events) == 0 {
        return
    }

    h.Dispatcher.Dispatch(ctx.Context(), events)
}

설계 원칙

1. Controller는 ExecutionContext를 모른다

Controller는 ExecutionContextHttpRequestContext를 직접 받지 않습니다. 대신 의미 타입(path.Int, query.Values 등), context.Context, 그리고 필요 시 ControllerContext로 값만 받습니다.

go
// ❌ 안티패턴
func (c *UserController) GetUser(ctx core.ExecutionContext) User

// ✓ Spine 방식
func (c *UserController) GetUser(ctx context.Context, userId path.Int) User

// ✓ Interceptor 주입 값이 필요할 때
func (c *UserController) GetUser(ctx context.Context, cc core.ControllerContext, userId path.Int) User

2. Resolver는 ExecutionContext를 받고 필요한 타입으로 단언한다

ArgumentResolver는 ExecutionContext를 받습니다. 프로토콜별 기능이 필요하면 HttpRequestContext, ConsumerRequestContext, 또는 WebSocketContext로 타입 단언합니다.

go
func (r *PathIntResolver) Resolve(ctx core.ExecutionContext, parameterMeta ParameterMeta) (any, error) {
    httpCtx, ok := ctx.(core.HttpRequestContext)
    if !ok {
        return nil, fmt.Errorf("HTTP 요청 컨텍스트가 아닙니다")
    }
    // ...
}

3. 단일 파이프라인, 다중 프로토콜

HTTP, Event Consumer, WebSocket이 동일한 파이프라인 구조를 공유합니다. Context 계층 분리로 각 프로토콜의 특성을 지원하면서 코드 재사용을 극대화합니다.

go
// HTTP Pipeline
httpPipeline.AddArgumentResolver(
    &resolver.StdContextResolver{},           // 공통
    &resolver.ControllerContextResolver{},    // 공통
    &resolver.HeaderResolver{},               // HTTP 전용
    &resolver.PathIntResolver{},              // HTTP 전용
    &resolver.PathStringResolver{},           // HTTP 전용
    &resolver.PathBooleanResolver{},          // HTTP 전용
    &resolver.PaginationResolver{},           // HTTP 전용
    &resolver.QueryValuesResolver{},          // HTTP 전용
    &resolver.DTOResolver{},                  // HTTP 전용
    &resolver.FormDTOResolver{},              // HTTP 전용
    &resolver.UploadedFilesResolver{},        // HTTP 전용
)

// Consumer Pipeline
consumerPipeline.AddArgumentResolver(
    &resolver.StdContextResolver{},           // 공통
    &eventResolver.EventNameResolver{},       // Consumer 전용
    &eventResolver.DTOResolver{},             // Consumer 전용
)

// WebSocket Pipeline
wsPipeline.AddArgumentResolver(
    &resolver.StdContextResolver{},           // 공통
    &wsResolver.ConnectionIDResolver{},       // WebSocket 전용
    &wsResolver.DTOResolver{},                // WebSocket 전용
)

요약

인터페이스역할주요 메서드사용 위치
ContextCarrierGo context 전달Context()모든 곳
EventBusCarrier이벤트 발행 (core.EventBus)EventBus()Controller, Consumer
ExecutionContext실행 흐름 제어Method(), Path(), Header(), Set(), Get()Router, Pipeline, Interceptor
ControllerContextExecutionContext 읽기 전용 FacadeGet()Controller
HttpRequestContextHTTP 입력 해석Param(), Query(), Header(), Headers(), Bind(), MultipartForm()HTTP ArgumentResolver
ConsumerRequestContext이벤트 입력 해석EventName(), Payload()Consumer ArgumentResolver
WebSocketContextWebSocket 입력 해석ConnID(), MessageType(), Payload()WebSocket ArgumentResolver

핵심 원칙: Context 계층 분리로 HTTP, Event Consumer, WebSocket이 동일한 파이프라인 모델을 공유합니다. Controller는 실행 모델을 전혀 알지 못하며, 오직 비즈니스 로직에만 집중합니다.