마이크로서비스 아키텍처를 위한 표준화된 서버 구조를 설계하는 과제를 맡았습니다.
주요 요구사항은 다음과 같았습니다.
서버의 핵심 구조와 핸들러 관리 로직을 담당하는 파일입니다. 핸들러 인터페이스와 서버 구성을 위한 옵션 패턴을 정의했습니다.
// 모든 핸들러(HTTP, gRPC)가 구현해야 하는 공통 인터페이스
type Handler interface {
Start() error
IsMainThread() bool // true => 메인 스레드로 실행, false => 고루틴으로 실행
}
// 서버의 핵심 구조체
type Server struct {
handlers []Handler // http handler, grpc handler, kafka handler 등 외부 요소 핸들링
}
// 함수형 옵션 패턴을 위한 타입 정의
type Option func(*Server) error
// 서버 생성 함수 - 옵션 패턴 적용
func New(opts ...Option) (*Server, error) {
s := &Server{}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, fmt.Errorf("failed to apply server option: %w", err)
}
}
return s, nil
}
// 서버 시작 함수 - 메인/백그라운드 핸들러 관리
func (s *Server) Start() {
var mainHandler Handler
for _, h := range s.handlers {
if h.IsMainThread() {
mainHandler = h
} else {
go func(handler Handler) {
if err := handler.Start(); err != nil {
log.Fatalf("handler error: %v", err)
}
}(h)
}
}
if mainHandler == nil {
log.Fatalf("main handler not found.")
}
if err := mainHandler.Start(); err != nil {
log.Fatalf("main handler start error: %v", err)
}
}
HTTP 프로토콜을 통해 GraphQL API를 제공하는 핸들러입니다. 캐싱, 트랜잭션, 미들웨어 등 다양한 기능을 옵션으로 제공합니다.
// HTTP 핸들러 구조체
type HttpHandler struct {
entClient EntClient
config appconfig.Config
server *handler.Server
middleware MiddlewareProvider
}
// HTTP 핸들러 옵션 타입
type HttpHandlerOption func(*HttpHandler)
// 미들웨어 제공자 인터페이스
type MiddlewareProvider interface {
Middleware(next http.Handler) http.Handler
}
// HTTP 핸들러 생성자
func NewHttpHandler(entClient EntClient, config appconfig.Config, executableSchema graphql.ExecutableSchema, opts ...HttpHandlerOption) *HttpHandler {
h := &HttpHandler{
entClient: entClient,
config: config,
}
h.server = handler.New(executableSchema)
// 필수 설정은 생성자에서 책임짐
h.server.AddTransport(transport.Options{})
h.server.AddTransport(transport.GET{})
h.server.AddTransport(transport.POST{})
h.server.AddTransport(transport.MultipartForm{})
h.server.Use(extension.Introspection{})
h.server.Use(entgql.Transactioner{TxOpener: h.entClient}) // 트랜잭션 자동화
h.server.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error {
return apperror.WrapError(ctx, err)
})
// 옵션 적용
for _, opt := range opts {
opt(h)
}
return h
}
// 서버 시작 구현
func (h *HttpHandler) Start() error {
corsWrapper := cors.AllowAll().Handler
http.Handle("/graphql", corsWrapper(h.middleware.Middleware(h.server)))
http.Handle("/", playground.Handler("GraphQL playground", "/graphql"))
log.Printf("connect to http://localhost:%s/ for GraphQL playground", h.config.ServicePort)
return http.ListenAndServe(fmt.Sprintf(":%s", h.config.ServicePort), nil)
}
gRPC 프로토콜을 지원하는 핸들러입니다. 마이크로서비스 간 통신에 적합한 gRPC 서버를 구성합니다.
// gRPC 핸들러 구조체
type GrpcHandler struct {
entClient EntClient
config appconfig.Config
grpcServer *grpc.Server
}
// gRPC 핸들러 생성자
func NewGrpcHandler(entClient EntClient, config appconfig.Config, grpcServer *grpc.Server) *GrpcHandler {
return &GrpcHandler{
entClient: entClient,
config: config,
grpcServer: grpcServer,
}
}
// 서버 시작 구현
func (s *GrpcHandler) Start() error {
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", s.config.GrpcServerPort))
if err != nil {
return fmt.Errorf("failed to listen: %v", err)
}
log.Printf("starting gRPC server on port %s", s.config.GrpcServerPort)
return s.grpcServer.Serve(lis)
}
// Ent 클라이언트 인터페이스
type EntClient interface {
entgql.TxOpener // 트랜잭션 관리를 위한 인터페이스
Close() error // 리소스 정리를 위한 메서드
}
tmpl 파일은 다음이 필요합니다.
{{/* gotype: entgo.io/ent/entc/gen.Graph */}}
{{ define "client_constructor" }}
{{/* 파일 헤더 추가 */}}
{{ $pkg := base $.Config.Package }}
{{ template "header" $ }}
import (
"context"
"entgo.io/ent/dialect/sql"
"{{ $.Config.Package }}/migrate"
)
// CreateClient creates a new client using the given sql drivers
func CreateClient(master *sql.Driver, replica *sql.Driver) *Client {
return NewClient(
Driver(
&dbconnection.Drivers{
Master: master,
Replica: replica,
},
),
)
}
// NewEntClient creates a new ent client with the given configuration
func NewEntClient(ctx context.Context, cfg appconfig.Config) (*Client, error) {
masterConfig := &dbconnection.Config{
Username: cfg.DbUsername,
Password: cfg.DbPassword,
Hostname: cfg.DbMaster,
DBName: cfg.DbName,
Port: cfg.DbPort,
}
replicaConfig := &dbconnection.Config{
Username: cfg.DbUsername,
Password: cfg.DbPassword,
Hostname: cfg.DbReplica,
DBName: cfg.DbName,
Port: cfg.DbPort,
}
entClient := dbconnection.NewClients[Client](
*masterConfig,
*replicaConfig,
CreateClient,
)
if err := entClient.Schema.Create(
ctx,
migrate.WithDropIndex(false),
migrate.WithDropColumn(false),
migrate.WithGlobalUniqueID(true),
); err != nil {
return nil, err
}
return entClient, nil
}
{{ end }}
func main() {
ctx := context.Background()
// 설정 로드
cfg := appconfig.LoadEnv(".env")
// Ent 클라이언트 생성
entClient, err := ent.NewEntClient(ctx, cfg)
if err != nil {
log.Fatalf("failed to create ent client: %v", err)
}
// 서버 생성 - HTTP와 gRPC 핸들러 동시 지원
srv, err := appbuilder.New(
// HTTP/GraphQL 핸들러 설정
appbuilder.WithHttpHandler(
entClient,
cfg,
resolver.NewSchema(ctx, entClient, cfg),
appbuilder.WithMiddleware(appsecurity.NewJWTMiddleware(cfg)),
appbuilder.WithQueryCache(1000),
appbuilder.WithAutomaticPersistedQuery(100),
),
)
if err != nil {
log.Fatalf("failed to create server: %v", err)
}
// 서버 시작
srv.Start()
}
[우아한 종료(Graceful Shutdown) 부재] 서버가 강제로 종료될 경우 진행 중인 요청을 적절히 처리하는 메커니즘이 없습니다. 시그널 핸들링과 컨텍스트 기반 종료 로직이 필요합니다.
[메트릭 및 모니터링 부재] 서버 성능, 요청 처리량, 응답 시간 등을 측정하고 모니터링하는 기능이 없습니다. Prometheus 같은 도구와의 통합이 필요합니다.