a story

Go스터디: 6주차(27~30장) 본문

Book Study/Tucker의 Go Programming

Go스터디: 6주차(27~30장)

한명 2023. 11. 5. 20:35

이 글은 골든래빗 ‘Tucker의 Go 언어 프로그래밍의 27~30장 써머리입니다.

27장 객체지향 설계 원칙 SOLID

객체지향 설계의 5가지 원칙인 SOLID를 살펴보고 좋은 설계가 무엇인지 살펴본다.

좋은 설계: 상호 결합도(coupling)가 낮고 응집도(cohesion)가 높은 설계를 말한다. 반대로 상호 결합도가 높다는 것은 모듈이 서로 강하게 결합되어 있어서 떼어 낼 수 없다는 의미이다. 한편 응집도가 낮다는 것은 하나의 모듈이 스스로 자립하지 못한다는 의미로, 다른 모듈에 의존적인 관계를 가지는 경우이다.

단일 책임의 원칙(single responsibility principle, SRP)

모든 객체는 하나의 책임만 져야 한다.

→ 코드의 재사용성을 높여준다.

아래는 나쁜 사례로, 회계 보고서라는 책임과 보고서 전송이라는 책임까지 지고 있다.

type FinanceReport stuct { // 회계 보고서
	report string
}

func (r *FinanceReport) SendReport (email string) { // 보고서 전송
}

책임이 두 가지가 되므로, 이후 마케팅 보고서라는 객체가 생겨도 회계 보고서의 SendReport()를 사용할 수 없다.

이를 개선하기 위해서 FinanceReport는 Report 인터페이스를 구현하고, ReportSender는 Report 인터페이스를 이용하는 관계를 형성하면 된다.

type Report interface { // Report() 메서드를 포함하는 Report 인터페이스
	Report() string
}

type FinanceReport Struct { // 경제 보고설르 담당하는 FinanceReport
	report sting
}

func (r *FinanceReport) Report() sting { // Report 인터페이스를 구현
}

type ReportSender struct { // 보고서 전송을 담당
}

func (s *ReportSender) SendReport(report Report) {
// Report 인터페이스 객체를 인수로 받음
}

개방-폐쇄 원칙(open-closed principle, OCP)

확장에는 열려 있고, 변경에는 닫혀 있어야 한다. (프로그램에 기능을 추가할 때 기존 코드의 변경을 최소화 해야 한다)

→ 상호 결합도를 줄여 새 기능을 추가할 때 쉽게 할 수 있고, 기존 구현을 변경하지 않아도 된다.

아래는 나쁜 사례로, 전송 방식을 추가할 때 새로운 case를 만들어 구현을 추가한다. (기존 SendReport() 함수 구현을 변경하게 된다.)

func SendReport(r *Report, method SendType, receiver string) {
	switch method {
	case Email:
		// 이메일 전송 
	case Fax:
		// Fax 전송 
	case PDF:
		// PDF 파일 생성
	case Printer:
		// 프린팅 
	..
	}
}

이를 개선하기 위해서 ReportSender 인터페이스를 생성하고, 각 방식이 이 인터페이스를 구현하도록 한다.

type ReportSender interface {
	Send(r *Report)
}

type EmailSender sturct {
}

func (e *EmailSender) Send(r *Report) {
	// 이메일 전송
}

type FaxSender sturct {
}

func (f *FaxSender ) Send(r *Report) {
	// F  ax 전송
}

리스코프 치환 원칙(liskov substitution principle, LSP)

q(x T)를 타입 T의 객체 x에 대해 증명(동작)할 수 있는 속성이라 하자. 그렇다면 S가 T의 하위 타입이라면 q(y S)는 타입 S의 객체 y에 대해 증명(동작)할 수 있어야 한다. (상위 타입을 인수로 받아 동작하는 함수는 하위 타입의 인수에도 동작해야 한다)

→ 예상치 못한 동작을 예방할 수 있다. (리스코프 치환 원칙이 지켜지지 않으면 함수의 동작을 예측하기 어렵다)

