Language/Golang

Golang (Go언어) Go로 만드는 웹 (4)

HeeWorld 2025. 1. 9. 18:25

Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.

Golang 마스코트 Gopher(고퍼)

 

RESTful API

- REST(Reperesentational State Transfer)로 표현식으로 데이터를 전송한다는 의미가 됨(직역).

- 로이 핑딩이 2000년에 소개한 웹 아키텍처 형식으로 REST 설계 원칙에 입각한 시스템을 RESTful API라고 부름.

- REST는 여러 아키텍처 설계 방법을 합친 방식으로 자세한 건 구글에 검색해 학습 필요.

 

✓ 간단히 REST를 말하면 URL과 메서드로 데이터와 동작을 표현하는 방식임.

예를 들어 웹 서버에서 데이터를 가져오는 URL이 아래와 같다고 가정

- GET https://www.hello.com/getworldinfo.aspx?id=3  

 

해당 URL가 하는 일이 무엇인지 이해하려면, getworldingo.aspx가 무엇을 하는지 알아야함.

이런 방식의 URL 요청은 자기 표현적이지 못하기에 범용성이 떨어짐.

아래와 같이 URL을 표현한다고 가정

- GET https://www.hello.com/worldinfo/3

 

URL과 메서드를 보면 해당 요청은 3번 세계 정보를 가져오는 것이라는 것을 유추할 수 있음.

별도 외부 지식 없이 자기 표현적으로 요청 URL을 생성하고, 환경에 구애받지 않고 똑같은 요청에 대해서 똑같은 결과를 보장한다면, 범용적인 데이터 제공자(Data Provider)로서 동작할 수 있음.

 

HTTP 메서드

- HTTP는 GET,POST,PUT,PATCH, DELETE 같은 메서드를 지원함.

메서드 URL 동작
GET /worldinfo 전체 세계 정보 데이터 반환
GET /worldinfo/region region에 해당하는 세계 정보 데이터 반환
POST /worldinfo 새로운 세계 정보 등록
PUT /worldinfo/region region에 해당하는 세계 정보 데이터 변경
DELETE /worldinfo/region region에 해당하는 세계 정보 데이터 삭제

 

- 위와 같이 URL과 메서드의 조합으로 데이터와 동작을 정의할 수 있음.

- 어떤 환경에서도 똑같이 메서드와 URL만 조합해서 요청을 만들 수 있기 때문에 범용적으로 사용할 수 있다는 장점이 있음.

 

이를 정리하면, RESTful API는 다음과 같은 특징을 가짐.

1. 자기 표현적인 URL: URL만으로도 어떤 데이터에 대한 요청인지 알 수 있음.

2. 메서드로 행위 표현: 메서드로 데이터에 대한 행위를 표현, URL과 메서드 조합으로 데이터에 대한 조작을 정의함.

3. 서버/클라이언트 구조: 서버는 데이터 제공자로 존재하고 클라이언트는 데이터 사용자로 동작함. 프론트와 백엔드로 분리하고 백엔드는 데이터만 제공하고 프론트에서 데이터를 처리하고 화면에 표시하는 역할을 함.

4. 무상태(stateless): 서버는 클라이언트의 상태를 유지하지 않음. 서버가 상태를 보관할 필요가 없기 때문에 서버를 손쉽게 교체할 수 있어 빠른 장애 대응이나 분산 처리에 유용함.

5. 캐시 처리(cacheable): REST 구조로 서버가 단순해져서 더 쉽게 캐시 정책을 적용해 성능을 개선할 수 있음.

 

 

RESTful API 서버 만들기

- 새로운 Rest 폴터를 만들고 그 안에서 새로운 코드를 생성.

- Rest 폴더 안에 myapp 이라는 폴더를 새로 만들고 그 안에서 코드 하나 더 생성.

// main.go

package main

import (
	"Rest/myapp"
	"net/http"
)

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

 

// myapp.go

package myapp

import "net/http"

// NewHandler make a new my app Handler
func NewHandler() http.Handler {
	mux := http.NewServeMux()

	return mux
}

 

→ 두 코드를 작성 후 'go mod init <폴더>' 실행 해준 뒤에 debugging 진행하고 web에서 호출하기.

내용이 없어서 이렇게 호출됨.

 

- goconvey를 웹으로 열 수 없어 코드 저장 후 'go test' 명령어를 터미널에 입력하여 결과를 확인함.

// app_test.go

package myapp

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

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

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

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

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

 

* 해당 코드 작성 후 go test를 했는데, 아래와 같이 assert pakage가 없으니, go get 명령어 사용해서 pakage 받으라는 에러가 뜸.

→ 이건 'go get ~' 명령어 그래도 터미널에 입력해주면 해결 됨.

 

Error 내용 확인

- 200번을 원했는데 404번이 응답이 와서 FAIL이 됨.

 

