Golang으로 웹서버 만들기(4)

김영한·2020년 11월 16일
0

코드 Github
참고, 참고


📢 RESTful API 구현해보기(GET과 POST)

WEB5 폴더에 작성했다.

지금까지 배운것을 이용해서 RESTful API를 구현해보자!
myapp 폴더를 생성하고 간단한 handler와 test코드를 작성한다.

app.go

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

func usersHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Get UserInfo by /users/{id}")
}

// NewHandler make a new myapp handler
func NewHandler() http.Handler {
	mux := http.NewServeMux()
    
	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler)
	return mux
}

app_test.go

func TestIndex(t *testing.T) {
	assert := assert.New(t)
	ts := httptest.NewServer(NewHandler()) // http서버를 모의한 테스트 서버가 나온다.
	defer ts.Close()

	resp, err := http.Get(ts.URL)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, _ := ioutil.ReadAll(resp.Body)
	assert.Equal("Hello World", string(data))
}

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

	ts := httptest.NewServer(NewHandler())
	defer ts.Close()

	resp, err := http.Get(ts.URL + "/users")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, _ := ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "Get UserInfo") // 어떤 문자열이 포함되어 있으면 OK
}

저번 포스트에 해본 것들이라 쉽게 작성할 수 있을 것이다.


🔔 REST의 개념

  • REST를 간단하게 알아보자!
    • Representational State Transfer의 약자
    • 자원에 대한 CRUD를 하는데 그것을 URI에다 표시하자하는 뜻
      • 구체적인 의미 : HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미한다.
    • CRUD Operation
      • Create : 생성(POST)
      • Read : 조회(GET)
      • Update : 수정(PUT)
      • Delete : 삭제(DELETE)
      • HEAD : header 정보 조회(HEAD)

이제 user ID를 Get 할 수 있게 test코드를 수정해보자(완전한 건 아니고 그냥 ID만 출력할 정도로)

User Id에 맞게 파싱하는 작업이 필요한데 우리는 Gorilla_mux 패키지를 이용할 것이다.
go get -u github.com/gorilla/mux를 터미널에서 설치

http.NewServeMux()대신에 Gorilla 패키지의 mux.NewRouter()를 사용할 것이다.

app.go 수정

...
func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r) // 알아서 ID를 파싱을 해줌
	fmt.Fprint(w, "User Id:", vars["id"])
}

func NewHandler() http.Handler {
	mux := mux.NewRouter()

	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler)
	mux.HandleFunc("/users/{id:[0-9]+}", getUserInfoHandler)
	return mux
}

app_test.go UserInfo 부분 추가

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

	ts := httptest.NewServer(NewHandler())
	defer ts.Close()

	resp, err := http.Get(ts.URL + "/users/89")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, _ := ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "User Id:89")

	resp, err = http.Get(ts.URL + "/users/56")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, _ = ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "User Id:56")
}

PASS가 나오는 것을 확인할 수 있다.


이제 Create(Method POST)로 User를 등록해보자!
GET과 POST처럼 어떤 메소드로 보내느냐에 따라서 다른 핸들러를 사용해야 한다.

app.go 수정

type User struct { // json을 사용하기 위한 user struct
	ID        int       `json:"id"`
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

...

func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
	/*vars := mux.Vars(r) // 알아서 ID를 파싱을 해줌
	fmt.Fprint(w, "User Id:", vars["id"]) // 그냥 User Id라고 정해준거여서 json을 사용할 때 오류남*/
   	// 그냥 임의로 정해줌
   	user := new(User)
	user.ID = 2
	user.FirstName = "tucker"
	user.LastName = "kim"
	user.Email = "soosungp33@gmail.com"

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

func createUserHandler(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, err)
		return
	}
    
	// Created User (실질적으로 user를 등록), (user를 기억하는 부분은 나중에)
	user.ID = 2
	user.CreatedAt = time.Now() // 현재 시간을 만들어진 시간으로 한다.
   	w.Header().Add("Content_Type", "application/json")
	w.WriteHeader(http.StatusCreated) // 잘 만들었다고 알려줌
	data, _ := json.Marshal(user) // user를 json 포멧에 맞게 변경(json 다루는 포스팅에서 다 한 내용)
	fmt.Fprint(w, string(data))
}

