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