Go 로 만드는 웹

솔다·2023년 1월 27일
0

web 서버를 만들기에 앞서서 간단하게 어떻게 통신이 일어나는지 정리하면,

client가 웹을 방문하고자 하면 통신을 하게 된다.

HTTP(Hyper Text Transfer Protocol)의 약자이다.

우리가 만일 never 페이지를 방문하려고 하면, DNS를 통해서 IP를 받고, 해당 IP를 향해서 Request를 날리게 된다. Request를 어떻게 날릴 것인지에 대한 Protocol이 다 정해져 있다.

Request에 따라서 문서를 보내면 이를 렌더링을 통해서 페이지를 띄워준다. 에전에는 단순하게 한번만 통신만 했지만, 이제는 여러번 반응형으로 페이지가 여러번 Request를 보내기도 하면서 요구사항이 많아지고 있다.

서버에서 Render를 할 수도 있으며, Client Render 두 가지 전부 가능한데, Client Render가 일어나기 위해서는 여러 데이터를 요청한다.

이를 Backend에서 데이터 Request에 맞춰서 데이터를 보내주는데 Go로 구성해주면 매우 빠르게 응답을 해줄 수 있다.

Go로 웹 서버 구현 실습

package main

import (
	"fmt"
	"net/http"
)

type fooHandler struct{}

func (f *fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello Foo")
}
func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello World")
	})

	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello bar!")
	})

	http.Handle("/foo", &fooHandler{})

	http.ListenAndServe(":1234", nil) //서버를 실행하는 구문. 자동으로 listen 상태로 실행되고 있는 상태이다.

}

http package를 이용해서 Handlefunc을 사용하면, 쉽게 기초적인 서버는 구동할 수 있는데, package의 ListenAndServe()함수를 사용하면 서버를 구동시킬 수 있다.

handler를 보면 각각 인자로 경로를 받아서, 라우터로 분배가 된다. 현재는 http에 정적으로 등록을 했다. 이를 새로 라우터를 생성하여서 mux로 새로 분배를 하고, ListenAndServe()함수의 두번째 인자로 mux를 등록해준다.

다 똑같은데, mux라는 인스턴스를 하나 만들어서 거기서 넘겨주는 방식으로 바꿀 수 있다.

package main

import (
	"fmt"
	"net/http"
)

type fooHandler struct{}

func (f *fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello Foo")
}
func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello World")
	})

	mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello bar!")
	})

	mux.Handle("/foo", &fooHandler{})

	http.ListenAndServe(":1234", mux) //서버를 실행하는 구문. 자동으로 listen 상태로 실행되고 있는 상태이다.

}

Request 받는 방법

Client가 web browser에서 서버에 request를 보낼때, input값을 넣을 수 있다. url에 직접적으로 넣어도 괜찮고, body에 input을 넣을 수도 있다. 이렇게 주고받은 데이터를 쓰는 규격이 같아야 한다. 많이 쓴게 XML 방식을 썼었다. data의 size가 커지는 문제점이 있기 때문에, 요새는 Json을 많이 사용한다.

Json(Java Script Object Notaion)의 약자로 자바스크립트에서 object를 나타내는 방식이다. 기본적으로 {Key: value} 방식으로 간단하게 되어있고, 사이즈가 작아서 일번적으로 사용함.

서버로 데이터를 받을 때도 Json으로 주고 받을때도 Json으로 주고, 받고 하는 서버를 아래에 작성해보자.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

type User struct {
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	Email     string    `json:"email"`
	CreateAt  time.Time `json:"created_at"` //어느 시간에 생성되었는지 알려주는 필드를 추가하였음
}

type fooHandler struct{}

func (f *fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user := new(User)
	err := json.NewDecoder(r.Body).Decode(user)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, "Bad Request: ", err)
		return
	}
	user.CreateAt = time.Now()

	data, _ := json.Marshal(user)
	w.Header().Add("content-type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(data))
}

func barHandler(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	if name == "" {
		name = "World"
	}
	fmt.Fprintf(w, "Hello %s!", name)
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello World")
	})

	mux.HandleFunc("/bar", barHandler)

	mux.Handle("/foo", &fooHandler{})

	http.ListenAndServe(":1234", mux) //서버를 실행하는 구문. 자동으로 listen 상태로 실행되고 있는 상태이다.

}

json에서 쓰이는 방식을 annotaion을 해주면(설명을 써주면), decode()하고, marshaling 할때 자동으로 이에 맞춰서 형식을 만들어준다. 그리고 웹에 데이터를 다시 json으로 보내줄 때, 웹이 봤을때, 이게 어떤 형식인지 알려주어야 한다. 이에 대한 정보는 header에 정보를 담고있다.

