본문으로 건너뛰기

spine.App ​

App은 생성자, HTTP 라우트, 인터셉터, 이벤트 소비자, WebSocket 핸들러와 사용자 정의 전송 방식을 등록하고 애플리케이션을 실행하는 진입점입니다. 이 참조는 Spine v0.5.1의 공개 API와 부트 옵션을 다룹니다.

go
import "github.com/NARUBROWN/spine"

인터페이스 정의 ​

go
type App interface {
    // 생성자 선언
    Constructor(constructors ...any)
    // 라우트 선언
    Route(method string, path string, handler any, opts ...router.RouteOption)
    // 인터셉터 선언
    Interceptor(interceptors ...core.Interceptor)
    // HTTP/WebSocket 범위를 지정한 전역 인터셉터 선언
    InterceptorFor(scope boot.InterceptorScope, interceptors ...core.Interceptor)
    // HTTP 서버 확장 (Echo 등)
    Transport(fn func(any))
    // 독립 실행되는 사용자 정의 전송 방식 등록
    RegisterTransport(t core.CustomTransport)
    // 네트워크 연결 없이 설정 검증
    Validate(opts boot.Options) error
    // 실행
    Run(opts boot.Options) error
    // 이벤트 소비자 레지스트리 반환
    Consumers() *consumer.Registry
    // 웹소켓 레지스트리 반환
    WebSocket() *ws.Registry
}

Route의 옵션은 앱 코드에서 pkg/route의 함수를 사용해 전달합니다. 반환 타입이 내부 패키지에 있는 Consumers()와 WebSocket()도 타입을 직접 선언하지 않고 메서드를 이어 호출할 수 있습니다.

생성자 ​

New ​

go
func New() App

새로운 Spine 애플리케이션 인스턴스를 생성합니다.

반환값

  • App - 애플리케이션 인스턴스

예시

go
app := spine.New()
app.Run(boot.Options{
    Address: ":8080",
    HTTP: &boot.HTTPOptions{},
})

메서드 ​

Constructor ​

go
Constructor(constructors ...any)

의존성 주입 컨테이너에 생성자 함수를 등록합니다. 생성자의 매개변수 타입에 맞춰 다른 등록된 객체를 주입합니다.

매개변수

  • constructors - 생성자 함수들 (가변 인자)

생성자 규칙

  • 함수여야 합니다
  • 반환값은 정확히 하나여야 합니다
  • 매개변수 타입을 제공하는 생성자도 등록해야 합니다

예시

go
// 의존성 없는 생성자
func NewUserRepository() *UserRepository {
    return &UserRepository{}
}

// 의존성 있는 생성자
func NewUserController(repo *UserRepository) *UserController {
    return &UserController{repo: repo}
}

// 이벤트 소비자 생성자
func NewOrderConsumer() *OrderConsumer {
    return &OrderConsumer{}
}

app.Constructor(
    NewUserRepository,
    NewUserController,
    NewOrderConsumer,
)

Route ​

go
Route(method string, path string, handler any, opts ...router.RouteOption)

HTTP 라우트를 등록합니다. method의 앞뒤 공백은 제거하고 대문자로 변환합니다.

매개변수

  • method - HTTP 메서드 ("GET", "POST", "PUT", "DELETE" 등). 대소문자 무시
  • path - URL 경로 패턴. :param 형식으로 경로 매개변수 정의
  • handler - 컨트롤러 메서드 표현식
  • opts - 라우트 옵션 (선택)

경로 패턴

  • /users - 정적 경로
  • /users/:id - 단일 매개변수
  • /users/:userId/posts/:postId - 다중 매개변수

예시

go
// 기본 라우트
app.Route("GET", "/users", (*UserController).List)
app.Route("GET", "/users/:id", (*UserController).GetUser)
app.Route("POST", "/users", (*UserController).CreateUser)
app.Route("PUT", "/users/:id", (*UserController).UpdateUser)
app.Route("DELETE", "/users/:id", (*UserController).DeleteUser)

// 중첩 경로
app.Route("GET", "/users/:userId/posts/:postId", (*PostController).GetPost)

라우트 옵션 ​

route.WithInterceptors로 특정 라우트에만 인터셉터를 적용합니다.

