GO Lang

GO_웹 제작(4)

u0jin 2020. 6. 23. 06:19

 

목표 : RESTful 코드 작성 ( GET / POST ) &  테스트 코드 실행 

 

먼저 REST를 아주 간단히 파악하자면,

자원의 이름으로 해당 자원의 상태를 주고 받는 모든것을 말합니다.

다시말하면,

HTTP를 통해 자원을 명시하고, HTTP Method( GET ,POST , PUT , DELETE )를 통해 자원의 CRUD Operation을 적용하는것을 의미합니다.

  •   CRUD Operation
    • Create : 생성 ( POST )
    • Read : 조회 ( GET )
    • Update : 수정 ( PUT )
    • Delete :  삭제 ( DELETE )
    • HEAD : header 정보 조회 ( HEAD )

REST라는 통일된 규격을 쓰면, 프레임워크 만들기에도 좋고, 상태를 주고 받을때도 편리하기 때문에 많이 사용합니다.

 

이정도 까지만 알고 코드로 넘어가 보겠습니다.


사용된 코드 : main.go / app.go / app_test.go

 

< main.go >

package main

import (
	"net/http"

	"yujinGo/web5/myapp"
)

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

 

< app.go >

package myapp

import (
	"net/http"
	"github.com/gorilla/mux"
)


// NewHandler make a new myapp handler
func NewHandler() http.Handler {
	mux := mux.NewRouter()

	return mux
}

 

기본 웹서버 틀이라고 보면 된다.

라우팅 설정에서 go의 외부 라이브러리인  mux 를 사용해야 하므로 설치를 진행해야한다.

깃헙 주소 : https://github.com/gorilla/mux

 

gorilla/mux

A powerful HTTP router and URL matcher for building Go web servers with 🦍 - gorilla/mux

github.com

 

(* cmd에 go get github.com/gorilla/mux 를 입력하면 된다. )

 

package myapp
/* 핸들러를 만들어주는 패키지 */

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"
	"github.com/gorilla/mux"
)


/* User 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"`
}

var userMap map[int]*User
/* 유저 정보를 들고있는 맵 */
var lastID int
/* 마지막 아이디를 등록할 변수 하나 만들어야함 -> 아이디는 매번 바뀌니까 */



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}")
}

func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
/*
	클라이언트가 요청했는지 확인후,요청이 있는경우 해당 유저 정보 반환하도록 만들어야함
	아이디정보를 뽑아옴
    r.URL.Path 에서 id 만 뽑아오면 되는데 복잡하니까 이미있는 패키지를 쓸거임
*/
	vars := mux.Vars(r)
/*
	mux패키지 안에 vars 라는게 있는데 여기에 req를 넣으면 자동으로 id를 파싱해줌
    유저가 보낸 인풋 정보가 들어잇음
*/
	id, err := strconv.Atoi(vars["id"])
/*
	아이디 정보를 뽑아오라는 뜻
    vars 는 string 으로 되어있기 때문에 int 로 변환해줘야함 
*/
    
	if err != nil {
    /* 에러가 있으면 */
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
    
	user, ok := userMap[id]
/*
	id에 해당하는 애가 맵에 있는지 확인 하는 작업
*/

	if !ok {
    /* id 에 해당하는 애가 맵에 없다면 */
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "No User Id:", id)
		return
	}

/* 여기까지 온거면 유저가 있다는 말과 같음 */

	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	data, _ := json.Marshal(user)
	fmt.Fprint(w, string(data))
 /*
 	fmt.Fprint(w, "User Id:89") 계속 이런식으로 쓸수 없기때문에 데이터를 가져오는거임 (id는 계속 바뀜)
    fmt.Fprint(w, "User Id:", vars["id"]) 이렇게 둬도 에러가 뜸
    ->create 하고 비교할때, U라는 글자로 고정되어있기 때문에 에러가 나옴
 */
    
}



func createUserHandler(w http.ResponseWriter, r *http.Request) {
/*
	클라이언트가 유저정보를 json으로 보냄
    json을 읽어들여야함
    json이 읽을수 있는 유저 struct를 만들어야함
    유저생성하는 코드를 만들어야함
*/
	user := new(User)
/*
	유저 struct instance를 만들기
*/
	err := json.NewDecoder(r.Body).Decode(user)
	//json에서 뉴이토더 해서 req바디 읽어와 - 클라이언트에서 보낸거
/*	
	json에서 NewDecoder를 통해 req 바디를 읽어옴 = 클라이언트에서 보낸걸 읽어옴
*/


	if err != nil {
    /* 에러가 있다면 */
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}

/*	여기로 넘어오면 에러가 없다는 말과 같음 */


/* Created User */

	lastID++
/* 
	유저 아이디는 매번 바껴야함 -> 유저 하나 만들때마다 하나씩 등록되어야함 
*/
	user.ID = lastID

	user.CreatedAt = time.Now()
	userMap[user.ID] = user
/*	
	유저맵에 유저 아이디에 해당하는 애를 등록함
*/
	w.Header().Add("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	data, _ := json.Marshal(user)
	fmt.Fprint(w, string(data))
}




/* NewHandler make a new myapp handler */
func NewHandler() http.Handler {
	/* http.handler 인터페이스틑 리턴함 */

	userMap = make(map[int]*User)
	lastID = 0  
/*
	사용하기전에 초기화해줘야함 -> create 할때 유저정보 등록할꺼임
*/

	mux := http.NewServeMux()
/*
	mux를 깔았으니까 http.serverMux 아니고 이제 고릴라 먹스 가지고 쓸거양
*/
	mux := mux.NewRouter()
/*
	mux를 깔았기 때문에
   	mux := http.NewServeMux() 안쓰고 
    고릴라 먹스를 이용할거임
*/

	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler).Methods("GET")
/*
	고릴라 먹스가 필요한 이유는 이거때문임.
	뒤에 메서드라고 붙여서 우리가 어떤 메서드를 쓸때 이 핸들러를 써라 라고 알려줌
	메서드를 지정해주지 않으면 그냥 상위 핸들러를 가져다가 써서 에러가 생김
*/
    
	mux.HandleFunc("/users", createUserHandler).Methods("POST")
	mux.HandleFunc("/users/{id:[0-9]+}", getUserInfoHandler)
/*
	mux.HandleFunc("/users/89", getUserInfoHandler) 
    이렇게 코딩을 하면 아이디를 계속 추가해야하기때문에
    고릴라 먹스 이용함 -> 먹스에 가면 예제가 나와있는데 자동으로 파싱이 된다.
    
*/
	return mux
}

 