아래는 LSP에 위반 사례로, Go에서 상속이 있다는 가정으로 생각해보면 동일한 동작을 예상하는 FillScreenWidth 에서 다른 동작이 일어나는 오류가 발생한다.

class Rectangle { // 사각형
	width int
	height int

	setWidth(w int) { width = w }
	setHeight(h int) { Height = h }
}

class Square extends Rectangle { // 정사각형 (사각형을 상속)
	@override
	setWidth(w int) { width = w; height = w; }
	@override
	setHeight(h int) { width = h; height = h; }
}

// 화면 가로 크기에 맞게 이미지의 가로 크기 늘립니다. -> Square가 들어오면? 세로도 늘어난다.
func FillScreenWidth(screenSize Rectangle, imageSize *Rrectangle) {
	if imageSize.width < screenSize.width {
		imageSize.setWidth(screenSize.width)
	}
}

Go에서는 상속이 없기 때문에 이러한 동작이 일어나지 않는다.

다만 Go에서도 인터페이스를 사용하면 상위 개체(인터페이스), 하위 개체(구현한 객체)의 관계가 생기기 때문에, 인터페이스를 인수로 받는 함수는 모든 하위 개체에서 동작이 되어야 하는 원칙이 지켜져야 한다. 그래서 인터페이스 타입 변환 같은 다이나믹 캐스팅(인터페이스 타입을 구체화된 객체로 바꾸는 것)을 하지 않는 것이 좋다.

인터페이스 분리 원칙(interface segregation principle, ISP)

클라이언트(인터페이스의 이용)는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

→ 인터페이스를 분리하면 불필요한 메서드들과 의존 관계가 끊어져 더 가볍게 인터페이스를이용할 수 있다.

아래는 나쁜 사례로, SendReport()는 Report 인터페이스가 포함한 4개 메서드 중에 Report()메서드만 사용한다. (인터페이스에 불필요한 메서드가 포함되어 있다)

type Report interface {
	Report() string
	Pages() int
	Author() string
	WrittenDate() time.Time
}

func SendReport(r Report) {  // 함수를 호출려면 인수는 4개의 메서를 다 구현하고 있어야 한다.
	send(r.Report())
}

해결하기 위해서 인터페이스를 적절하게 분리해야 한다.

type Report interface {
	Report() string
}

type WrittenInfo interface {
	Pages() int
	Author() string
	WrittenDate() time.Time
}

func SendReport(r Report) { // 함수를 호출하는 Report는 사용하는 Report() 하나의 메서드만 구현하면 된다.
	send(r.Report())
}

인터페이스를 분리해 불필요한 메서드들과 의존 관계를 끊으면 더 가볍게 인터페이스를 이용할 수 있다.

의존 관계 역전 원칙(dependency inversion principle, DIP)

구체화된 객체는 추상화된 객체와 의존 관계를 가져야 한다.

→ 구체화된 모듈이 아닌 추상 모듈에 의존함으로써 확장성이 증가한다. 상호 결합도가 낮아져서 다른 프로그램으로 이식성이 증가한다.

아래는 메일이라는 모듈과 알람이라는 모듈이 관계를 맺고 있는 코드이다. (메일이 알람을 소유하고 있다)

type Mail struct {
	alarm Alarm
}

type Alarm struct {
}

func (m *Mail) OnRecv() { // OnRecv() 메서드는 메일 수신 시 호출된다.
	m.alarm.Alarm() // 알람을 울린다.
}

이를 인터페이스를 통해 의존하도록 바꾼다.

package main

import "fmt"

type Event interface {
	Register(EventListener)
}

type EventListener interface {
	OnFire()
}

type Mail struct {
	listener EventListener
}

func (m *Mail) Register(listener EventListener) { // Event 인터페이스 구현
	m.listener = listener
}

func (m *Mail) OnRecv() { // 등록된 listner의 OnFire() 호출
	m.listener.OnFire()
}