...

func NewHandler() http.Handler {
	mux := mux.NewRouter()
    
	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler).Methods("GET") // GET메소드일 때 이 핸들러가 불려라
   	mux.HandleFunc("/users", createUsersHandler).Methods("POST") // POST메소드일 때 이 핸들러가 불려라
	mux.HandleFunc("/users/{id:[0-9]+}", getUserInfoHandler)
	return mux
}

app_test.go 추가

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

	ts := httptest.NewServer(NewHandler())
	defer ts.Close()

	resp, err := http.Get(ts.URL + "/users/89") // user id를 임의로 정해서 줌
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, _ := ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "No User Id:89") // id를 보내주게 될 때 에러가 난다.
   	// 89라는 Id를 만든 적이 없기 때문
   	// 따라서 89라는 Id가 없다라고 변경해준다.(밑에 map을 이용해서 user정보를 저장해줄 때 보면 이해가 될 것이다.)

	resp, err = http.Get(ts.URL + "/users/56")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	data, _ = ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "No User Id:56")
}

...

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

	ts := httptest.NewServer(NewHandler())
	defer ts.Close()

	// Get 대신 Post 사용 (url, response, body) -> json을 사용할 것이다.
	resp, err := http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"tucker", "last_name":"kim", "email":"soosungp33@gmail.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	// 읽어서 재대로 왔는지 확인
	user := new(User)
	err = json.NewDecoder(resp.Body).Decode(user)
	assert.NoError(err)
	assert.NotEqual(0, user.ID) // user의 ID가  0이 아니여야함.

	id := user.ID                                               // TestGetUserInfo함수와 다르게 실제 user ID정보가 오도록 해보자
	resp, err = http.Get(ts.URL + "/users/" + strconv.Itoa(id)) // int를 string으로 변경해서 id부분에 대입
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	user2 := new(User)
	err = json.NewDecoder(resp.Body).Decode(user2)
	assert.NoError(err)
	assert.Equal(user.ID, user2.ID) // 서로 같아야 맞음
	assert.Equal(user.FirstName, user2.FirstName)
}

이렇게 수정하고 실행시켜보면 오류가 난다.
그 이유는 Create된 유저의 정보를 등록하지 않아서 그런 것이다.(app.go의 getUserInfoHandler 함수에서 임의로 2라는 ID를 가져옴)
(해당 유저 정보가 있으면 정보를 리턴하고 없으면 No User ID를 리턴할 수 있게 변경해함)


유저 정보를 저장할 수 있게 map을 이용하자!

app.go 수정

...

var userMap map[int] * User // user정보를 저장할 Map 생성
var lastID int // ID는 매번 바뀌어야 하므로 변수 설정

...

func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.Atoi(vars["id"]) // string을 int로 변경
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	user, ok := userMap[id] // map에 ID에 해당하는 것이 있는지
	if !ok {                // map에 해당하는 ID가 없으면
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No User Id:", id)
		return
	}
    
	// ID가 있으면 그 user의 정보를 보내준다.
	w.Header().Add("Content_Type", "application/json")
	w.WriteHeader(http.StatusOK)
	data, _ := json.Marshal(user)
	fmt.Fprint(w, string(data))
}

...

func createUserHandler(w http.ResponseWriter, r *http.Request) {
	...

	// Created User (실질적으로 user를 등록), (user를 기억하는 부분은 나중에)
   	lastID++
	user.ID = lastID
	user.CreatedAt = time.Now() // 현재 시간을 만들어진 시간으로 한다.
   	userMap[user.ID] = user // map에 user 정보를 저장
    
   	w.Header().Add("Content_Type", "application/json")
   
	...
}

func NewHandler() http.Handler {
	userMap = make(map[int] *User)
    lastID = 0
	...
}

이제 실제 ID가 map에 저장되서 그 map에 ID가 있는지 없는지 검사하는 방식으로 구현된다.
PASS되는 것을 볼 수 있다.


🔔 이번 포스팅에는 REST API의 GET과 POST(Create)를 다뤄보았고 다음은 GET할 때 등록된 유저들의 list를 보여주는 부분(실제로 사용해보기)과 DELETE, UPDATE하는 부분에 대해 공부하고 포스팅해봐야겠다.

0개의 댓글