go
import "github.com/NARUBROWN/spine/pkg/route"

// 라우트별 인터셉터 적용
app.Route(
    "GET",
    "/users/:id",
    (*UserController).GetUser,
    route.WithInterceptors(&LoggingInterceptor{}),
)

// 타입이 있는 nil 포인터는 컨테이너에서 조회
app.Route(
    "POST",
    "/admin/users",
    (*AdminController).CreateUser,
    route.WithInterceptors((*AuthInterceptor)(nil)),
)

// 여러 인터셉터 적용
app.Route(
    "POST",
    "/admin/users",
    (*AdminController).CreateUser,
    route.WithInterceptors(
        (*AuthInterceptor)(nil),
        &AdminRoleInterceptor{},
    ),
)

Interceptor ​

go
Interceptor(interceptors ...core.Interceptor)

HTTP 요청과 WebSocket 메시지에 전역 인터셉터를 등록합니다. app.Interceptor(...)는 app.InterceptorFor(boot.InterceptorAll, ...)과 같습니다. 등록 순서대로 PreHandle이 실행되고, 역순으로 PostHandle, BeforeResponse, AfterCompletion이 실행됩니다.

같은 포인터 인스턴스를 여러 범위에 등록하면 범위를 합쳐 한 번 실행합니다. 같은 타입이어도 서로 다른 인스턴스는 각각 실행합니다. (*AuthInterceptor)(nil)처럼 타입이 있는 nil 포인터를 등록하면 컨테이너에서 공유 인스턴스를 찾아 사용합니다.

매개변수

  • interceptors: 인터셉터 인스턴스 목록

실행 순서

  1. 전역 인터셉터의 PreHandle을 등록 순서로 실행합니다.
  2. 라우팅 후 입력값을 해석합니다.
  3. 라우트 인터셉터의 PreHandle을 등록 순서로 실행합니다.
  4. 컨트롤러를 실행하고 응답을 준비한 뒤 실행 후 훅을 호출합니다.
  5. 라우트 인터셉터, 전역 인터셉터 순서로 PostHandle을 실행합니다. 각각 등록의 역순입니다.
  6. 같은 역순으로 BeforeResponse를 실행한 뒤 실제 응답을 기록합니다.
  7. 응답 처리 후 같은 역순으로 AfterCompletion을 실행합니다.

오류와 중단 시의 동작은 인터셉터 참조를 참고하십시오.

예시

go
app.Interceptor(
    cors.New(cors.Config{
        AllowOrigins: []string{"*"},
        AllowMethods: []string{"GET", "POST", "OPTIONS"},
        AllowHeaders: []string{"Content-Type"},
    }),
    &LoggingInterceptor{},
)

InterceptorFor ​

go
InterceptorFor(scope boot.InterceptorScope, interceptors ...core.Interceptor)

전역 인터셉터를 적용할 전송 방식을 선택합니다. 이벤트 소비자에는 적용하지 않습니다.

범위적용 대상
boot.InterceptorHTTPHTTP 요청
boot.InterceptorWebSocketWebSocket 메시지와 지원하는 연결 전 처리
boot.InterceptorAll두 범위 모두
go
app.InterceptorFor(boot.InterceptorHTTP, corsInterceptor)
app.InterceptorFor(boot.InterceptorWebSocket, wsAuthInterceptor)
app.InterceptorFor(boot.InterceptorAll, loggingInterceptor)

Transport ​

go
Transport(fn func(any))

HTTP 서버의 Echo 인스턴스를 설정하는 콜백을 등록합니다. 콜백에서 미들웨어나 정적 파일 경로를 추가할 수 있습니다.

매개변수

  • fn - Echo 인스턴스를 받는 콜백 함수

예시

go
import "github.com/labstack/echo/v4"
import "github.com/labstack/echo/v4/middleware"

app.Transport(func(e any) {
    echo := e.(*echo.Echo)

    // Echo 미들웨어 추가
    echo.Use(middleware.RequestID())

    // 정적 파일 서빙
    echo.Static("/static", "public")
})

RegisterTransport ​

go
RegisterTransport(t core.CustomTransport)

