# Golang HTTP Package **Published by:** [Primrose](https://paragraph.com/@primrose/) **Published on:** 2023-08-11 **URL:** https://paragraph.com/@primrose/golang-http-package ## Content 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)) } ## Publication Information - [Primrose](https://paragraph.com/@primrose/): Publication homepage - [All Posts](https://paragraph.com/@primrose/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@primrose): Subscribe to updates