spine.App
메인 애플리케이션 인터페이스에 대한 API 참조.
개요
App은 Spine 애플리케이션의 진입점입니다. 생성자 등록, 라우트 정의, Interceptor 설정, 이벤트 컨슈머 등록, WebSocket 핸들러 등록, Custom Transport 등록, 서버 실행을 담당합니다.
import "github.com/NARUBROWN/spine"인터페이스 정의
type App interface {
// 생성자 선언
Constructor(constructors ...any)
// 라우트 선언
Route(method string, path string, handler any, opts ...router.RouteOption)
// 인터셉터 선언
Interceptor(interceptors ...core.Interceptor)
// HTTP Transport 확장 (Echo 등)
Transport(fn func(any))
// 독립 실행되는 Custom Transport 등록
RegisterTransport(t core.CustomTransport)
// 실행
Run(opts boot.Options) error
// 이벤트 소비자 레지스트리 반환
Consumers() *consumer.Registry
// 웹소켓 레지스트리 반환
WebSocket() *ws.Registry
}생성자
New
func New() App새로운 Spine 애플리케이션 인스턴스를 생성합니다.
반환값
App- 애플리케이션 인스턴스
예시
app := spine.New()
app.Run(boot.Options{
Address: ":8080",
HTTP: &boot.HTTPOptions{},
})메서드
Constructor
Constructor(constructors ...any)IoC Container에 생성자 함수를 등록합니다. 등록된 생성자는 의존성 주입에 사용됩니다.
매개변수
constructors- 생성자 함수들 (가변 인자)
생성자 규칙
- 함수여야 합니다
- 반환값은 정확히 하나여야 합니다
- 매개변수는 다른 등록된 타입이어야 합니다 (의존성)
예시
// 의존성 없는 생성자
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
Route(method string, path string, handler any, opts ...router.RouteOption)HTTP 라우트를 등록합니다. HTTP 메서드는 대문자로 자동 변환됩니다.
매개변수
method- HTTP 메서드 ("GET","POST","PUT","DELETE"등). 대소문자 무시path- URL 경로 패턴.:param형식으로 경로 파라미터 정의handler- Controller 메서드 표현식opts- 라우트 옵션 (선택)
경로 패턴
/users- 정적 경로/users/:id- 단일 파라미터/users/:userId/posts/:postId- 다중 파라미터
예시
// 기본 라우트
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를 사용하여 특정 라우트에만 Interceptor를 적용할 수 있습니다.
import "github.com/NARUBROWN/spine/pkg/route"
// 라우트별 Interceptor 적용
app.Route(
"GET",
"/users/:id",
(*UserController).GetUser,
route.WithInterceptors(&LoggingInterceptor{}),
)
// nil 포인터 → Container에서 Resolve
app.Route(
"POST",
"/admin/users",
(*AdminController).CreateUser,
route.WithInterceptors((*AuthInterceptor)(nil)),
)
// 여러 Interceptor 적용
app.Route(
"POST",
"/admin/users",
(*AdminController).CreateUser,
route.WithInterceptors(
(*AuthInterceptor)(nil),
&AdminRoleInterceptor{},
),
)Interceptor
Interceptor(interceptors ...core.Interceptor)전역 Interceptor를 등록합니다. 전역 Interceptor는 라우팅 전에 실행됩니다. 등록 순서대로 PreHandle이 실행되고, 역순으로 PostHandle과 AfterCompletion이 실행됩니다.
같은 타입이 여러 번 등록되면 최초 등록만 유지됩니다. nil 포인터로 등록하면 Container에서 Resolve됩니다.
매개변수
interceptors- Interceptor 인스턴스들 (가변 인자)
실행 순서
- 전역 Interceptor PreHandle (등록 순서)
- Router
- 라우트 Interceptor PreHandle (등록 순서)
- Controller 실행
- 라우트 Interceptor PostHandle (역순)
- 전역 Interceptor PostHandle (역순)
예시
app.Interceptor(
cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type"},
}),
&LoggingInterceptor{},
)Transport
Transport(fn func(any))HTTP Transport(Echo) 확장 훅을 등록합니다. Echo 인스턴스에 직접 접근하여 미들웨어나 설정을 추가할 수 있습니다.
매개변수
fn- Echo 인스턴스를 받는 콜백 함수
예시
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
RegisterTransport(t core.CustomTransport)Spine HTTP 파이프라인 외부에서 독립 실행되는 Custom Transport를 등록합니다. gRPC, GraphQL 등 별도 프로토콜을 Spine 애플리케이션에 통합할 때 사용합니다.
매개변수
t-core.CustomTransport인터페이스 구현체
CustomTransport 인터페이스
// core/transport.go
type CustomTransport interface {
// Init은 DI Container 준비 이후 호출됩니다.
Init(container Container) error
// Start는 Init 이후 별도 goroutine에서 호출됩니다.
Start() error
// Stop은 Graceful Shutdown 시 호출됩니다.
Stop(ctx context.Context) error
}
type Container interface {
Resolve(t reflect.Type) (any, error)
}예시
type GRPCTransport struct {
server *grpc.Server
}
func (t *GRPCTransport) Init(container core.Container) error {
// Container에서 서비스 Resolve
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, _ := net.Listen("tcp", ":9090")
return t.server.Serve(lis)
}
func (t *GRPCTransport) Stop(ctx context.Context) error {
t.server.GracefulStop()
return nil
}
app.RegisterTransport(&GRPCTransport{})Consumers
Consumers() *consumer.Registry이벤트 컨슈머 레지스트리를 반환합니다. Kafka, RabbitMQ 등의 메시지 브로커에서 이벤트를 수신하는 핸들러를 등록합니다.
반환값
*consumer.Registry- 컨슈머 레지스트리
예시
// 이벤트 컨슈머 등록
app.Consumers().Register(
"order.created", // 토픽/이벤트 이름
(*OrderConsumer).OnCreated, // 핸들러 메서드
)
app.Consumers().Register(
"stock.created",
(*StockConsumer).OnCreated,
)컨슈머 핸들러
컨슈머 핸들러는 HTTP 컨트롤러와 유사한 시그니처를 가집니다.
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
WebSocket() *ws.RegistryWebSocket 레지스트리를 반환합니다. WebSocket 경로와 핸들러를 등록합니다.
반환값
*ws.Registry- WebSocket 레지스트리
예시
app.WebSocket().Register("/ws/chat", (*ChatController).OnMessage)WebSocket 핸들러
WebSocket 핸들러는 HTTP 컨트롤러와 유사한 시그니처를 가집니다. ws 패키지의 타입을 파라미터로 사용합니다.
import "github.com/NARUBROWN/spine/pkg/ws"
type ChatController struct{}
func NewChatController() *ChatController {
return &ChatController{}
}
func (c *ChatController) OnMessage(
connID ws.ConnectionID,
msg ws.TextPayload,
sender ws.Sender,
) {
// connID.Value - 연결 ID
// msg.Value - 텍스트 메시지
// sender.Send() - 응답 전송
sender.Send(ws.TextMessage, []byte("echo: "+msg.Value))
}WebSocket은 HTTP 서버와 같은 Echo 인스턴스를 공유하며, 부트스트랩 시 Transport Hook을 통해 Echo에 자동 마운트됩니다.
Run
Run(opts boot.Options) error애플리케이션을 시작합니다. HTTP 서버, WebSocket 런타임, 이벤트 컨슈머 런타임, Custom Transport를 함께 구동합니다.
매개변수
opts- 부트 옵션 (boot.Options)
반환값
error- 서버 시작 실패 시 에러
예시
if err := app.Run(boot.Options{
Address: ":8080",
HTTP: &boot.HTTPOptions{},
}); err != nil {
log.Fatal(err)
}boot.Options
애플리케이션 부트스트랩 옵션입니다.
import "github.com/NARUBROWN/spine/pkg/boot"구조체 정의
type Options struct {
// 서버가 바인딩될 주소 (예: ":8080")
Address string
// Graceful Shutdown 활성화 여부
EnableGracefulShutdown bool
// Graceful Shutdown 시 최대 대기 시간
ShutdownTimeout time.Duration
// Kafka 이벤트 인프라 설정
// nil인 경우 Kafka는 구성되지 않음
Kafka *KafkaOptions
// RabbitMQ 이벤트 인프라 설정
// nil인 경우 RabbitMQ는 구성되지 않음
RabbitMQ *RabbitMqOptions
// HTTP Runtime 전용 설정
// nil인 경우 HTTP 서버는 실행되지 않음
HTTP *HTTPOptions
}
type HTTPOptions struct {
// HTTP API 전역 Prefix (예: "/api/v1")
// 빈 값이면 Prefix를 적용하지 않습니다.
GlobalPrefix string
// Recover 미들웨어 비활성화 여부 (기본: false = 활성화)
DisableRecover bool
}GlobalPrefix 검증 규칙
부트스트랩 시 GlobalPrefix에 대한 유효성 검사가 수행됩니다:
"/"로 시작해야 합니다":"(Path 파라미터)를 포함할 수 없습니다"*"(와일드카드)를 포함할 수 없습니다- 끝의
"/"는 자동으로 제거됩니다
// ✓ 유효
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/v1"}
HTTP: &boot.HTTPOptions{GlobalPrefix: "/v2"}
// ✗ panic 발생
HTTP: &boot.HTTPOptions{GlobalPrefix: "api"} // '/'로 시작하지 않음
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/:ver"} // Path 파라미터 포함
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/*"} // 와일드카드 포함기본 사용
app.Run(boot.Options{
Address: ":8080",
HTTP: &boot.HTTPOptions{},
})Graceful Shutdown
app.Run(boot.Options{
Address: ":8080",
EnableGracefulShutdown: true,
ShutdownTimeout: 10 * time.Second,
HTTP: &boot.HTTPOptions{
GlobalPrefix: "/api/v1",
},
})Graceful Shutdown이 활성화되면:
SIGINT,SIGTERM시그널을 수신합니다- 진행 중인 요청이 완료될 때까지 대기합니다
- WebSocket 연결에 Close 메시지를 전송합니다
- Custom Transport의
Stop()을 호출합니다 ShutdownTimeout후 강제 종료됩니다 (기본값: 10초)
Recover 미들웨어
기본적으로 Echo의 panic recover 미들웨어가 활성화되어 있습니다. panic 발생 시 500 응답으로 변환합니다.
// Recover 비활성화
HTTP: &boot.HTTPOptions{
DisableRecover: true,
}Kafka 설정
KafkaOptions
type KafkaOptions struct {
// Kafka 브로커 주소 목록
Brokers []string
// 이벤트 소비(Consumer) 설정
// nil이면 Kafka Consumer는 활성화되지 않음
Read *KafkaReadOptions
// 이벤트 발행(Producer) 설정
// nil이면 Kafka로 이벤트를 발행하지 않음
Write *KafkaWriteOptions
}
type KafkaReadOptions struct {
// Kafka Consumer Group ID
GroupID string
}
type KafkaWriteOptions struct {
// 이벤트 이름 앞에 붙일 Topic Prefix
TopicPrefix string
}예시
app.Run(boot.Options{
Address: ":8080",
Kafka: &boot.KafkaOptions{
Brokers: []string{"localhost:9092"},
Read: &boot.KafkaReadOptions{
GroupID: "my-consumer-group",
},
Write: &boot.KafkaWriteOptions{
TopicPrefix: "myapp.",
},
},
HTTP: &boot.HTTPOptions{},
})RabbitMQ 설정
RabbitMqOptions
type RabbitMqOptions struct {
// RabbitMQ AMQP 연결 문자열
// 예: amqp://guest:guest@localhost:5672/
URL string
// 이벤트 소비(Consumer) 설정
// nil이면 RabbitMQ Consumer는 활성화되지 않음
Read *RabbitMqReadOptions
// 이벤트 발행(Publisher) 설정
// nil이면 RabbitMQ로 이벤트를 발행하지 않음
Write *RabbitMqWriteOptions
}
type RabbitMqReadOptions struct {
// 큐가 바인딩될 Exchange 이름
Exchange string
}
type RabbitMqWriteOptions struct {
// 이벤트를 발행할 Exchange 이름
Exchange string
}예시
app.Run(boot.Options{
Address: ":8080",
RabbitMQ: &boot.RabbitMqOptions{
URL: "amqp://guest:guest@localhost:5672/",
Read: &boot.RabbitMqReadOptions{
Exchange: "stock-exchange",
},
Write: &boot.RabbitMqWriteOptions{
Exchange: "stock-exchange",
},
},
HTTP: &boot.HTTPOptions{},
})이벤트 발행
Controller에서 도메인 이벤트를 발행할 수 있습니다.
DomainEvent 인터페이스
import "github.com/NARUBROWN/spine/pkg/event/publish"
type DomainEvent interface {
Name() string
OccurredAt() time.Time
}이벤트 정의
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
}Controller에서 발행
import "github.com/NARUBROWN/spine/pkg/event/publish"
func (c *OrderController) Create(ctx context.Context, req *CreateOrderRequest) Order {
order := c.repo.Save(req)
// 이벤트 발행
publish.Event(ctx, OrderCreated{
OrderID: order.ID,
At: time.Now(),
})
return order
}이벤트는 Controller 실행 완료 후 PostExecutionHook에서 일괄 발행됩니다. publish.Event()는 context.Context에 주입된 EventBus에 이벤트를 수집하며, 실행이 에러 없이 완료되면 등록된 Publisher(Kafka/RabbitMQ)를 통해 전송됩니다.
전체 예시
package main
import (
"context"
"log"
"time"
"github.com/NARUBROWN/spine"
"github.com/NARUBROWN/spine/interceptor/cors"
"github.com/NARUBROWN/spine/pkg/boot"
"github.com/NARUBROWN/spine/pkg/event/publish"
"github.com/NARUBROWN/spine/pkg/path"
"github.com/NARUBROWN/spine/pkg/route"
"github.com/NARUBROWN/spine/pkg/ws"
)
func main() {
app := spine.New()
// 생성자 등록
app.Constructor(
NewUserController,
NewOrderConsumer,
NewChatController,
)
// HTTP 라우트 등록
app.Route("GET", "/users", (*UserController).GetUserQuery)
app.Route("GET", "/users/:id", (*UserController).GetUser,
route.WithInterceptors(&LoggingInterceptor{}),
)
app.Route("POST", "/orders/:orderId", (*UserController).CreateOrder)
// 전역 Interceptor 등록
app.Interceptor(
cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type"},
}),
)
// 이벤트 컨슈머 등록
app.Consumers().Register("order.created", (*OrderConsumer).OnCreated)
// WebSocket 등록
app.WebSocket().Register("/ws/chat", (*ChatController).OnMessage)
// 서버 실행
app.Run(boot.Options{
Address: ":8080",
EnableGracefulShutdown: true,
ShutdownTimeout: 10 * time.Second,
RabbitMQ: &boot.RabbitMqOptions{
URL: "amqp://guest:guest@localhost:5672/",
Read: &boot.RabbitMqReadOptions{
Exchange: "stock-exchange",
},
Write: &boot.RabbitMqWriteOptions{
Exchange: "stock-exchange",
},
},
HTTP: &boot.HTTPOptions{},
})
}
// Controller
type UserController struct{}
func NewUserController() *UserController {
return &UserController{}
}
func (c *UserController) CreateOrder(ctx context.Context, orderId path.Int) string {
publish.Event(ctx, OrderCreated{
OrderID: orderId.Value,
At: time.Now(),
})
return "OK"
}
// Event
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 }
// Consumer
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
}
// WebSocket
type ChatController struct{}
func NewChatController() *ChatController {
return &ChatController{}
}
func (c *ChatController) OnMessage(
connID ws.ConnectionID,
msg ws.TextPayload,
sender ws.Sender,
) {
sender.Send(ws.TextMessage, []byte("echo: "+msg.Value))
}부트스트랩 순서
Run() 호출 시 다음 순서로 초기화됩니다:
- IoC Container 생성
- 생성자 등록
- 이벤트 인프라 구성 (설정된 경우)
- Kafka Publisher / Consumer
- RabbitMQ Publisher / Consumer
- Custom Transport 초기화 (
Init()호출) - Custom Transport 시작 (
Start()- 별도 goroutine) - HTTP Runtime 구성 (설정된 경우)
- Router 구성 및 HandlerMeta 생성
- 라우트 Interceptor Resolve (nil 포인터 → Container)
- 중복 경로 검증 (
assertNoAmbiguousRoute) - Controller Warm-up (의존성 미리 해결)
- HTTP Pipeline 구성 (ArgumentResolver, ReturnValueHandler 등록)
- 전역 Interceptor 등록 (중복 타입 제거, nil 포인터 Resolve)
- WebSocket Runtime 구성 (등록된 경우)
- WS 전용 Pipeline 생성
- Echo Transport Hook으로 자동 마운트
- Echo Adapter 마운트 (Recover 미들웨어 포함)
- Consumer Runtime 시작 (설정된 경우)
- HTTP 서버 시작
Graceful Shutdown 시
SIGINT/SIGTERM시그널 수신- WebSocket 연결에 Close 메시지 전송
- 이벤트 컨슈머 런타임 중지
- Custom Transport
Stop()호출 - HTTP 서버 Shutdown (타임아웃까지 대기)
- 이벤트 Publisher 리소스 정리
- 종료
참고
- Interceptor - Interceptor 인터페이스
- 실행 파이프라인 - 요청 처리 흐름
- IoC Container - 의존성 주입
