함수형 옵션 패턴으로 MSA Go 서버 라이브러리 만들기

dasd412·2025년 5월 5일
0

실무 문제 해결

목록 보기
17/17

요구 사항

마이크로서비스 아키텍처를 위한 표준화된 서버 구조를 설계하는 과제를 맡았습니다.
주요 요구사항은 다음과 같았습니다.

  1. 모든 마이크로서비스가 일관된 방식으로 구성될 수 있는 표준 프레임워크 제공
  2. HTTP(GraphQL), gRPC 등 다양한 프로토콜을 유연하게 지원
  3. 개발자 생산성 향상을 위한 보일러플레이트 코드 최소화
  4. 기능을 필요에 따라 선택적으로 활성화할 수 있는 유연성 제공

코드

server.go

서버의 핵심 구조와 핸들러 관리 로직을 담당하는 파일입니다. 핸들러 인터페이스와 서버 구성을 위한 옵션 패턴을 정의했습니다.

// 모든 핸들러(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_handler.go ( GraphQL 서버 구현)

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_handler.go (gRPC 서버 구현)

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_client.go (데이터베이스 클라이언트 인터페이스)

// 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()
}

한계

  1. [우아한 종료(Graceful Shutdown) 부재] 서버가 강제로 종료될 경우 진행 중인 요청을 적절히 처리하는 메커니즘이 없습니다. 시그널 핸들링과 컨텍스트 기반 종료 로직이 필요합니다.

  2. [메트릭 및 모니터링 부재] 서버 성능, 요청 처리량, 응답 시간 등을 측정하고 모니터링하는 기능이 없습니다. Prometheus 같은 도구와의 통합이 필요합니다.


profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글