Go 언어로 HTTP API 서버 만들기

byron1st·2022년 10월 24일
0

mux

HTTP 기반으로 통신하는 RESTful API 서버를 만들기 위해 필요한 것이 무엇이 있을까? 일단 HTTP 요청에 대해 적절한 API 함수를 호출할 수 있어야 할 것이다. 입력으로 들어온 HTTP 요청을 "적절한 경로로 보낸다"라고 하여, 보통 라우터 또는 멀티플렉서(multiplexer, 줄여서 mux)라고 부른다. 일단 우린 이게 필요하다.

  • mux (multiplexer) - 개인적으로 라우터 라는 말이 더 친숙하지만, Go 의 표준 라이브러리에서 mux 라는 용어를 쓰니, 우리도 mux라고 부르기로 한다.

Go 언어에는 다양한 mux 라이브러리가 존재한다. 또한 서버 프레임워크들은 기본적으로 mux 기능을 포함하고 있다. 그리고 표준 라이브러리에도 mux가 있다. 그렇다면 무엇을 써야할까?

Gin 이나 Echo, Fiber 같은 프레임워크를 쓰지 않는다고 가정할 때, 개인적으로 이 글(Which Go router should I use? )이 판단에 큰 도움이 되었다. 현재 알림을 처리하는 작은 마이크로서비스는 표준 라이브러리의 http.ServerMux를, 개인 프로젝트로 진행하는 API 서버는 go-chi를 쓰고 있다.

go-chi의 주요 기능은 다음과 같다:

  • 경로에 변수입력: /users/{userID} 와 같이 입력한다. 표준 라이브러리가 이게 안된다. 단순 변수 외에도 정규식 매칭도 가능하다.
  • 경로 임배딩과 경로 별로 서로 다른 미들웨어 사용 가능
  • 각종 미들웨어 제공: Logger, Recoverer, CORS 등 30여종의 미들웨어들을 제공한다. 필요한건 다 있는 편인데, 적당히 골라서 쓰면 좋다. 라이선스 자체도 MIT 의 오픈소스라 미들웨어 구현을 참조하여 직접 구현할 수도 있다.

logger

이제 두번째로 필요한 것이 무엇일까? 개인적으로 라우터가 준비되면 바로 로그를 설정한다. 로그가 적절히 기록되지 못하면, 개발 자체가 상당히 답답하다. 그렇다고 그냥 임시방편으로 로그를 처리하면, 나중에 고쳐야 할 곳이 너무 많아진다. 애초에 초반부터 로그 설정을 해두고 개발을 진행하는게 편리하다.

일단, 난 JSON으로 구조화된 로그를 사용한다. Go 언어에는 기본 log 패키지가 존재하고, Go 언어 log 패키지의 간결함을 상당히 좋아하지만, 애석하게도 구조화된 로깅을 지원하지는 않는다. 만약 굳이 구조화된 로깅이 필요없거나, 좋아하지 않는다면, 다른 로깅 라이브러리를 찾지 말고 표준 라이브러리의 log 패키지를 사용해보길 권한다.

하지만 난 구조화된 로깅이 필요하다. 난 큰 고민 없이 zerolog를 사용한다. zerolog는 속도와 개발자 경험에 중점을 둔 라이브러리로 Uber 의 zap으로부터 영감을 받아 개발된 로깅 라이브러리이다. 양쪽이 서로 자기가 더 빠르다고 주장하긴 하지만, 솔직히 속도는 둘다 그냥 blazing fast 라고 할만하니, 어느쪽이 더 쓰기 편하냐의 문제일 것이다. 개인적으로 zerolog 쪽의 API가 월등히 편하고 가독성이 좋았기에, 쭉 사용 중이다.

request parameters

요청은 보통 무언가 파라미터들을 가지고 들어오기 마련이다. POST 요청이라면 application/x-www-form-urlencoded, multipart/form-data, application/json 등의 형식으로, GET 이라면 Url 뒤에 Query 형태로 추가적인 파라미터를 함께 보낼 수 있다.

API도 함수인 만큼, 가장 중요한 것 중 하나가 "제대로 된" 파라미터가 입력값으로 주어지는 것이다. 그렇기 때문에 DB를 들락날락하는 일을 하기 전에 요청의 파라미터들이 제대로 된 값인지 확인해야 한다.