HTTP 파이프라인과 독립적으로 실행할 전송 방식을 등록합니다. 예를 들어 gRPC 서버를 Spine의 의존성 주입과 시작 및 종료 흐름에 연결할 때 사용합니다.

매개변수

  • t - core.CustomTransport 인터페이스 구현체

CustomTransport 인터페이스

go
// core/transport.go
type CustomTransport interface {
    // Init은 DI 컨테이너 준비 이후 호출됩니다.
    Init(container Container) error
    // Start는 Init 이후 별도 고루틴에서 호출됩니다.
    Start() error
    // Stop은 정상 종료 시 호출됩니다.
    Stop(ctx context.Context) error
}

type Container interface {
    Resolve(t reflect.Type) (any, error)
}

예시

go
type GRPCTransport struct {
    server *grpc.Server
}

func (t *GRPCTransport) Init(container core.Container) error {
    // 컨테이너에서 서비스 조회
    svc, err := container.Resolve(reflect.TypeOf((*MyService)(nil)))
    if err != nil {
        return err
    }
    t.server = grpc.NewServer()
    RegisterMyServiceServer(t.server, svc.(*MyService))
    return nil
}

func (t *GRPCTransport) Start() error {
    lis, err := net.Listen("tcp", ":9090")
    if err != nil {
        return err
    }
    return t.server.Serve(lis)
}

func (t *GRPCTransport) Stop(ctx context.Context) error {
    t.server.GracefulStop()
    return nil
}

app.RegisterTransport(&GRPCTransport{})

Consumers ​

go
Consumers() *consumer.Registry

이벤트 소비자 등록용 레지스트리를 반환합니다. Kafka나 RabbitMQ에서 수신할 이벤트 이름과 핸들러를 등록합니다. 소비자 생성자도 Constructor로 등록해야 합니다.

반환값

  • *consumer.Registry - 소비자 레지스트리

예시

go
// 이벤트 소비자 등록
app.Consumers().Register(
    "order.created",            // 토픽/이벤트 이름
    (*OrderConsumer).OnCreated, // 핸들러 메서드
)

app.Consumers().Register(
    "stock.created",
    (*StockConsumer).OnCreated,
)

이벤트 핸들러 ​

이벤트 핸들러는 표준 컨텍스트, 이벤트 이름과 이벤트 DTO를 매개변수로 받을 수 있습니다.

go
type OrderConsumer struct{}

func NewOrderConsumer() *OrderConsumer {
    return &OrderConsumer{}
}

// 이벤트 핸들러
func (c *OrderConsumer) OnCreated(
    ctx context.Context,
    eventName string,
    event OrderCreated,
) error {
    log.Println("이벤트 수신:", eventName)
    log.Println("주문 ID:", event.OrderID)
    return nil
}

// 이벤트 DTO
type OrderCreated struct {
    OrderID int64     `json:"order_id"`
    At      time.Time `json:"at"`
}

WebSocket ​

go
WebSocket() *ws.Registry

WebSocket 레지스트리를 반환합니다. WebSocket 경로와 핸들러를 등록합니다.

반환값

  • *ws.Registry - WebSocket 레지스트리

예시

go
if err := app.WebSocket().Register("/ws/chat", (*ChatController).OnMessage); err != nil {
    log.Fatal(err)
}

WebSocket 핸들러 ​

WebSocket 핸들러는 ws 패키지의 연결 ID와 메시지 DTO를 매개변수로 받습니다. 다음 예는 연결별 전송 객체를 저장하고 메시지를 전체 연결에 전송합니다. ChatController 생성자는 Constructor로 등록합니다.

go
import "github.com/NARUBROWN/spine/pkg/ws"

type ChatController struct {
    mu      sync.RWMutex
    clients map[string]ws.Sender
}

func NewChatController() *ChatController {
    return &ChatController{clients: make(map[string]ws.Sender)}
}

type ChatMessage struct {
    Message string `json:"message"`
}

type ChatEvent struct {
    Type    string `json:"type"`
    From    string `json:"from"`
    Message string `json:"message"`
    At      string `json:"at"`
}

