본문 바로가기

Go

[묘공단] Tucker의 Go 언어 프로그래밍 30장 : RESTful API 서버 만들기

  • 30.1 해법
  • 30.2 사전지식 : RESTful API
  • 30.3 RESTful API 서버 만들기
  • 30.4 테스트 코드 작성하기
  • 30.5 특정 학생 데이터 반환하기
  • 30.6 학생 데이터 추가/삭제하기 

 

30.1 해법

  • gorilla / mux 와 같은 RESTful API 웹서버 제작을 도와주는 패키지를 설치한다.
  • RESTful API에 맞춰서 웹 핸들러 함수를 만들어준다.
  • RESTful API를 테스트하는 테스트 코드를 만든다.
  • 웹브라우저로 데이터를 조회한다. 

 

30.2 사전지식 : RESTful API

  • REST (Representational State Transfer) 는 두 컴퓨터 시스템이 인터넷을 통해 정보를 안전하게 교환하기 위해 사용하는 인터페이스이다.

 

30.3 RESTful API 서버 만들기

  • 먼저 gorilla/mux를 설치한다.
    • go get -u github.com/gorilla/mux
  • 구조체로 Students를 만든다. 키값은 Id이다.
  • MakeWebHandler() 핸들러를 생성하고 학생데이터 2개를 저장해두자.
  • /students를 호출하면 GetStudentListHandler()를 호출할수있게 핸들러를 연결한다.
    • mux.HandleFunc("/students", GetStudentListHandler).Methods("GET")
  • GetStudentListHandler() 함수는 학생정보를 가져와서 JSON포맷으로 만들어주는 함수이다. 

 

//ch30/ex30.1/ex30.1.go
package main

import (
	"encoding/json"
	"net/http"
	"sort"

	"github.com/gorilla/mux"
)

type Student struct {
	Id    int
	Name  string
	Age   int
	Score int
}

var students map[int]Student // ❶ 학생 목록을 저장하는 맵
var lastId int

func MakeWebHandler() http.Handler {
	mux := mux.NewRouter() // ❷ gorilla/mux를 만듭니다.
	mux.HandleFunc("/students", GetStudentListHandler).Methods("GET")
	//-- ❸ 여기에 새로운 핸들러 등록 --//

	students = make(map[int]Student) // ❹ 임시 데이터 생성
	students[1] = Student{1, "aaa", 16, 87}
	students[2] = Student{2, "bbb", 18, 98}
	lastId = 2

	return mux
}

type Students []Student // Id로 정렬하는 인터페이스
func (s Students) Len() int {
	return len(s)
}
func (s Students) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func (s Students) Less(i, j int) bool {
	return s[i].Id < s[j].Id
}

func GetStudentListHandler(w http.ResponseWriter, r *http.Request) {
	list := make(Students, 0) // ➎ 학생 목록을 Id로 정렬
	for _, student := range students {
		list = append(list, student)
	}

	sort.Sort(list)
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(list) // ➏ JSON 포맷으로 변경
}

func main() {
	http.ListenAndServe(":3000", MakeWebHandler())
}

 

 

 

30.4 테스트 코드 작성하기

 

//ch30/ex30.1/ex30_1_test.go
package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
)

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

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/students", nil) // ❶ /students 경로 테스트

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

	assert.Equal(http.StatusOK, res.Code)
	var list []Student
	err := json.NewDecoder(res.Body).Decode(&list) // ❷ 결과 변환
	assert.Nil(err)                                // ❸ 결과 확인
	assert.Equal(2, len(list))
	assert.Equal("aaa", list[0].Name)
	assert.Equal("bbb", list[1].Name)
}

 

  • 아래와 같이 정상적으로 결과를 반환하는걸 확인할수있다.

30.5 특정 학생 데이터 반환하기

  • 이번에는 특정학생의 데이터만 반환하는 예제를 만들어보자
  • 아래와 같은 핸들러를 추가한다.
    • mux.HandleFunc("/students/{id:[0-9]+}", GetStudentHandler).Methods("GET")
  • 테스트함수에서는, 새로운 테스트함수인 TestJsonHandler2()를 만들어보자.
  • 해당 함수는 /students/1 , /students/2 를 각각 테스트하는 함수이다.

 

// ch30/ex30.2/ex30.2.go
package main

import (
	"encoding/json"
	"net/http"
	"sort"
	"strconv"

	"github.com/gorilla/mux"
)

type Student struct {
	Id    int
	Name  string
	Age   int
	Score int
}

var students map[int]Student // ❶ 학생 목록을 저장하는 맵
var lastId int

func MakeWebHandler() http.Handler {
	mux := mux.NewRouter() // ❷ gorilla/mux를 만듭니다.
	mux.HandleFunc("/students", GetStudentListHandler).Methods("GET")
	//-- ❸ 여기에 새로운 핸들러 등록 --//
	mux.HandleFunc("/students/{id:[0-9]+}", GetStudentHandler).Methods("GET")

	students = make(map[int]Student) // ❹ 임시 데이터 생성
	students[1] = Student{1, "aaa", 16, 87}
	students[2] = Student{2, "bbb", 18, 98}
	lastId = 2

	return mux
}

type Students []Student // Id로 정렬하는 인터페이스
func (s Students) Len() int {
	return len(s)
}
func (s Students) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func (s Students) Less(i, j int) bool {
	return s[i].Id < s[j].Id
}