type Alarm struct {
}

func (a *Alarm) OnFire() { // EVentListner 인터페이스 구현
	// 알람
	fmt.Println("알람")
}

func main() {
	var mail = &Mail{}
	var listener EventListener = &Alarm{}

	mail.Register(listener)
	mail.OnRecv() // 알람이 울린다.
}

28장 테스트와 벤치마크

테스트 코드는 작성한 코드를 테스트 하는 코드이고, 벤치마크 코드는 코드 로직의 성능을 측정하는 코드이다.

28.1 테스트 코드

Go에서는 테스트 코드 작성과 실행을 언어에서 지원한다. 테스트 코드를 작성하면 go test 명령으로 실행할 수 있다.

Go의 테스트 코드의 작성 규약은 아래와 같다.

  1. 테스트 코드는 파일명이 _test.go로 끝나는 파일 안에 존재해야 한다.
  2. 테스트 코드를 작성하려면 import “testing”으로 testing 패키지를 가져와야 한다.
  3. 테스트 코드들은 함수로 묶여 있어야 하고, 함수명은 반드시 Test 로 시작해야 한다. 형태는 func TestXxxx(t *testing.T) 형태이어야 한다.

예를 들어, 아래 코드에 대한 테스트 코드 main.go를 작성해본다.

package main

import "fmt"

func square(x int) int {
	return 81
}

func main() {
	fmt.Printf("9*9=%d\\n", square(9))
}

동일한 위치에서 main_test.go를 만들고 아래와 같이 작성한다.

package main

import "testing"

func TestSquare(t *testing.T) {
	rst := square(9)

	if rst != 81 {
		t.Errorf("square(9) should be 81, but returns %d", rst)
	}
}

아래는 테스트 결과이다.

$ go test
PASS
ok      goprojects/test28       1.010s

테스트 코드 작성 규칙을 따라 테스트 코드 파일을 _main.go 로 끝나게 맞춰줘야 테스트 코드로 인식한다. 아래는 테스트 파일을 제대로 인식하지 못한 에러이다.

$ go test
?       goprojects/test28       [no test files]

테스트 코드를 추가한다.

func TestSquare2(t *testing.T) {
	rst := square(3)

	if rst != 9 {
		t.Errorf("square(3) should be 9, but returns %d", rst)
	}
}

다시 테스트를 돌려보면 테스트 코드가 실패한 것을 확인할 수 있다.

$ go test
--- FAIL: TestSquare2 (0.00s)
    main_test.go:17: square(3) should be 9, but returns 81
FAIL
exit status 1
FAIL    goprojects/test28       0.950s

실제 코드가 하드 코딩되어 발생한 에러로 return x*x 로 수정한다.

VS Code에서는 아래와 같이 자동으로 테스트가 가능하도록 생긴다. run package tests 를 클릭해도 된다. run package tests 는 패키지 전체의 테스트를 수행하고, run test 로 개별 테스트를 수행할 수 있다. 커맨드 라인에서는 go test -run 테스트명 으로 개별 테스트를 수행할 수 있다.

테스트 코드 작성을 간결하게 도와주는 stretchr/testify 패키지를 사용해본다.

package main