func (c *ChatController) OnMessage(
    ctx context.Context,
    connID ws.ConnectionID,
    msg ChatMessage,
) error {
    sender, ok := ctx.Value(ws.SenderKey).(ws.Sender)
    if ok && sender != nil {
        c.mu.Lock()
        c.clients[connID.Value] = sender
        c.mu.Unlock()
    }

    message := strings.TrimSpace(msg.Message)
    if message == "" {
        return nil
    }

    payload, err := json.Marshal(ChatEvent{
        Type:    "message",
        From:    connID.Value,
        Message: message,
        At:      time.Now().UTC().Format(time.RFC3339),
    })
    if err != nil {
        return err
    }

    c.mu.RLock()
    clients := make(map[string]ws.Sender, len(c.clients))
    for id, client := range c.clients {
        clients[id] = client
    }
    c.mu.RUnlock()

    var firstErr error
    for id, client := range clients {
        if err := client.Send(ws.TextMessage, payload); err != nil {
            if firstErr == nil {
                firstErr = err
            }
            c.mu.Lock()
            delete(c.clients, id)
            c.mu.Unlock()
        }
    }
    return firstErr
}

WebSocket은 HTTP 서버와 같은 Echo 인스턴스에 경로를 등록하므로 boot.Options.HTTP가 필요합니다. 출처, 연결 수와 인증 설정은 보안 설정을 참고하십시오.

Validate ​

go
Validate(opts boot.Options) error

네트워크 연결을 열거나 서버와 소비자를 시작하지 않고 부트 옵션을 검사합니다. Kafka와 RabbitMQ의 연결 설정, HTTP와 WebSocket의 제한값, 전역 인터셉터의 nil 여부와 범위 등을 확인합니다. Run도 시작 전에 같은 검사를 수행합니다.

오류가 있으면 *boot.ConfigError를 반환합니다. Issues의 각 항목에는 설정 위치인 Path, 오류 분류인 Code, 설명인 Message와 수정 안내인 Hint가 있습니다. 유효한 옵션이면 nil입니다.

Validate는 생성자의 의존성을 만들거나 브로커에 접속하지 않습니다. 생성자와 라우트의 적합성, DI 구성, 실제 인프라 연결까지 성공했다고 볼 수는 없습니다.

go
options := boot.Options{Address: ":8080", HTTP: &boot.HTTPOptions{}}
if err := app.Validate(options); err != nil {
    log.Fatal(err)
}
if err := app.Run(options); err != nil {
    log.Fatal(err)
}

Run ​

go
Run(opts boot.Options) error

등록한 구성과 boot.Options에 따라 HTTP 서버, WebSocket, 이벤트 소비자와 사용자 정의 전송 방식을 시작합니다. 실행 중에는 종료 신호나 런타임 오류를 기다립니다. 설정이나 초기화, 실행 또는 종료에 실패하면 오류를 반환합니다.

go
if err := app.Run(boot.Options{
    Address: ":8080",
    HTTP: &boot.HTTPOptions{},
}); err != nil {
    log.Fatal(err)
}

boot.Options ​

서버 실행과 외부 인프라 사용 여부를 지정합니다. HTTP, Kafka, RabbitMQ가 nil이면 해당 기능을 구성하지 않습니다.

go
import "github.com/NARUBROWN/spine/pkg/boot"

구조체 정의 ​

go
type Options struct {
    Address                string
    EnableGracefulShutdown bool
    ShutdownTimeout        time.Duration
    Kafka                  *KafkaOptions
    RabbitMQ               *RabbitMqOptions
    HTTP                   *HTTPOptions
}

type HTTPOptions struct {
    GlobalPrefix      string
    DisableRecover    bool
    ReadHeaderTimeout time.Duration
    ReadTimeout       time.Duration
    WriteTimeout      time.Duration
    IdleTimeout       time.Duration
    MaxHeaderBytes    int
    MaxBodyBytes      int64
    WebSocket         WebSocketOptions
}
Options 필드의미와 기본 동작
AddressHTTP 서버가 수신할 주소. 예: ":8080"
EnableGracefulShutdowntrue이면 HTTP 서버에서 SIGINT, SIGTERM을 받아 정상 종료. 기본값 false
ShutdownTimeoutHTTP 종료와 사용자 정의 전송 방식 종료 시 사용할 대기 시간. 0이면 10초. 음수 불가
KafkaKafka 발행과 소비 설정. nil이면 비활성화
RabbitMQRabbitMQ 발행과 소비 설정. nil이면 비활성화
HTTPHTTP와 WebSocket 설정. nil이면 HTTP 서버 비활성화
HTTPOptions 필드0 또는 생략했을 때제약
GlobalPrefix접두사 없음아래 경로 규칙 적용
DisableRecoverfalse, panic을 HTTP 500으로 바꾸는 복구 미들웨어 사용true이면 미들웨어 비활성화
ReadHeaderTimeout5초음수 불가
ReadTimeout30초음수 불가
WriteTimeout30초음수 불가
IdleTimeout120초음수 불가
MaxHeaderBytes1 MiB음수 불가
MaxBodyBytes32 MiB음수이면 본문 크기 제한 비활성화
WebSocketWebSocket 기본값boot.WebSocketOptions

