Golang HTTP Package

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

Last updated