Primrose Docs
  • Home
    • ⛓️Blockchain
      • Avalanche
        • What is AVAX?
      • Ethereum
        • Ethereum Cancun Upgrade Explained(draft)
        • go-ethereum: gas estimate
        • Blockchain Transaction Lifecycle
        • Mempool
        • Gas optimization in Solidity, Ethereum
      • Solidity DeepDive
        • Meta transaction
        • solidity: patterns
        • UUPS vs Transparent
        • Solidity Interface
        • Smart contract storage
        • ERC-2981 Contract
        • Solidity modifier
        • Solidity delete keyword
        • How To Make NFTs with On-Chain Metadata - Hardhat and JavaScript
        • How to Build "Buy Me a Coffee" DeFi dapp
        • How to Develop an NFT Smart Contract (ERC 721) with Alchemy
        • Upgradeable Contract
        • Smart Contract Verification
      • Common
        • Eigenlayer
        • MultiSig(draft)
        • Chain-Based Proof-of- Stake, BFT-Style Proof-of-Stake
        • Byzantine Fault Tolerance
        • Zero-knowledge
        • Hierarchical Deterministic Wallet
        • Maker DAO
        • Defi
        • Uniswap
        • IBC
        • Cosmos
        • Gossip Protocol
        • Tendermint
        • UTXO vs Account
        • Blockchain Layer
        • Consensus Algorithm
        • How does mining work?
        • Immutable Ledger
        • SHA256 Hash
        • Filecoin
        • IPFS - InterPlanetary File System
        • IPFS와 파일코인
        • Livepeer
        • Layer 0
      • Bitcoin
        • BIP for HD Wallet
        • P2WPKH
        • Segwit vs Native Segwit
    • 📖Languages
      • Javascript/Typescript
        • Hoisting
        • This value in Javascript
        • Execution Context
        • About Javscript
        • tsconfig.json
        • Nest js Provider
        • 'return await promise' vs 'return promise'
      • Python
        • Pythonic
        • Python: Iterable, Iterator
        • Uvicorn & Gunicorn
        • WSGI, ASGI
        • Python docstring
        • Decorator in Python
        • Namespace in Python
        • Python Method
      • Go
        • GORM+MySQL Connection Pool
        • Context in golang
        • How to sign Ethereum EIP-1559 transactions using AWS KMS
        • Mongo DB in golang(draft)
        • Golang HTTP Package
        • Panic
        • Golang new/make
        • golang container package
        • errgroup in golang
        • Generic Programming in Golang
        • Goroutine(draft)
    • 📝Database
      • MongoDB in golang
      • Nested loop join, Hash join
      • DB Query plan
      • Index
      • Optimistic Lock Pessimistic Lock
    • 💻Computer Science
      • N+1 query in go
      • Web server 를 구성할 때 Thread, Process 개수를 어떻게 정할 것인가?
      • CAP
      • Socket programming
      • DNS, IP
      • URL, URI
      • TLS과 SSL
      • Caching(draft)
      • Building Microservices: Micro Service 5 Deploy Principle
      • Red Black Tree
      • AOP
      • Distributed Lock
      • VPC
      • Docker
      • All about Session and JWT
      • Closure
      • Singleton Pattern
      • TCP 3 way handshake & 4 way handshake
      • Race Condition
      • Process Address Space 
      • Call by value, Call by reference, Call by assignment
      • Zookeeper, ETCD
      • URL Shortening
      • Raft consensus
      • Sharding, Partitioning
    • 📒ETC
      • K8S SIGTERM
      • SQS
      • Git Branch Strategy: Ship / Show / Ask
      • Kafka
      • Redis Data Types
      • CI/CD
      • How does Google design APIs?
      • Minishell (42 cursus)
      • Coroutine & Subroutine
      • Redis
Powered by GitBook
On this page
  1. Home
  2. Languages
  3. Go

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))
}
PreviousMongo DB in golang(draft)NextPanic

Last updated 1 year ago

📖