GlobalPrefix 검증 규칙 ​

Validate와 Run은 다음 규칙에 맞지 않는 값을 HTTP_GLOBAL_PREFIX_INVALID 설정 오류로 반환합니다. 빈 문자열은 접두사를 적용하지 않습니다.

  • "/"로 시작해야 합니다
  • ":"(경로 매개변수)를 포함할 수 없습니다
  • "*"(와일드카드)를 포함할 수 없습니다
  • 끝의 "/"는 자동으로 제거됩니다
go
// 유효한 접두사
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/v1"}
HTTP: &boot.HTTPOptions{GlobalPrefix: "/v2"}

// Validate와 Run이 설정 오류 반환
HTTP: &boot.HTTPOptions{GlobalPrefix: "api"}       // '/'로 시작하지 않음
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/:ver"}  // 경로 매개변수 포함
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/*"}     // 와일드카드 포함

기본 사용 ​

go
app.Run(boot.Options{
    Address: ":8080",
    HTTP: &boot.HTTPOptions{},
})

정상 종료 ​

go
app.Run(boot.Options{
    Address:                ":8080",
    EnableGracefulShutdown: true,
    ShutdownTimeout:        10 * time.Second,
    HTTP: &boot.HTTPOptions{
        GlobalPrefix: "/api/v1",
    },
})

HTTP 정상 종료를 활성화하면 SIGINT, SIGTERM을 받아 새 요청의 유입을 닫고 처리 중인 요청을 기다립니다. WebSocket 연결을 닫고 이벤트 소비자를 중지한 뒤 사용자 정의 전송 방식의 Stop(ctx)를 호출합니다.

ShutdownTimeout은 HTTP 종료와 사용자 정의 전송 방식의 종료 컨텍스트에 적용합니다. 소비자의 Stop은 작업, ACK/NACK와 리더가 종료될 때까지 기다리므로 이 값을 전체 프로세스의 강제 종료 시한으로 해석하지 마십시오. HTTP 없이 소비자나 사용자 정의 전송 방식만 실행할 때도 종료 신호를 처리합니다.

Recover 미들웨어 ​

기본적으로 panic을 HTTP 500 응답으로 변환하는 복구 미들웨어가 활성화되어 있습니다.

go
// Recover 비활성화
HTTP: &boot.HTTPOptions{
    DisableRecover: true,
}

Kafka 설정 ​

KafkaOptions ​

Read와 Write는 독립적입니다. 하나라도 활성화하면 Brokers가 필요하며, Read를 사용하면 GroupID도 필요합니다. 별도 전송 설정이 없으면 TLS 1.2 이상을 적용합니다.

go
type KafkaOptions struct {
    // Kafka 브로커 주소 목록
    Brokers []string

    // 사용자 정의 TLS, 리더 Dialer, 라이터 Transport
    TLS *tls.Config
    Dialer *kafka.Dialer
    Transport *kafka.Transport

    // 격리된 로컬 환경의 평문 연결 명시적 허용
    AllowInsecureTransport bool

    // 리더 오류와 ACK/NACK 실패 뒤 재연결 정책
    ConsumerRetry ConsumerRetryOptions

    // 이벤트 소비 설정
    // nil이면 Kafka 소비자를 활성화하지 않음
    Read *KafkaReadOptions

    // 이벤트 발행 설정
    // nil이면 Kafka로 이벤트를 발행하지 않음
    Write *KafkaWriteOptions
}

type KafkaReadOptions struct {
    // Kafka 소비자 그룹 ID
    GroupID string
}

type KafkaWriteOptions struct {
    // 이벤트 이름 앞에 붙일 토픽 접두사
    TopicPrefix string
}

예시 ​

go
app.Run(boot.Options{
    Address: ":8080",
    Kafka: &boot.KafkaOptions{
        Brokers: []string{"localhost:9092"},
        AllowInsecureTransport: true, // 격리된 로컬 개발 환경 전용
        Read: &boot.KafkaReadOptions{
            GroupID: "my-consumer-group",
        },
        Write: &boot.KafkaWriteOptions{
            TopicPrefix: "myapp.",
        },
    },
    HTTP: &boot.HTTPOptions{},
})

RabbitMQ 설정 ​

RabbitMqOptions ​

Read와 Write는 독립적입니다. 활성화한 역할마다 Exchange를 지정합니다. amqps:// URL을 기본으로 요구하며, 평문 amqp://는 격리된 개발 환경에서만 명시적으로 허용합니다.

go
type RabbitMqOptions struct {
    // RabbitMQ AMQP 연결 문자열
    // 예: amqp://guest:guest@localhost:5672/
    URL string

    // 로컬 amqp:// 연결 명시적 허용
    AllowInsecureTransport bool

    ConsumerRetry  ConsumerRetryOptions
    PublisherRetry PublisherRetryOptions

    // 이벤트 소비 설정
    // nil이면 RabbitMQ 소비자를 활성화하지 않음
    Read *RabbitMqReadOptions

    // 이벤트 발행 설정
    // nil이면 RabbitMQ로 이벤트를 발행하지 않음
    Write *RabbitMqWriteOptions
}

type RabbitMqReadOptions struct {
    // 큐를 바인딩할 교환기 이름
    Exchange string
    PrefetchCount int
    FailurePolicy RabbitMqFailurePolicy
    DeadLetter *RabbitMqDeadLetterOptions

    // Deprecated: FailurePolicy 사용 권장
    RequeueOnError bool
}

type RabbitMqWriteOptions struct {
    // 이벤트를 발행할 교환기 이름
    Exchange string
}

실패 정책과 재시도 기본값은 보안 설정을 참고하십시오. RequeueOnError는 이전 버전과의 호환용이며 새 코드에서는 FailurePolicy를 사용합니다.

예시 ​

go
app.Run(boot.Options{
    Address: ":8080",
    RabbitMQ: &boot.RabbitMqOptions{
        URL:                    "amqp://guest:guest@localhost:5672/",
        AllowInsecureTransport: true, // 격리된 로컬 개발 환경 전용
        Read: &boot.RabbitMqReadOptions{
            Exchange: "stock-exchange",
        },
        Write: &boot.RabbitMqWriteOptions{
            Exchange: "stock-exchange",
        },
    },
    HTTP: &boot.HTTPOptions{},
})

이벤트 발행 ​

컨트롤러에서 도메인 이벤트를 수집하고, 등록된 Kafka나 RabbitMQ 발행기로 전송할 수 있습니다.

DomainEvent 인터페이스 ​

go
import "github.com/NARUBROWN/spine/pkg/event/publish"

type DomainEvent interface {
    Name() string
    OccurredAt() time.Time
}

이벤트 정의 ​

go
type OrderCreated struct {
    OrderID int64     `json:"order_id"`
    At      time.Time `json:"at"`
}

func (e OrderCreated) Name() string {
    return "order.created"
}

func (e OrderCreated) OccurredAt() time.Time {
    return e.At
}

컨트롤러에서 발행 ​

go
import "github.com/NARUBROWN/spine/pkg/event/publish"

func (c *OrderController) Create(ctx context.Context, req *CreateOrderRequest) httpx.Response[Order] {
    order := c.repo.Save(req)

    // 이벤트 발행
    publish.Event(ctx, OrderCreated{
        OrderID: order.ID,
        At:      time.Now(),
    })

    return httpx.Response[Order]{Body: order}
}

publish.Event()는 context.Context에 연결된 이벤트 버스에 이벤트를 수집합니다. 컨트롤러 실행과 응답 준비가 성공하면 실행 후 훅에서 등록된 Kafka 또는 RabbitMQ 발행기로 일괄 전송합니다. 이 전송은 BeforeResponse보다 앞서 실행되므로 DB 트랜잭션의 커밋과 원자적으로 묶이지 않습니다. 원자적 처리가 필요하면 트랜잭셔널 아웃박스를 구성하십시오.

CLI 프로젝트에서 사용 ​

첫 앱 만들기에서 생성한 프로젝트는 internal/application/application.go의 BuildApplication에서 앱을 구성하고, 루트 main.go에서 Validate 오류를 확인한 뒤 Run을 호출합니다. 다음 명령으로 프로젝트를 점검할 수 있습니다.

sh
spine doctor
spine contract check
spine verify

다음은 생성되는 BuildApplication의 기본 형태입니다.

go
package application

import (
	"os"

	"github.com/NARUBROWN/spine"
	"github.com/NARUBROWN/spine/pkg/boot"
	"github.com/NARUBROWN/spine/pkg/httpx"
)

type HealthController struct{}

func NewHealthController() *HealthController { return &HealthController{} }

func (c *HealthController) Get() httpx.Response[map[string]string] {
	return httpx.Response[map[string]string]{Body: map[string]string{"status": "ok"}}
}

// BuildApplication은 서버 실행, 계약 검사와 오프라인 검증이 함께 사용하는 앱 구성 함수입니다.
func BuildApplication() (spine.App, boot.Options, error) {
	app := spine.New()
	app.Constructor(NewHealthController)
	app.Route("GET", "/health", (*HealthController).Get)

	address := ":8080"
	if port := os.Getenv("SPINE_PORT"); port != "" {
		address = ":" + port
	}
	return app, boot.Options{Address: address, HTTP: &boot.HTTPOptions{}}, nil
}

시작과 종료 순서 ​

Run은 옵션 사전 검증 후 의존성 주입 컨테이너와 이벤트 발행기를 구성합니다. 사용자 정의 전송 방식의 Init을 호출하고, HTTP와 WebSocket의 라우트 및 인터셉터를 해석하며 필요한 컨트롤러를 미리 생성합니다. 등록된 소비자가 있으면 브로커 런타임도 검증합니다.

이 준비가 끝난 뒤 소비자와 사용자 정의 전송 방식의 Start를 별도 고루틴에서 실행하고, 마지막에 HTTP 서버를 시작합니다. HTTP 라우트는 같은 메서드에서 경로가 모호하게 겹치면 시작에 실패합니다. 예를 들어 /users/:id와 /users/me는 겹치는 경로로 검사합니다.

종료 시에는 WebSocket과 HTTP로 들어오는 요청을 먼저 닫고, 소비자가 멈출 때까지 기다린 뒤 사용자 정의 전송 방식의 Stop을 초기화 역순으로 호출합니다. 이후 이벤트 발행기의 자원을 정리합니다. 구체적인 구현은 v0.5.1 시작 및 종료 코드를 참고하십시오.

제약과 오류 ​

  • 생성자는 반환값이 정확히 하나여야 합니다. (T, error)를 반환하는 생성자는 등록할 수 없습니다.
  • HTTP 입력 DTO는 포인터로 받고, 성공 응답은 httpx.Response[T]처럼 지원하는 응답 타입으로 반환합니다. 컨트롤러 작성법을 참고하십시오.
  • Validate에 성공해도 Run에서 생성자, 의존성, 라우트 또는 브로커 연결 오류가 발생할 수 있습니다. 두 메서드의 반환 오류를 모두 확인하십시오.
  • WebSocket 경로를 등록하고 HTTP를 생략하면 WEBSOCKET_HTTP_REQUIRED 설정 오류가 발생합니다.
  • InterceptorFor의 범위는 InterceptorHTTP, InterceptorWebSocket, InterceptorAll 중 하나를 사용합니다. 0이나 지원하지 않는 비트가 있으면 INTERCEPTOR_SCOPE_INVALID 설정 오류가 발생합니다.
  • 네트워크 전송, 이벤트 발행과 DB 커밋은 하나의 원자적 작업이 아닙니다. 재시도와 중복 처리가 필요한지 앱에서 결정해야 합니다.

참고 ​