Response Header의 content-type을 "application/json"이라고 넣어주면 웹이 알아서 이를 json으로 인식한다.

웹 Test 환경 구축하기

웹을 구축하면서 테스트를 같이 해줄 수 있는 패키지를 배워서 실행해보고자 한다.

go에서는 파일명에 _test 를 뒤에 붙여주면 자연스럽게 테스트 파일로 연결된다.

쉽게 사용할 수 있는 패키지로 smartystreets를 다운받아 실행했다. 패키지 다운을 위한 명령어는 아래와 같다.

go get github.com/smartystreets/goconvey

이 명령어를 통해 받고, 웹 서버를 테스트하려고 하는 src 코드가 있는 디렉토리에서 goconvey 를 실행하면 localhost:8080 로 서버가 실행된다. 이 서버가 test파일이나 src 코드의 변경을 감지하면 자동으로 test를 실행해준다.

package myapp

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestIndexPathHandler(t *testing.T) {
	//실제 network를 사용하지 않고, response와 request를 만드는 함수
	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/", nil)

	indexHandler(res, req)

	if res.Code != http.StatusOK {
		t.Fatal("Failed!! ", res.Code)
	}
}

위 코드는 app_test.py 파일의 코드를 그대로 가져온 것이다. func의 이름앞에 Test를 붙여주면 테스트 코드로 인식하고 TestIndexPathHandler(t *testing.T) 수정사항이 발생하고 저장되면, 그때마다 이 함수가 테스트로 실행된다.

우리가 사전에 만들어줬던 indexHandler() 함수를 호출한 후에 res로 받은 응답결과를 if문으로 검사하는 과정이다.

또한 앞으로는 assert()패키지를 받아서 위 if문으로 하던 검사를 대체해서 실행할 것이다. assert객체 새로 생성하고 패키지의 Equal함수에 인자를 넣어서 쉽게 할 수 있다.

실제 reponse로 받는 결과 값은 res.body에 들어있는데 이는 buffer 형태라서 ioutil 패키지를 활용해서 값을 읽어와야 한다.

func TestBarPathHandler_WithoutName(t *testing.T) {
	assert := assert.New(t)

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "bar", nil)

	barHandler(res, req)

	assert.Equal(http.StatusOK, res.Code)
	data, _ := ioutil.ReadAll(res.Body)
	assert.Equal("Hello World!", string(data))
}

위 테스트에서, 5번째 줄, req를 처음에 변수 선언해줄때, "bar?=sol"로 넣어도 테스트가 통과하는 기이한 현상이 있다. 그 이유는, 해당 test가 직접 barHandler()를 호출하고 있기 때문이다. 우리가 mux를 설정해주었기 때문에, 이를 활용하는 방법으로 작성하면 정상적으로 테스트가 작동하게 할 수 있다.

func TestBarPathHandler_WithName(t *testing.T) {
	assert := assert.New(t)

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/bar?name=sol", nil)

	mux := NewHttpHandler()
	mux.ServeHTTP(res, req)

	assert.Equal(http.StatusOK, res.Code)
	data, _ := ioutil.ReadAll(res.Body)
	assert.Equal("Hello sol!", string(data))
}

위는 mux를 이용해서 올바른 테스트가 되도록 한 경우. 밑의 assert.Equal()을 보면, Hello,sol 로 해서 테스트 결과를 바꿔줘야 함을 알 수 있다.

마지막으로 json을 주고받는데 잘 되었는지 확인하는 테스트이다.

func TestFooHandler_WithJson(t *testing.T) {
	assert := assert.New(t)

	res := httptest.NewRecorder()
	req := httptest.NewRequest("POST", "/foo",
		strings.NewReader(`{"first_name":"tucker", "last_name":"kim", "emai":"tucker@gmail.com"}`))

	mux := NewHttpHandler()
	mux.ServeHTTP(res, req)

	assert.Equal(http.StatusCreated, res.Code)

	user := new(User)
	err := json.NewDecoder(res.Body).Decode(user)

	assert.Nil(err)
	assert.Equal("tucker", user.FirstName)
	assert.Equal("kim", user.LastName)
}

mux에 의해서 response 가 작성되었고, 작성된 response의 body의 내용이 우리가 원하는 요청했던 내용이 제대로 들어갔는지 assert를 이용해서 확인하는 test이다.

web 에서 Json방식으로 데이터를 주고받는데 알아야 하는 것과, 이를 테스하는 것까지가 이번 포스팅내용이었다.

0개의 댓글