func GetStudentListHandler(w http.ResponseWriter, r *http.Request) {
	list := make(Students, 0) // ➎ 학생 목록을 Id로 정렬
	for _, student := range students {
		list = append(list, student)
	}

	sort.Sort(list)
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(list) // ➏ JSON 포맷으로 변경
}

func GetStudentHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r) // ❶ id를 가져옵니다.
	id, _ := strconv.Atoi(vars["id"])
	student, ok := students[id]
	if !ok {
		w.WriteHeader(http.StatusNotFound) // ❷ id에 해당하는 학생이 없으면 에러
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(student)
}

func main() {
	http.ListenAndServe(":3100", MakeWebHandler())
}

 

 

 

//ch30/ex30.2/ex30_2_test.go
package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
)

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

	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/students", nil) // ❶ /students 경로 테스트

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

	assert.Equal(http.StatusOK, res.Code)
	var list []Student
	err := json.NewDecoder(res.Body).Decode(&list) // ❷ 결과 변환
	assert.Nil(err)                                // ❸ 결과 확인
	assert.Equal(2, len(list))
	assert.Equal("aaa", list[0].Name)
	assert.Equal("bbb", list[1].Name)
}

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

	var student Student
	mux := MakeWebHandler()
	res := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/students/1", nil) // ❶ id 1 학생

	mux.ServeHTTP(res, req)
	assert.Equal(http.StatusOK, res.Code)
	err := json.NewDecoder(res.Body).Decode(&student)
	assert.Nil(err)
	assert.Equal("aaa", student.Name)

	res = httptest.NewRecorder()
	req = httptest.NewRequest("GET", "/students/2", nil) // ❷ id 2 학생
	mux.ServeHTTP(res, req)
	assert.Equal(http.StatusOK, res.Code)
	err = json.NewDecoder(res.Body).Decode(&student)
	assert.Nil(err)
	assert.Equal("bbb", student.Name)
}

 

 

 

 

30.6 학생 데이터 추가/삭제하기 

 

 

  • 학생데이터를 조회,추가,삭제하기 위해서 아래의 핸들러를 등록한다.
mux.HandleFunc("/students/{id:[0-9]+}", GetStudentHandler).Methods("GET")
mux.HandleFunc("/students", PostStudentHandler).Methods("POST")
mux.HandleFunc("/students/{id:[0-9]+}", DeleteStudentHandler).Methods("DELETE")

 

 

 

//ch30/ex30.3/ex30.3.go
package main

import (
	"encoding/json"
	"net/http"
	"sort"
	"strconv"

	"github.com/gorilla/mux"
)

type Student struct {
	Id    int
	Name  string
	Age   int
	Score int
}

var students map[int]Student // ❶ 학생 목록을 저장하는 맵
var lastId int

func MakeWebHandler() http.Handler {
	mux := mux.NewRouter() // ❷ gorilla/mux를 만듭니다.
	mux.HandleFunc("/students", GetStudentListHandler).Methods("GET")
	//-- ❸ 여기에 새로운 핸들러 등록 --//
	mux.HandleFunc("/students/{id:[0-9]+}", GetStudentHandler).Methods("GET")
	mux.HandleFunc("/students", PostStudentHandler).Methods("POST")
	mux.HandleFunc("/students/{id:[0-9]+}", DeleteStudentHandler).Methods("DELETE")

	students = make(map[int]Student) // ❹ 임시 데이터 생성
	students[1] = Student{1, "aaa", 16, 87}
	students[2] = Student{2, "bbb", 18, 98}
	lastId = 2

	return mux
}

type Students []Student // Id로 정렬하는 인터페이스
func (s Students) Len() int {
	return len(s)
}
func (s Students) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func (s Students) Less(i, j int) bool {
	return s[i].Id < s[j].Id
}

func GetStudentListHandler(w http.ResponseWriter, r *http.Request) {
	list := make(Students, 0) // ➎ 학생 목록을 Id로 정렬
	for _, student := range students {
		list = append(list, student)
	}

	sort.Sort(list)
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(list) // ➏ JSON 포맷으로 변경
}

func GetStudentHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r) // ❶ id를 가져옵니다.
	id, _ := strconv.Atoi(vars["id"])
	student, ok := students[id]
	if !ok {
		w.WriteHeader(http.StatusNotFound) // ❷ id에 해당하는 학생이 없으면 에러
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(student)
}

func PostStudentHandler(w http.ResponseWriter, r *http.Request) {
	var student Student
	err := json.NewDecoder(r.Body).Decode(&student) // ❶ JSON 데이터 변환
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	lastId++ // ❷ id를 증가시킨후 맵에 등록
	student.Id = lastId
	students[lastId] = student
	w.WriteHeader(http.StatusCreated)
}

func DeleteStudentHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r) // ❶ id를 가져옵니다.
	id, _ := strconv.Atoi(vars["id"])
	_, ok := students[id]
	if !ok {
		w.WriteHeader(http.StatusNotFound) // ❷ id에 해당하는 학생이 없으면 에러
		return
	}
	delete(students, id)
	w.WriteHeader(http.StatusOK) // ❸ StatusOK 반환
}

func main() {
	http.ListenAndServe(":3000", MakeWebHandler())
}