Spring 프레임워크나 Nest.js 같은 서버 프레임워크들은 어노테이션 등을 통해 필드의 타입, Required 여부 등을 정의할 수 있도록 해준다. Go 언어에도 Tag 를 통해 구조체의 필드값들에 대한 검증을 수행할 수 있도록 해주는 라이브러리가 있다. go-playground/validator 라이브러리가 그것이다. 근데, 이 라이브러리를 쓰기 위해서는 일단 "구조체"여야 한다. 그렇다면, Form 형식의 데이터, JSON 형식의 데이터, 또는 URL Query 형식의 데이터가 구조체로 어떻게 변환될 수 있을까?

Go 언어의 표준 라이브러리의 http.RequestURL.Query 를 통해 URL Query 값들을, ParseForm() 함수를 통해 Form 형식의 데이터 값들을 url.Values 타입으로 정의한다. 이 타입은 map[string][]string 의 aliasing 타입이다. 그리고 이 url.Values 타입에 저장된 데이터는 go-playground/form 라이브러리를 통해 구조체로 파싱될 수 있다.

application/json 형식의 데이터는 Go 언어 표준 라이브러리의 http.Request 객체의 Body 필드값에 io.ReadCloser 형태로 저장된다. 이 값은 Go 언어 표준 라이브러리의 json.Decoder 객체의 Decode 함수를 통해 구조체로 변환될 수 있다.

이렇게 구조체로 일단 변환되면, validator 라이브러리를 이용하여 올바른 필드가 요청 파라미터로 들어왔는지 검증할 수 있다.

Config

이쯤 되면, 필수적인건 다 있는 것 같지만, 하나가 빠졌다. 바로 서버 설정 정보를 저장/이용하는 방법이다. Go 언어에서 설정 관련해서 가장 유명하고 널리 쓰이는 라이브러리는 viper 이다. viper 라이브러리는 .env, JSON, YAML 등 거의 대부분의 설정 파일 형식을 지원함은 물론, OS의 환경변수도 지원한다. 커맨드라인을 통해 입력된 파라미터들도 지원해서 CLI 앱을 만들 때도 널리 사용된다.

고민할 필요 없이 viper 를 쓰면 되겠다.

Testing

Go 언어 표준 라이브러리에서 REST API 함수는 func(w http.ResponseWriter, r *http.Request) 타입을 갖는다. 그리고 이 함수를 서버 실행 없이 별도로 단위 테스트 할 수 있도록 라이브러리를 제공하는데, httptest 패키지가 그것이다.

httptest 패키지는 NewRequest 함수를 통해 http.Request 객체를 생성하고, http.Request.Header.Set 함수를 통해 헤더도 설정할 수 있다. 그리고 httptest 패키지의 NewRecorder 함수를 통해 http.ResponseWriter 타입의 객체를 생성할 수 있다. 그리고 이 두 값을 API 함수의 파리미터로 넘김으로써, 별도의 서버 실행 없이 이 API를 실행할 수 있다.

실행 결과 반환값에 대한 Assertion 은 단순히 if-else 구문을 통해서도 진행할 수 있지만, 별도의 Assertion 라이브러리를 사용한다면, 코드의 가독성도 높이고, 다양한 기능을 활용할 수 있다. node.js 진영에 chai가 있다면(너무 올드한가), Go 언어에는 testify/assert 라이브러리가 있다. assert.Equal, assert.NotNil 등의 함수를 활용해서 다양한 Assertion 이 가능하니 잘 활용하면 좋다.

정리

정리하자면, 다음과 같다.

  • mux: go-chi 또는 표준 라이브러리의 http.ServerMux
  • logger: zerolog
  • request parameters: go-playground/validator, go-playground/form
  • config: viper
  • testing: 표준 라이브러리의 httptest 와 testify/assert 라이브러리

Gin, Echo, Fiber 같은 서버 프레임워크를 쓰는 것은 손쉬운 해결책이다. 실제로 boilerplate 코드 양을 줄여주고, 가독성을 높여 좋은 코드를 만들기도 한다. 하지만 항상 소잡는 칼이 필요한건 아니다. 작은 마이크로서비스 서버를 짜거나, API 서버를 짜더라도 굳이 거대한 서버 프레임워크가 필요하지 않을 수 있다. 이럴 경우, 위 라이브러리들을 활용해서 서버를 한번 짜보는 것도 좋다.

profile
Fullstack software engineer specialized for Blockchain

0개의 댓글