import (
	"testing"

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

func TestSquare1(t *testing.T) {
	assert := assert.New(t) // 테스트 객체 생성
	assert.Equal(81, square(9), " square(9) should be 81") // 테스트 함수 호출
}

func TestSquare2(t *testing.T) {
	assert := assert.New(t)
	assert.Equal(9, square(3), " square(3) should be 9")
}

go get 으로 패키지를 설치하고 테스트를 수행해본다.

$ go test
# goprojects/test28
main_test.go:6:2: no required module provides package github.com/stretchr/testify/assert; to add it:
        go get github.com/stretchr/testify/assert
FAIL    goprojects/test28 [setup failed]
$ go get github.com/stretchr/testify/assert
go: added github.com/davecgh/go-spew v1.1.1
go: added github.com/pmezard/go-difflib v1.0.0
go: added github.com/stretchr/testify v1.8.4
go: added gopkg.in/yaml.v3 v3.0.1
$ go test
PASS
ok      goprojects/test28       1.201s

임의로 테스트 코드에 에러를 발생시켜 보면 테스트 코드의 결과가 상세하게 바뀐 것을 알 수 있다.

$ go test
--- FAIL: TestSquare1 (0.00s)
    main_test.go:11:
                Error Trace:    C:/../projects/goprojects/test28/main_test.go:11
                Error:          Not equal:
                                expected: 8
                                actual  : 81
                Test:           TestSquare1
                Messages:        square(9) should be 81
FAIL
exit status 1
FAIL    goprojects/test28       0.924s

Equal() 메서드 외에도 NotEqual(), Nil(), NotNil() 등의 메서드를 제공한다.

stretchr/testify 패키지에서 mock, suite 패키지를 제공하고 있다.

mock 패키지: 모듈의 행동을 가장하는 목업(mockup) 객체를 제공한다. 예를 들어, 온라인 기능 테스트를 할 때 하위 영역인 네트워크 객체를 가장하는 목업 객체를 만들 때 유용하다.
suite 패키지: 테스트 준비 작업이나 테스트 종료 후 처리 작업을 도와주는 패키지이다. 예를 들어, 텍스트에 특정 파일이 있어야 하는 경우, 임시 파일을 생성하고, 테스트 종료 후 삭제해주는 작업을 만들 때 유용하다.

28.2 테스트 주도 개발

테스트의 중요성이 커짐에 따라 코드 작성 이전에 테스트 코드를 작성하는 테스트 주도 개발(Test Driven Development, TDD) 방식을 소개한다.

 

테스트는 크게 블랙박스 테스트와 화이트 박스 테스트로 구분할 수 있다.

블랙박스 테스트는 제품 내부를 오픈하지 않은 상태에서 진행되는 테스트이다. 사용자 입장의 테스트라고 해서 사용성 테스트(usability test)라고 하기도 한다. 프로그램 코드에 대한 검증이 아니라, 프로그램을 실행한 상태에서 동작을 검사하는 방식이다. 보통 전문 테스트, QA 직군에서 담당한다.

화이트박스 테스트는 내부 코드를 직접 검증하는 방식이다. 유닛 테스트(unit test, 단위 테스트)라고 부른다. 프로그래머가 직접 테스트 코드를 작성해서 내부 테스트를 검사하는 방식이다.

 

전통적인 화이트박스 테스트는 코드 작성 → 테스트 → 버그 발견 → 코드 수정 로 이뤄진다.

이러한 방식은 코드 작성 후 테스트 코드를 작성하다 보니 메인 시나리오에 의존해 테스트를 하여 예외 상황이나 경계 체크(boundary check)가 무시되기 쉽다. 또한 테스트 통과를 목적으로 하는 형식적인 테스트 코드가 될 수 있다.

 

테스트 주도 개발(TDD)이 대안이 될 수 있다. 테스트 주도 개발은 테스트 코드 작성 시기를 코드 작성 이전으로 옮긴 방식이다. 테스트 작성 → 테스트 실패 → 코드 작성 → 테스트 성공→ 개선 을 반복하여 코드를 테스트를 성공시키고 리팩터링(refactoring)하여 완성하는 방식이다.

28.3 벤치마크 코드

Go의 testing 패키지는 테스트 코드 외에 코드의 성능을 검사하는 벤치마크 기능을 지원한다.

Go의 벤치마크 코드의 작성 규약은 아래와 같다.

  1. 벤치마크 코드는 파일명이 _test.go로 끝나는 파일 안에 존재해야 한다.
  2. 벤치마크 코드를 작성하려면 import “testing”으로 testing 패키지를 가져와야 한다.
  3. 벤치마크 코드들은 함수로 묶여 있어야 하고, 함수명은 반드시 Benchmark로 시작해야 한다. 형태는 func BenchmarkXxxx(b *testing.B) 형태이어야 한다.

벤치마크 코드는 go test -bench . 로 실행할 수 있다.

29장 Go 언어로 만드는 웹 서버

HTTP 프로토콜을 사용하여 요청에 응답하는 서버를 웹 서버 혹은 HTTP 서버라고 한다. Go에서는 net/http 패키지를 제공한다.

Go에서 웹서버를 만들려면 핸들러 등록과 웹서버 시작이라는 두 단계를 거친다.

핸들러란 각 HTTP 요청이 수신됐을 때 HTTP 요청 URL 경로에 대응해 처리하는 함수 또는 객체라고 보면 된다.

http://goldenrabbit.co.kr/news?startDate=2023-11-03 와 같은 HTTP 요청 URL을 구분할 수 있다.

http:// : 프로토콜
goldenrabbit.co.kr : 도메인
/news : 경로
?startDate=2023-11-03 : 쿼리 스트링 (startDate: 매개변수, 2023-11-03: 값)

 

핸들러는 HandleFunc() 함수로 등록할 수 있고, ListenAndServer() 함수로 웹 서버를 시작한다.

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello world!") // 핸들러 등록
	})

	http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Bar pasge!") // /bar에 대한 핸들러 등록
	})

	http.ListenAndServe(":3000", nil) // 웹서버 시작
}