indexHandler
usersHandler - get

 

post 입력 창 - user3정보 보냄
첫번쩨 user정보 입력후 모습
첫번째 user정보와 같은 바디를 보냈을경우 id는 +1 되서 저장된다.
다른정보를 보냈을경우

getUserInfoHandler

< app_test.go >

package myapp

import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
	"strings"
	"github.com/stretchr/testify/assert"
)

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

	ts := httptest.NewServer(NewHandler())
/*
	서버를 만듬 -> 핸들러 등록을 해줘야함
    실제 서버가 아닌 모의테스트서버임
*/
	
	defer ts.Close()
/* 항상 닫아줘야함 */

	resp, err := http.Get(ts.URL)
/*
	테스트서버의 url을 요청하면, 응답과 err가 반환됨
*/
	
	assert.NoError(err)
/* 
	에러가 없다면 
*/
	assert.Equal(http.StatusOK, resp.StatusCode)
/*
	인덱스 핸들러가 app.go에 정의되어있다면 에러가나지않는다.
*/

	data, _ := ioutil.ReadAll(resp.Body)
/*
	결과가 제대로 나오는지 확인
    바디를 모두 읽어오게만듬
*/
	assert.Equal("Hello World", string(data))
}


/*
	TestIndex 와 똑같은데 인덱스 경로만 달라짐
*/
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")
/*
	데이터에서 어떤 문자열이 포함되어있어야한다는 뜻
     "Get UserInfo" 라는 문자열이 포함되어있어야함 
     TestIndex 와 TestUsers  가 각각 다른 핸들러가 불려지는걸 확인해야함
*/
}



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")

/*
	아이디는 계속 바뀌기 때문에 GetUserInfo를 어떻게 할지 생각해야함
*/
}



/* 이제 create 하는 부분임 */
func TestCreateUser(t *testing.T) {
	assert := assert.New(t)

	ts := httptest.NewServer(NewHandler())
	defer ts.Close()
/* 
	테스트 서버 만들었음
*/

/*
	이제 http로 request 를 보내면 된다.
*/
	
	resp, err := http.Post(ts.URL+"/users", "application/json",
		strings.NewReader(`{"first_name":"yujin", "last_name":"kim", "email":"youjin@studentpartner.com"}`))
/*
	get이 아닌 post로 보내보는 작업
    post는 인자가 3개임 -> url string , contentType string , body io.Reader 이렇게 3개를 인자로 받아 
    응답은 resp http.Response , err error 가 나옴
    테스트 서버의 url , json으로 보냄, 바디는 json할때 보낸거 처럼 strings에 뉴 리더를 만들어서 할거임
	string 을 바로 보냄
*/
       
	assert.NoError(err)
	//우리는 create 에 한거니까 이러케해야해
	assert.Equal(http.StatusCreated, resp.StatusCode)
/*
	create 한거기때문에 ok를 쓰면 안됌
    만약 에러가 뜬가면, 핸들러를 제대로 못불러서 그런거임
    어떤 메소드를 부르느냐에 따라 다른 핸들러를 써야하는데
    post 를 썻는데 get을 불러서 201 에러가 나오는거임
*/


/* 이제 유저정보 읽어오자 */

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


	assert.NoError(err)
	assert.NotEqual(0, user.ID)
    /* 유저 아이디가 0이면 안됌 */

	id := user.ID
/* 이 아이디를 가지고 다시 get 할거임 */

	resp, err = http.Get(ts.URL + "/users/" + strconv.Itoa(id))
    /* int -> string 으로 바꿈 */
    
	assert.NoError(err)
	assert.Equal(http.StatusOK, resp.StatusCode)

/*
	읽어온거를 다시 json포맷으로 바꿔야하니까 파싱해서 다시 읽어올것
*/
	user2 := new(User)
	err = json.NewDecoder(resp.Body).Decode(user2)
    
	assert.NoError(err)

	assert.Equal(user.ID, user2.ID)
/*
	user1 과 user2는 같아야함 -> 마지막 점검임
*/
	assert.Equal(user.FirstName, user2.FirstName)
}

 

goconvey 실행화면

 

 


주석이 없는 코드는 아래의 사이트 - restful api 파일에 존재합니다.

github.com/u0jin/Go_Website_first.git