- app.go 파일의 index경로에 Handler 등록이 되어있지 않아 에러가 발생하여 Handler 등록

// app_test.go

package myapp

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

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

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

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

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

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

 

- HandleFunc을 만들고 상단에 func으로 해당 함수를 만들어 "Hello World"가 출력되게 만듦.

- 결과가 Hello World가 나오는 것을 확인하는 테스트를 진행.

- io.ReadAll을 사용하여 resp의 Body를 읽어오게 됨.

- 해당 데이터가 "Hello World"와 같아야하고, 읽은 데이터를 string(data)로 저장함.

- 저장하고 'go test'를 실행하면 PASS가 되는 것을 확인 할 수 있음.

PASS가 된 것을 확인할 수 있음.

 

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

 

- Index경로가 아닌 기존 경로에 "/users"를 추가하여 테스트 실행 결과가 PASS가 나옴.

- index경로에 /users를 붙였는데, app.go 파일에 mux.HandleFunc에 경로가 별도 핸들러가 없어 "/"로 호출

 

// app.go

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

...

// NewHandler make a new my app Handler
func NewHandler() http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler)
	return mux
}


// app_test.go

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, _ := io.ReadAll(resp.Body)
	assert.Contains("Get UserInfo", string(data))
}

 

- mux.HandleFunc을 "/users"라는 경로로 분리하고, func 새로 생성함.

- assert의 결과 값이 "Hello World"가 아닌, contains로 "Get UserInfo"로 해당 문자열이 포함되도록 코드 수정

- 저장 후 'go test'를 실행하면 PASS 되는 것을 볼 수 있음.

PASS가 된 것을 확인할 수 있음.

 

// app_test.go


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, _ := io.ReadAll(resp.Body)
	assert.Equal("Hello World", string(data))
	// assert.Contains("Get UserInfo", string(data))
}

 

- 실제로 GET UserInfo가 들어와야 하는데, Hello World가 들어온 것은 아닌지 다시 확인하기.

- 위 처럼 TestUsers의 assert.Contains를 TestIn의 assert.Equal 값을 가지고 와서 수정 후 'go test' 실행하면 FAIL과 "GET UserInfo~" 정보가 왔다는 것을 볼 수 있음.

Error 문구 확인

 

- 이는 "/"과, "/users"가 다른 Handler로 동작한다는 것을 알 수 있음.

 

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

 

- ID를 넣었을 때 어떤 결과가 발생되는지 확인하기 위해 새로운 테스트 코드를 작성

- 해당 코드를 저장하고 'go test'를 하면, PASS가 되는 것을 확인할 수 있음.

- PASS가 된 이유는 mux.HandleFunc에 "/users" 하위 내용이 정의가 안되어있어 상위에 있는 것으로 호출됨.

PASS가 된 것을 확인할 수 있음.

 

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

 

- 데이터를 확인하는 코드를 추가해서 'go test'를 실행하면 FAIL이 되는 것을 확인할 수 있음.

- "User ID:89"가 포함되어있지 않다는 에러 문구를 확인할 수 있음.

Error 문구 확인

 

// app.go

func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "User ID:89")
}

...

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

	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler)
	mux.HandleFunc("/users/89", getUserInfoHandler)
	return mux
}

 

- getUserInfoHandler를 새롭게 추가하고 저장하여 'go test'를 실행하면 OK가 되는 것을 확인할 수 있음.

PASS된 것을 확인할 수 있음

 

gorilla 패키지 사용

- github에서 gorilla라는 패키지를 설치하여 새로운 테스트를 진행

(https://github.com/gorilla/mux)

 go get -u github.com/gorilla/mux

 

// app.go

package myapp

import (
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
)

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) {
	fmt.Fprint(w, "User ID:89")
}

// NewHandler make a new my app Handler
func NewHandler() http.Handler {
	mux.NewRouter()
	mux := http.NewServeMux()

	mux.HandleFunc("/", indexHandler)
	mux.HandleFunc("/users", usersHandler)
	mux.HandleFunc("/users/89", getUserInfoHandler)
	return mux
}

 

- NewHandler()에 mux.NewRouter()를 추가하면 gorilla 패키지가 추가 됨.

- gollia를 사용하는 방법은 github에 가면 나와 있음.

id 사용하는 방법

func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	fmt.Fprint(w, "User ID:", vars["id"])
}

...

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

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

 

- NewHandler에 id를 사용하는 방법으로 getUserInfoHandler를 수정함.

- 그리고 func getUserInfoHandler에 고정 id값이 아닌 다른 값이 들어 올 수 있도록, mux패키지의 Vars를 사용하면 vars가 스스로 ID를 파싱함.

- 결과는 PASS가 되는 것을 확인할 수 있음.

 

 

p.s 아 연달아 빡시게 보니까 오타를 자꾸 낸다..........ㅠㅠ 챗 GPT가 시간을 벌어준다ㅎㅎ