[Go] go-github SDK 패턴을 적용한 리팩토링 - 2. 실전 적용

3 분 소요


회사에서 Internal API Client를 리팩토링하면서 go-github SDK의 구조적 패턴을 적용해 보았다. 해당 패턴이 무엇인지, 그리고 go-github에서 어떻게 구현되어 있는지 알아 보고, 어떻게 적용했는지 정리한다. go-github SDK 패턴 이해하기에 이어, 실제 프로젝트에 패턴을 적용한 경험을 다룬다.


실제 적용

내 상황에 어떻게 정리했는지 살펴 보자.


현재 상황 분석

공유 리소스가 있고, 도메인이 나뉘고, 메서드가 많아질 예정이라 이 패턴을 적용하기에 적합하다고 판단했다.

조건 현재 상황 패턴 적용 이유
두 개의 다른 서버 Argo Server + Workflow Server (FastAPI) 서비스 분리 필요
공유 리소스 httpClient, minioConfig, trainerConfig 한 곳에서 관리
도메인이 다름 Argo = 워크플로우 제어, Pipeline = 트리거 메서드 그룹화
기존 코드의 문제 한 Client에 10개+ 메서드가 뒤섞임 정리 필요


구현

Client 정의

type Client struct {
    // 설정
    config        Config
    trainerConfig TrainerConfig
    splitConfig   SplitConfig

    // HTTP 클라이언트
    httpClient Doer

    // 서비스들
    Argo     ArgoService
    Pipeline PipelineService
}

func New(cfg Config, opts ...ClientOption) (*Client, error) {
    if err := cfg.Validate(); err != nil {
        return nil, err
    }

    c := &Client{
        config:     cfg,
        httpClient: &http.Client{},
        splitConfig: SplitConfig{
            TrainRatio: 0.8,
            ValRatio:   0.2,
        },
    }

    for _, opt := range opts {
        opt(c)
    }

    // 서비스 초기화
    c.Argo = newArgoService(c)
    c.Pipeline = newPipelineService(c)

    return c, nil
}


내부 서비스: ArgoService

Argo Workflow 엔진 서버와 직접적으로 통신하는 서비스이다.

type argoService struct {
    httpClient Doer
    serverURL  string
    namespace  string
}

func newArgoService(c *Client) *argoService {
    return &argoService{
        httpClient: c.httpClient,
        serverURL:  c.config.ArgoServerURL,
        namespace:  c.config.ArgoServerNamespace,
    }
}


내부 서비스: PipelineService

Argo Workflow CRD를 관리하고, Pipeline을 실제로 트리거링하는 Pipeline Server와 통신하는 서비스이다.

type pipelineService struct {
    httpClient    Doer
    serverURL     string
    argoServerURL string
    minio         ObjectStorageConfig
    trainer       TrainerConfig
    split         SplitConfig
}

func newPipelineService(c *Client) *pipelineService {
    return &pipelineService{
        httpClient:    c.httpClient,
        serverURL:     c.config.WorkflowServerURL,
        argoServerURL: c.config.ArgoServerURL,
        minio:         c.config.Minio,
        trainer:       c.trainerConfig,
        split:         c.splitConfig,
    }
}


go-github 방식과의 비교

Client 참조 방식

현재 코드는 서비스가 2개뿐이고 로직이 단순해서, 서비스 초기화 시 Client에서 필요한 값만 복사하는 방식을 선택했다. 나중에 서비스가 늘어나면 go-github 방식으로 전환할 수도 있다.


현재 방식: 필요한 값만 복사

func newArgoService(c *Client) *argoService {
    return &argoService{
        httpClient: c.httpClient,      // 필요한 값만 복사
        serverURL:  c.config.ArgoServerURL,
        namespace:  c.config.ArgoServerNamespace,
    }
}


go-github 방식: Client 참조

서비스가 Client 전체를 참조하고, 필요할 때 Client의 필드에 접근한다. 설정이 런타임에 변경되어도 서비스에 반영된다.

func newArgoService(c *Client) *argoService {
    return &argoService{
        client: c,  // Client 전체 참조
    }
}

func (s *argoService) GetStatus(...) {
    s.client.httpClient.Do(...)  // 필요할 때 접근
}


추가 개선: NewRequest/Do 패턴 적용

현재는 각 서비스에서 HTTP 요청을 직접 생성하고 실행하지만, 서비스가 늘어나면 go-github처럼 NewRequest/Do 패턴을 적용해 공통 로직을 Client에 집중시킬 수 있다.

func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
    // URL 결정 로직: 경로에 따라 Base URL 선택
    var baseURL string
    if strings.HasPrefix(path, "/api/v1/workflows") {
        baseURL = c.config.ArgoServerURL
    } else {
        baseURL = c.config.WorkflowServerURL
    }

    u, err := url.Parse(baseURL + path)
    if err != nil {
        return nil, err
    }

    var buf io.ReadWriter
    if body != nil {
        buf = &bytes.Buffer{}
        if err := json.NewEncoder(buf).Encode(body); err != nil {
            return nil, err
        }
    }

    req, err := http.NewRequest(method, u.String(), buf)
    if err != nil {
        return nil, err
    }

    if body != nil {
        req.Header.Set("Content-Type", "application/json")
    }

    return req, nil
}

func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error {
    req = req.WithContext(ctx)

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("API error: %d", resp.StatusCode)
    }

    if v != nil {
        return json.NewDecoder(resp.Body).Decode(v)
    }

    return nil
}


서비스에서는 Client의 공통 메서드를 호출하여 요청을 처리하면 될 것이다.

func (s *argoService) GetWorkflow(ctx context.Context, name string) (*Workflow, error) {
    req, err := s.client.NewRequest("GET",
        fmt.Sprintf("/api/v1/workflows/%s/%s", s.client.config.ArgoServerNamespace, name),
        nil)
    if err != nil {
        return nil, err
    }

    wf := new(Workflow)
    err = s.client.Do(ctx, req, wf)
    return wf, err
}


정리

SDK-style Client 패턴의 특징을 정리하면 다음과 같다.

특징 설명
공유 리소스 HTTP 클라이언트, 인증, base URL 등을 한 곳에서 관리
일관된 설정 모든 서비스가 동일한 설정 사용
네임스페이스 client.Users.Get(), client.Repos.List() 처럼 깔끔한 API
Lazy init 가능 필요할 때만 서비스 초기화 가능
공통 로직 재사용 NewRequest/Do 패턴으로 중복 코드 제거


외부에 배포하는 SDK가 아니더라도, 내부 클라이언트에 이 패턴을 적용하면 코드 구조가 깔끔해지고 확장성도 좋아진다. 도메인별로 메서드가 분리되어 있어 유지보수도 쉬워진다.




hit count

댓글남기기