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

김영한·2020년 11월 24일
0
post-custom-banner

코드 Github
참고, 참고


📢 POST와 GET을 실제로 사용해보기

WEB5 폴더에 작성했다.

chrome://apps에 들어가서 ARC를 키고 POST로 바디를 보내준다.(Id와 name들 기억)

GET으로 users/5를 받아보면 id가 5인 정보들을 알 수 있다.


📢 DELETE 구현해보기

이제 DELETE하는 작업을 해보자
먼저 테스트 코드를 만들고
app_test.go

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

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

	// Get과 Post와 다르게 기본적으로 delete는 제공해주지 않는다.
	req, _ := http.NewRequest("DELETE", ts.URL+"/users/1", nil) // Id는 1로 임의 설정
	resp, err := http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
	data, _ := ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "No User Id:1") // 아무 것도 없는 상태이므로 없다고 출력되야 정상이다.
}
...

다음으로 Delete 핸들러를 만들 것이다.

func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)                 // Gorilla mux에서 제공하는 기능, 알아서 id부분을 추출해줌
	id, err := strconv.Atoi(vars["id"]) // int로 변환
	if err != nil {                     // 잘못 보냈을 때
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	// 맵에 없는 Id를 지우려고 할 때
	_, ok := userMap[id]
	if !ok { // 해당 Id가 없으면
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No User Id:", id)
		return
	}
	// 있으면 해당 id를 usermap에서 삭제
	delete(userMap, id) 
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, "Deleted User ID:", id)
}

테스트 코드를 실행해보면 아무것도 없는 상태에서 delete를 하므로 없다고 출력되는 것이 확인된다.


따라서 테스트 코드를 수정해서 정보가 있을 때와 없을 때를 비교해보겠다.

app_test.go

...
func TestDeleteUser(t *testing.T) {
...
	// 정보를 받아서 다시 delete 수행해보기
	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)

	req, _ = http.NewRequest("DELETE", ts.URL+"/users/1", nil)
	resp, err = http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
	data, _ = ioutil.ReadAll(resp.Body)
	assert.Contains(string(data), "Deleted User ID:1") // 정보가 있는 상태이므로 잘 지워졌다고 출력된다.
}

정보가 없는 상태에서는 No User Id:1이 들어오고, 있는 상태에서는 Deleted User ID:1이 들어오므로 전부 PASS되는 것을 볼 수 있다.


📢 UPDATE 구현해보기

먼저 테스트 코드와 핸들러를 만들어보자

app_test.go : 테스트 코드

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

	ts := httptest.NewServer(NewHandler())
	defer ts.Close()
   
	// body를 업데이트 해야함
   // PUT으로 받아온다.
	req, _ := http.NewRequest("PUT", ts.URL+"/users",
		strings.NewReader(`{"id":1, "first_name":"updated", "last_name":"updated", "email":"updated@nvaer.com"}`))
	resp, err := http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
}

app.go : 핸들러

...
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
}
...

test해보면 pass되는 것을 볼 수 있다.

이제 동작 처리를 만들어주자
1. Update or Create : 없을 경우 만들어준다.
2. Update : 없을 경우 에러를 띄워준다.
우리는 단순하게 2번을 만들어 볼 것 이다.

app.go

func updateUserHandler(w http.ResponseWriter, r *http.Request) {
	updateUser := new(User)
	err := json.NewDecoder(r.Body).Decode(updateUser)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	_, ok := userMap[updateUser.ID]
	if !ok { // 해당하는 유저가 없으면
		w.WriteHeader(http.StatusOK)                // OK를 주고
		fmt.Fprint(w, "No User ID:", updateUser.ID) // 그 ID는 없다고 알려줌
		return
	}
}

다음으로 실제적으로 create한 다음에 데이터를 update를 해보자
app_test.go

func TestUpdateUser(t *testing.T) {
...
	// 실제적으로 create한 다음에 update하는 테스트 코드
	// ID를 create 한다.
	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)

	// create한 ID를 알아낸 다음
	user := new(User)
	err = json.NewDecoder(resp.Body).Decode(user)
	assert.NoError(err)
	assert.NotEqual(0, user.ID)

	updateStr := fmt.Sprintf(`{"id":%d, "first_name":"updated"}`, user.ID) // 동적으로 ID를 받아서
	// updateStr에 저장되어 있는 user의 정보를 변경해준다.
	req, _ = http.NewRequest("PUT", ts.URL+"/users",
		strings.NewReader(updateStr))
	resp, err = http.DefaultClient.Do(req)
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)
    
 	// update된 유저 정보가 실제로 변경된 것인지 확인
	updateUser := new(User)
	err = json.NewDecoder(resp.Body).Decode(updateUser)
	assert.NoError(err)
    
	assert.Equal(updateUser.ID, user.ID)              // create된 ID와 update한 ID가 같아야 한다.
	assert.Equal("updated", updateUser.FirstName)     // create한 후 update된 FirstName이 update로 변경되어 있어야 한다.
	assert.Equal(user.LastName, updateUser.LastName) // update 후에도 LastName은 같아야 한다.
	assert.Equal(user.Email, updateUser.Email)        // update 후에도 Email도 같아야 한다.
}