HandleFunction()의 핸들러 함수는 두 인수를 가지는데, http.Request 에는 클라이언트에서 보낸 메서드, 헤더, 바디와 같은 HTTP 요청 정보를 가지고, http.ResponseWriter 인수는 이후 Fprint()의 출력 스트림으로 지정된다. http.ResponseWriter 타입에 값을 쓰면 HTTP 응답으로 전송된다.

 

ListenAndServe() 는 두가지 인수를 가지는데, 첫번째 인수는 HTTP 요청을 수신하는 주소를 입력하고, 두번째 인수에는 핸들러 인스턴스를 넣어준다. 이 값이 nil을 넣어주면 DefaultServeMux를 사용하는데, DefaultServeMux는 http.HandleFunc() 함수로 등록된 핸들러들을 사용한다.

http를 인스턴스를 명시적으로 만들지 않고, 바로 사용한다? http.ResponseWriter와 http.Request는 언제 만들어지는 걸까?
http.HandleFunc에 함수 리터럴인 부분은 실제로 언제 사용될지 모르는 부분이다. http.ListenAndServer()로 웹서버가 시작되고, 실제로 이 핸들러가 호출될 때, http.ResponseWriter와 http.Request가 전달된다.

 

 

DefaultServeMux를 사용하면 http.HandleFunc()을 이용해서 등록한 핸들러를 사용하기 때문에 다양한 기능을 추가하기 어렵다.

앞의 기본 예제를 ServeMux 인스턴스를 생성해 사용할 수 있다.

package main

import (
	"fmt"
	"net/http"
)

type fooHandler struct{}

func (f *fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "foo pasge!") // 핸들러로 사용되는 구조체는 ServeHTTP를 구현해야 한다
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello world!") // 핸들러 등록
	})

	mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Bar pasge!") // /bar에 대한 핸들러 등록
	})

	mux.Handle("/foo", &fooHandler{}) // handler 구조체를 선언해서 mxu.Handle()에 추가하는 방법

	http.ListenAndServe(":3000", mux) // 웹서버 시작
}

Mux: multiplexer(멀티플렉서)의 약자로 여러 입력 중 하나를 선택해서 반환하는 디지털 장치를 말한다. 여기서는 각 URL에 대한 핸들러를 등록한 다음, HTTP 요청이 왔을 때 URL에 해당하는 핸들러를 선택해서 실행하는 방식이다. 이러한 방식을 라우터(router)라고 말하기도 한다.

 

 

과거의 웹 서버가 서버에 위치한 HTML을 전달하는 목적이었다면, 현재의 웹 서버에는 아래와 같은 변화가 있다.

