# Golang HTTP Package

By [Primrose](https://paragraph.com/@primrose) · 2023-08-11

---

HTTP Client Package
===================

최근 회사에서 NestJS 를 이용해서 외부 API를 연동해야 하는 일들이 많았다.

Go 기반 서버에서도 외부 API를 이용하는 로직이 있어서, HTTP 요청을 보내는 것 자체를 패키지화해서 관리를 하고 있는데, 최근 개선된 부분에 대해서 기록하고자 한다.

먼저 기존의 코드를 보자.

    type WebClient interface {
        WebClientMetadata
        WebClientFactory
        WebClientRequest
    }
    
    type WebClientMetadata interface {
        URI(uri string) WebClient
        QueryParams(values map[string]string) WebClient
        Headers(values map[string]string) WebClient
        Body(values map[string]string) WebClient
        Resp(resp *http.Response, err error) ([]byte, error)
    }
    
    type WebClientRequest interface {
        Get() ([]byte, error)
        Post() ([]byte, error)
        Put() ([]byte, error)
        Patch() ([]byte, error)
        Delete() ([]byte, error)
    }
    
    type WebClientFactory interface {
        Create() WebClient
    }
    

크게 네 가지의 인터페이스로 구성되어 있다.

`WebClientFactory` 인터페이스는 최초에 WebClient 구조체를 생성한다.

`WebClientRequest`를 통해서 각 HTTP Method에 따라서 Request를 Send하는 로직이 담긴다.

요청에 필요한 Body, Header 등은 `WebClientMetadata` 라는 인터페이스를 통해서 value를 구조체에 set 하고 자기 자신을 반환하는 방식으로 진행된다.

간단한 사용 예제는 다음과 같다.

    client := http.Client{}
    
    responseBody, err := client.Create().URI("https://www.naver.com").Get()
    if err != nil {
    // DO SOMETHING...
    }
    
    log.Println(string(responseBody))
    

일단 NestJS에 있던 패키지를 그대로 따온 것이라서, 뭔가 go 스럽지 않다는 느낌도 든다.

추가로 내부 구현 코드를 보면,

    func (c Client) Get() ([]byte, error) {
        if c.queryParams != nil {
            c.uri += "?"
            for k, v := range c.queryParams {
                c.uri += fmt.Sprintf("%s=%s", k, v)
            }
        }
    
        request, err := http.NewRequest(http.MethodGet, c.uri, nil)
        if err != nil {
            return nil, err
        }
    
        if c.headers != nil {
            for k, v := range c.headers {
                request.Header.Add(k, v)
            }
        }
    
        return c.Resp(c.sender.Do(request))
    }
    
    func (c Client) Post() ([]byte, error) {
        var body []byte
        var err error
    
        if c.body != nil {
            body, err = json.Marshal(c.body)
            if err != nil {
                return nil, errors.Join(constants.MarshalError, err)
            }
        }
    
        request, err := http.NewRequest(http.MethodGet, c.uri, bytes.NewBuffer(body))
        if err != nil {
            return nil, err
        }
    
        if c.headers != nil {
            for k, v := range c.headers {
                request.Header.Add(k, v)
            }
        }
    
        return c.Resp(c.sender.Do(request))
    }
    
    func (c Client) Put() ([]byte, error) {
        var body []byte
        var err error
    
        if c.body != nil {
            body, err = json.Marshal(c.body)
            if err != nil {
                return nil, errors.Join(constants.MarshalError, err)
            }
        }
    
        request, err := http.NewRequest(http.MethodPut, c.uri, bytes.NewBuffer(body))
        if err != nil {
            return nil, err
        }
    
        if c.headers != nil {
            for k, v := range c.headers {
                request.Header.Add(k, v)
            }
        }
    
        return c.Resp(c.sender.Do(request))
    }
    
    func (c Client) Patch() ([]byte, error) {
        var body []byte
        var err error
    
        if c.body != nil {
            body, err = json.Marshal(c.body)
            if err != nil {
                return nil, errors.Join(constants.MarshalError, err)
            }
        }
    
        request, err := http.NewRequest(http.MethodPatch, c.uri, bytes.NewBuffer(body))
        if err != nil {
            return nil, err
        }
    
        if c.headers != nil {
            for k, v := range c.headers {
                request.Header.Add(k, v)
            }
        }
    
        return c.Resp(c.sender.Do(request))
    }
    
    func (c Client) Delete() ([]byte, error) {
        var body []byte
        var err error
    
        if c.queryParams != nil {
            c.uri += "?"
            for k, v := range c.queryParams {
                c.uri += fmt.Sprintf("%s=%s", k, v)
            }
        }
    
        request, err := http.NewRequest(http.MethodDelete, c.uri, bytes.NewBuffer(body))
        if err != nil {
            return nil, err
        }
    
        if c.headers != nil {
            for k, v := range c.headers {
                request.Header.Add(k, v)
            }
        }
    
        return c.Resp(c.sender.Do(request))
    }
    
    func (c Client) Resp(resp *http.Response, err error) ([]byte, error) {
        if err != nil {
            return nil, err
        }
    
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            return nil, err
        }
    
        defer func(Body io.ReadCloser) {
            _ = Body.Close()
        }(resp.Body)
    
        return body, nil
    }
    