app.go

func updateUserHandler(w http.ResponseWriter, r *http.Request) {
...
	userMap[updateUser.ID] = updateUser
	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	data, _ := json.Marshal(updateUser)
	fmt.Fprint(w, string(data))
}

이 코드를 가지고 테스트해보면 Fail이 난다.

FirstName은 update로 잘 변경되어 있지만 LastName과 Email이 기존과 같아야되는데 ""으로 비워져있다.

그 이유는 app_test.goupdateStr := fmt.Sprintf(`{"id":%d, "first_name":"updated"}`, user.ID) 이 부분에서 firstname만 셋팅해서 보냈다. json이 파싱을 할 때(정보를 읽을 때) 나머지 정보는 오지 않아서 default 값으로 보내지는데 app.gouserMap[updateUser.ID] = updateUser에서 그대로 맵에 덮어 씌워버려서 그 전에 저장되어 있는 값들이 전부 날라간 것이다.

따라서 update하고 싶은 항목만 변경해줘야 한다.
app.go

func updateUserHandler(w http.ResponseWriter, r *http.Request) {
...
	user, ok := userMap[updateUser.ID]
	if !ok { // 해당하는 유저가 없으면
		w.WriteHeader(http.StatusOK)                // OK를 주고
		fmt.Fprint(w, "No User Id:", updateUser.ID) // 그 ID는 없다고 알려줌
		return
	}
	if updateUser.FirstName != "" {
		user.FirstName = updateUser.FirstName
	}
	if updateUser.LastName != "" {
		user.LastName = updateUser.LastName
	}
	if updateUser.Email != "" {
		user.Email = updateUser.Email
	}
	// userMap[updateUser.ID] = user   ->   user가 포인트 타입이여서 굳이 덮어씌우지 않아도 위에 if에서 변경된다.
	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	data, _ := json.Marshal(user)
	fmt.Fprint(w, string(data))
}

테스트해보면 pass가 나는 것을 볼 수 있다.

하지만 이러면 또 문제가 발생한다.
예를 들어 LastName을 ""로 비워주고 싶은데 위 코드는 ""이면 변경해주지 않는 코드이기 때문에 비워지지 않는다.

실무에서도 많이 발생하는 문제라고 하는데 해결 방법으로는 struct를 따로 만든다고 한다.

코드를 보면

type 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"`
}

type UpdateUser struct {
	ID               int       `json:"id"`
	UpdatedFirstName bool      `json:"updated_first_name`
	FirstName        string    `json:"first_name"`
	UpdatedLastName  bool      `json:"updated_last_name`
	LastName         string    `json:"last_name"`
	UpdatedEmail     bool      `json:"updated_email`
	Email            string    `json:"email"`
	CreatedAt        time.Time `json:"created_at"`
}

이런 식으로 bool을 이용해 update인지 아닌지를 판단한다고 한다.
너무 복잡해지기 때문에 이런식으로 해결한다는 것만 알고 넘어가자!


📢 GET List

마지막으로 유저들의 list를 반환해주는 코드를 작성해보자!
userHandler의 리턴을 수정해야한다.

app.go

...
// userHandler를 수정
func usersHandler(w http.ResponseWriter, r *http.Request) {
	//fmt.Fprint(w, "Get UserInfo by /users/{id}")
	if len(userMap) == 0 { // user가 한 명도 없으면
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No Users")
		return
	}
	users := []*User{}
	for _, u := range userMap {
		users = append(users, u) // 맵에 있는 모든 유저 정보를 꺼내 users에 저장
	}
	data, _ := json.Marshal(users)
	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(data))
}
...

app_test.go

...
func TestUsers(t *testing.T) {
...
	assert.Equal(string(data), "No Users")
}
...
// 추가
func TestUsers_WithUsersData(t *testing.T) {
	assert := assert.New(t)

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

	// 유저를 2개 등록한다.
	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)

	resp, err = http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"jason", "last_name":"park", "email":"jason@gmail.com"}`))
	assert.NoError(err)
	assert.Equal(http.StatusCreated, resp.StatusCode)

	// 유저를 확인
	resp, err = http.Get(ts.URL + "/users")
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

	users := []*User{}
	err = json.NewDecoder(resp.Body).Decode(&users) // json으로 디코더가 잘 되는지 확인
	assert.NoError(err)
	assert.Equal(2, len(users)) // 2개개를 만들었으니까 길이가 2여야 한다.
}

테스트해보면 pass가 된다.


이렇게 해서 RESTful API를 전부 만들어보았다.

post-custom-banner

0개의 댓글