Server Rendering → Client Rendering

Server Rendering은 요청에 대해 서버에서 로직을 수행 결과로 HTML를 만들어 응답하는 구조이라면, Client Rendering은 요청에 대해 서버가 HTML의 틀(템플릿) 제공하고, 클라이언트에서 렌더링을 할 때 템플릿을 동적으로 데이터를 채워나가는 방식이다. 동적인 요청에 대한 결과를 JSON이라는 데이터 형태로 받는다.

Frontend와 Backend의 혼합 → Frontend와 Backend의 역할의 분할

Frontend가 Client Rendering을 담당하고, Backend에서는 로직을 수행하고 데이터의 전달을 담당한다.

이 과정에서 중요한 역할을 하는 JSON 데이터를 처리하는 방식을 살펴본다.

JSON(JavaScript Object Notation) 데이터를 전송하기 위해서 encoding/json 패키지를 사용한다. 이 패키지를 사용해 구조체를 JSON 데이터로 변환(marshal, encode)하고, 다시 JSON 데이터를 구조체로 변환(unmarshal, decode)할 수 있다.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type Student struct {
	Name  string
	Age   int
	Score int
}

func MakeWebHandler() http.Handler { // 핸들러 인스턴스를 생성하는 함수
	mux := http.NewServeMux()
	mux.HandleFunc("/student", StudentHandler)
	return mux
}

func StudentHandler(w http.ResponseWriter, r *http.Request) {
	var student = Student{"aaa", 16, 87}
	data, _ := json.Marshal(student) // Student 객체를 []byte로 변환

	w.Header().Add("content-type", "application/json") // 헤더에 JSON 포맷임을 표시
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(data))
}

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

테스트 코드를 통해서 JSON 데이터를 받아 객체로 변환해본다 처리해 본다.

package main

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

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

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

	res := httptest.NewRecorder()                      // 테스트 response 레코더를 만든다.
	req := httptest.NewRequest("GET", "/student", nil) // /student 경로 테스트

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

	assert.Equal(http.StatusOK, res.Code) // http 상태 코드 확인
	student := new(Student)
	err := json.NewDecoder(res.Body).Decode(student) // 결과 변환(res.Body의 결과를 decode해서 student 객체에 담는다)
	// 결과 확인
	assert.Nil(err)
	assert.Equal("aaa", student.Name)
	assert.Equal(16, student.Age)
	assert.Equal(87, student.Score)
}

 

기타 Go의 다양한 웹 프레임워크는 아래 참고를 확인한다.

참고: https://velog.io/@geunwoobaek/Go-Framework-비교

30장 RESTful API 서버 만들기

RESTful API는 서버에서 어떠한 리소스를 제공할 때 이 리소스에 접근하는 API의 설계 방식이다. REST에서는 HTTP 프로토콜을 사용하고, 리소스를 URI(uniform resource identifier)로 대응시키고, 지정된 URI에 대해서 HTTP 메서드를 대칭해 해당 리소스에 대한 CRUD를 관리한다.

REST에서는 URL과 HTTP 메서드로 데이터와 동작을 표현한다.

GET <Https://somesite.com/students/3>

URL과 메서드를 보면 이 요청이 3번 학생 데이터를 가져오는 요청이라는 것을 유추할 수 있다.

이때 메서드는 HTTP 메서드를 의미하는데, 아래와 같은 메서드를 지원한다. 이러한 메서드를 바탕으로 URL로 전달한 자원에 대한 동작을 정의한다. 이렇게 URL 만으로 어떤 요청인지 알 수 있기 때문에 RESTful API의 특징을 자기표현적인 URL이라고 한다.

메서드 URL 동작

GET /students 전체 학생 데이터 반환
GET /students/3 id에 해당하는 학생 데이터 반환
POST /students 새로운 학생 등록
PUT /students/id id에 해당하는 학생 데이터 변경
DELETE /students/id id에 해당하는 학생 데이터 삭제