뭔가 각 메소드가 비슷비슷하고 중복 코드가 꽤 많이 있다.

리팩토링을 진행한 코드는 다음과 같다.

    package main
    
    type WebClient interface {
        WebClientMetadata
        WebClientRequest
    }
    
    type WebClientMetadata interface {
        URI(uri string) WebClient
        Body(values map[string]string) WebClient
        Resp(resp *http.Response, err error) ([]byte, error)
        Headers(values map[string]string) WebClient
        ContentType(contentType string) WebClient
        QueryParams(values map[string]string) WebClient
    }
    
    type WebClientRequest interface {
        Get() WebClient
        Post() WebClient
        Put() WebClient
        Patch() WebClient
        Delete() WebClient
        Retrieve() ([]byte, error)
    }
    

우선 Factory interface를 삭제하고, 실제 Request를 수행하는 코드를 `Retrieve()`라는 메소드를 이용해서 통일했다.

사용 코드를 보자.

    package main
    
    func main() {
        client := http.NewWebClient()
    
        responseBody, err := client.URI("https://www.google.com").Get().Retrieve()
        if err != nil {
            // DO SOMETHING...
        }
    
        log.Println(string(responseBody))
    }
    

내 눈에는 조금 나아진 것 같았다. 그래도 조금 거슬리는 부분이 있다면, URI를 따로 메소드를 구현해서 굳이 저렇게 호출해야할까? 하는 부분이었다.

    package main
    
    func (c Client) Get() WebClient {
        request, _ := http.NewRequest(http.MethodGet, c.uri, nil)
        c.request = request
        return c
    }
    
    func (c Client) Post() WebClient {
        request, _ := http.NewRequest(http.MethodPost, c.uri, nil)
        c.request = request
        return c
    }
    
    func (c Client) Put() WebClient {
        request, _ := http.NewRequest(http.MethodPut, c.uri, nil)
        c.request = request
        return c
    }
    
    func (c Client) Patch() WebClient {
        request, _ := http.NewRequest(http.MethodPatch, c.uri, nil)
        c.request = request
        return c
    }
    
    func (c Client) Delete() WebClient {
        request, _ := http.NewRequest(http.MethodDelete, c.uri, nil)
        c.request = request
        return c
    }
    

위와 같이 바뀌어서, Method에 따라서 Request를 새로이 교체해주는 방식을 사용했다.

그러다보니 uri를 그때그때 주입해줘도 상관 없을 것 같다는 생각이 들었다.

최종적으로는 다음과 같다.

    package main
    
    func main() {
        client := http.NewWebClient()
    
        responseBody, err := client.Get("https://www.naver.com").Retrieve()
        if err != nil {
            // DO SOMETHING...
        }
    
        log.Println(string(responseBody))
    }

---

*Originally published on [Primrose](https://paragraph.com/@primrose/golang-http-package)*