여기서는 아래의 단계로 RESTful API에 맞는 웹서버를 구현한다.

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

이 예제에서는 학생(Student) 구조체를 정의하고, 이 리소스를 위의 표와 같은 동작을 하는 REST API로 구현해본다.

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               // map id로 사용

// gorilla/mux 패키지를 이용해 웹 핸들러를 만들고, 임시 학생 데이터 두개 생성 저장
func MakeWebHandler() http.Handler {
	mux := mux.NewRouter() // gorilla/mux를 만든다.
	// /students 요청을 받으면 GetStudentListHandler() 함수가 호출되도록 하고,
	// Methods() 메서들르 통해 GET 메서드 요청을 받을 때만 핸들러가 동작하도록 한다.
	mux.HandleFunc("/students", GetStudentListHandler).Methods("GET") // 학생 리스트
	// <- 여기에 새로운 핸들러 등록 ->
	mux.HandleFunc("/students/{id:[0-9]+}", GetStudentHandler).Methods("GET")       // id 학생 정보
	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", 17, 89}
	lastId = 2

	return mux
}

// Id 로 정렬하는 인터페이스 구현
// Len(), Less(), Swap() 메서드를 구현해 sort.Interface를 사용할 수 있게함
// sort.Sort(Students(s)) 형태로 사용
type Students []Student

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
}

// 학생 정보를 가져와 JSON 포맷으로 변경하는 핸들러
func GetStudentListHandler(w http.ResponseWriter, r *http.Request) {
	list := make(Students, 0)
	for _, student := range students {
		list = append(list, student)
	}

	sort.Sort(list) // 학생 목록을 Id로 정렬
    w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK) // 책에서는 윗라인과 순서가 바뀜. w.Header().Set() 후에 w.WriteHeader() 해야한다. 
	json.NewEncoder(w).Encode(list) // JSON 포맷으로 변경해서 결과를 쓴다.
}

// id에 해당하는 학생을 가져와 반환하는 핸들러
func GetStudentHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)               // mux에 요청으로 들어온 URL에서 인수를 가져온다.
	id, _ := strconv.Atoi(vars["id"]) // 경로가 /students/{id:[0-9]+} 이므로, gorillar/mux에서 자동으로 id값을 내부 맵에 저장한다. vars["id"]로 id 값 가져온다.
	student, ok := students[id]       // student 맵에서 데이터가 있는지 확인한다.
	if !ok {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK) 
	json.NewEncoder(w).Encode(student)
}

// POST 요청이 오면 학생을 추가하는 핸들러
func PostStudentHandler(w http.ResponseWriter, r *http.Request) {
	var student Student
	err := json.NewDecoder(r.Body).Decode(&student)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	lastId++
	student.Id = lastId
	students[lastId] = student
	w.WriteHeader(http.StatusCreated)
}

// DELETE 요청이 오면 학생을 삭제하는 핸들러
func DeleteStudentHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, _ := strconv.Atoi(vars["id"])
	_, ok := students[id]

	if !ok {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	delete(students, id)
	w.WriteHeader(http.StatusOK)
}

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

 

참고로 노출하지 않은 HTTP 메서드로 접근하면 아래와 같은 에러가 발생한다. 405: Method Not Allowed 이다.

--- FAIL: TestJsonHandler3 (0.00s)
    main_test.go:68:
                Error Trace:    C:/Users/montauk/Desktop/projects/goprojects/rest30/main_test.go:68
                Error:          Not equal:
                                expected: 201
                                actual  : 405
                Test:           TestJsonHandler3

 

'Book Study > Tucker의 Go Programming' 카테고리의 다른 글

Go스터디: 7주차(31장)  (0) 2023.11.09
Go스터디: 5주차(23~26장)  (0) 2023.10.29
Go스터디: 4주차(18~22장)  (1) 2023.10.22
Go스터디: 3주차(12~17장)  (1) 2023.10.15
Go스터디: 2주차(3~11장)  (1) 2023.10.08