| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 
| 30 | 
                            Tags
                            
                        
                          
                          - OpenAI
 - 머신러닝
 - 분류
 - deep learning
 - GPT-4
 - regression
 - supervised learning
 - 해커톤
 - LLM
 - 티스토리챌린지
 - 회귀
 - Classification
 - LG
 - PCA
 - 오블완
 - ChatGPT
 - 지도학습
 - gpt
 - 딥러닝
 - LG Aimers
 - AI
 - LG Aimers 4th
 - Machine Learning
 
                            Archives
                            
                        
                          
                          - Today
 
- Total
 
SYDev
[Go 언어로 배우는 웹 애플리케이션 개발] Chapter 17. 엔드포인트 추가하기 본문
Section 65. Entity.Task 타입 정의와 영구 저장 방법의 임시 구현
1. Entity.Task 타입 정의
- 자체 타입을 지정하여 잘못된 대입 방지
 
package entity
import "time"
// 자체 타입을 지정하여, 잘못된 대입 방지
type TaskID int64
type TaskStatus string
const (
	TaskStatusTodo TaskStatus = "todo"
	TaskStatusDoing TaskStatus = "doing"
	TaskStatusDone TaskStatus = "done"
)
type Task struct {
	ID		TaskID		`json:"id"`
	Title	string		`json:"title"`
	Status	TaskStatus	`json:"status"`
	Created	time.Time	`json:"created"`
}
type Tasks []*Task

-> 3번째 줄은 타입 추론(type inference)에 의해서 컴파일러가 TaskID 타입의 1로 해석
2. entity.Task의 영구 저장 방법 임시 구현
- entity.Task 타입값을 영구 저장하는 임시 구현
 - store directory에 store.go라는 파일로 저장
 
package stroe
import (
	"errors"
	"github.com/Imsyp/go_todo_app/chapter17/section65/entity"
)
var (
	Tasks = &TaskStore{Tasks: map[entity.TaskID]*entity.Task{}}
	ErrNotFound = errors.New("not found")
)
type TaskStore struct {
	// 동작 확인용
	LastID entity.TaskID
	Tasks map[entity.TaskID]*entity.Task
}
func (ts *TaskStore) Add(t *entity.Task) (entity.TaskID, error) {
	ts.LastID++
	t.ID = ts.LastID
	ts.Tasks[t.ID] = t
	return t.ID, nil
}
func (ts *TaskStore) Get(id entity.TaskID) (*entity.Task, error) {
	if ts, ok := ts.Tasks[id]; ok {
		return ts, nil
	}
	return nil, ErrNotFound
}
// 정렬이 끝난 task 목록을 반환
func (ts *TaskStore) All() entity.Tasks {
	tasks := make([]*entity.Task, len(ts.Tasks))
	for i, t := range ts.Tasks {
		tasks[i-1] = t
	}
	return tasks
}
-> Task.ID field: 실제 구현에서는 RDBMS에 의해 자동 증가한 값을 설정해야 하므로, TaskStore 타입에서도 내부에 할당이 끝난 ID 번호를 저장
Section 66. 헬퍼 함수 구현
- 지금부터 작성하는 몇 개의 HTTP handler -> response data를 JSON으로 변환해서 상태 코드와 함게 http.ResponseWriter 인터페이스를 충족하는 type의 값으로 기록하는 작업을 반복
 - HTTP handler 구현을 공통화하기 위해 미리 helper function 작성
 
package handler
import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
)
type ErrResponse struct {
	Message string		`json:"message"`
	Details []string	`json:"details,omitempty"`
}
func RespondJSON(ctx context.Context, w http.ResponseWriter, body any, status int) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	bodyBytes, err := json.Marshal(body)
	if err != nil {
		fmt.Printf("encode response error: %v", err)
		w.WriterHeader(http.StatusInternalServerError)
		rsp := ErrResponse{
			Message: http.StatusText(http.StatusInternalServerError),
			if err := json.NewEncoder(w).Encode(rsp); err != nil {
				fmt.Printf("write error response error: %v", err)
			}
			return
		}
	}
	w.WriteHeader(status)
	if _, err := fmt.Fprintf(w, "%s", bodyBytes); err != nil {
		fmt.Printf("write response error: %v", err)
	}
}
-> HTTP handler 내에서 귀찮은 JSON 응답 작성을 간략화
1. 테스트용 헬퍼 함수 구현
- HTTP 핸들러의 테스트에서 사용할 검증 순서도 공통 헬퍼로 작성
 - 복잡한 JSON 구조를 문자열로 비교하면 어디에 차이가 있는지 파악하기 어려움
- AssetJSON: JSON 문자열을 Unmarshal하고 cmp 패키지를 사용해 차이를 비교
 
 
package testutil
import (
	"encoding/json"
	"io"
	"net/http"
	"os"
	"testing"
	"github.com/google/go-cmp/cmp"
)
func AssertJson(t *testing.T, want, got []byte) {
	t.Helper()
	var jw, jg any
	if err := json.Unmarshal(want, &jw); err != nil {
		t.Fatalf("cannot unmarshal want %q: %v", want, err)
	}
	if err := json.Unmarshal(got, &jg); err != nil {
		t.Fatalf("cannot unmarshal got %q: %v", got, err)
	}
	if diff := cmp.Diff(jg, jw); diff != "" {
		t.Errorf("got differs: (-got +want)\n%s", diff)
	}
}
func AssertResponse(t *testing.T, got *http.Response, status int, body []byte) {
	t.Helper()
	t.Cleanup(func() { _ = got.Body.Close() })
	gb, err := io.ReadAll(got.Body)
	if err != nil {
		t.Fatal(err)
	}
	if got.StatusCode != status {
		t.Fatalf("want status %d, but got %d, body: %q", status, got.StatusCode, gb)
	}
	if len(gb) == 0 && len(body) ==0 {
		// 어느 쪽이든 응답 바디가 없으므로 AssetJSON 호출할 필요 X
		return
	}
	AssetJSON(t, body, gb)
}
// 입력값, 기댓값을 파일에서 읽어옴
func LoadFile(t *testing.T, path string) []byte {
	t.Helper()
	bt, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("cannot read from %q: %v", path, err)
	}
	return bt
}
marshal, unmarshal?
- marshal: json 형태의 구조체 -> byte나 string 형태의 data로 encoding
- unmarshal: byte나 string 형태의 data -> json 형태의 구조체로 decoding
Section 67. Task를 등록하는 Endpoint 구현
- ToDo application에서 task를 등록하는 HTTP handler 구현
- POST /task로 오는 요청 처리
 - task title을 포함한 JSON request body를 반드시 지정해야 함
 - task가 성공적으로 등록되면 해당 task의 ID를 response body로 반환
 
 - AddTask type: http.HandlerFunc type을 충족하는 ServeHTTP method를 구현
 - request 처리가 정상 완료된 경우 RespondJSON을 사용해 JSON 응답을 반환
 - 오류가 발생한 경우 ErrResponse type에 정보를 포함시켜 RespondJSON을 사용해 JSON 응답을 반환
 
package handler
import (
	"encoding/json"
	"net/http"
	"time"
	"github.com/Imsyp/go_todo_app/chapter17/section65/entity"
	"github.com/Imsyp/go_todo_app/chapter17/section65/store"
	"github.com/go-playground/validator/v10"
)
type AddTask struct {
	Store		*store.TaskStore
	validator	*validator.Validate
}
func (at *AddTask) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	var b struct {
		Title string `json:"title" validate:"required"`
	}
	if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
		RespondJSON(ctx, w, &ErrResponse{
			Message: err.Error(),
		}, http.StatusInternalServerError)
		return
	}
	if err := at.Validator.Struct(b); err != nil {
		RespondJSON(ctx, w, &ErrResponse{
			Message: err.Error(),
		}, http.StatusBadRequest)
		return
	}
	t := &entity.Task{
		Title:		b.Title,
		Status: 	"todo",
		Created:	time.Now(),
	}
	id, err := store,Tasks.Add(t)
	if err != nil {
		RespondJSON(ctx, w, &ErrResponse{
			Message: err.Error(),
		}, http.StatusInternalServerError)
		return
	}
	rsp := struct {
		ID int `json:"id"`
	}{ID: int(id)}
	RespondJSON(ctx, w, rsp, http.StatusOK)
}
요청 바디 검증
- 해당 HTTP handler에서는 task title을 포함한 JSON request body를 필수로 요구
 - request body를 검증하기 위해 request body를 Unmarshal한 type의 값에 대해 if문을 반복해서 검증하는 방법도 존재
 - 하지만, JSON 구조가 방대하거나 JSON의 각 필드 조건이 복잡한 경우 JSON 로직만으로도 많은 로직 작성해야함, 경우에 따라 로직을 작성하지 않아 놓치는 필드가 발생
 - github.com/go-playground/validator: Unmarshal한 type에 validate라는 key를 태그로 사용해 해당 필드의 검증 조건을 설정할 수 있음
 - 설정한 조건은 *validator.ValidateStruct method로 검증할 수 있음
 - 정의 완료된 조건도 다수 있으며, 'ip 주소로 유효한가', '전화번호로 유효한가' 등의 검증도 간단히 정의 가능
 - 위 코드에서는 task title이 필수적 -> Title string 'json: "title" validate: "required"'라고 설정
 
Section 68. 테이블 주도 테스트와 골든 테스트를 조합한 테스트 코드
- TestAddTask 함수는 httptest 패키지를 사용해 HTTP handler에 request를 보내서 테스트 수행
 - tests 변수로 여러 개의 테스트 데이터 선언 -> t.Run method로 서브 테스트 실행
 - 테이블 주도 테스트(Table Driven Test): 여러 개의 입력 및 기댓값을 조합해서 공통화된 실행 순서로 테스트하는 패턴을 Go에서는 테이블 주도 테스트라 한다.
 - 골든 테스트(Golden Test): 테스트 입력값이나 기댓값을 파일에 저장한 테스트 -> 테스트 코드와 별도로 저장하는 데이터를 *.json.golden 형태로 저장하기 때문에 golden test라 부름
 
package handler
import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"testing"
	"github.com/Imsyp/go_todo_app/chapter17/section65/entity"
	"github.com/Imsyp/go_todo_app/chapter17/section65/store"
	"github.com/Imsyp/go_todo_app/chapter17/section65/testutil"
	"github.com/go-playground/validator/v10"
)
func TestAddTask(t *testing.T) {
	type want struct {
		status int
		rspFile string
	}
	tests := map[string]struct {
		reqFile string
		want	want
	}{
		"ok": {
			reqFile: "testdata/add_task/ok_req.json.golden",
			want: want{
				status: http.StatusOK,
				rspFile: "testdata/add_task/ok_rsp.json.golden",
			},
		},
		"badRequest": {
			reqFile: "testdata/add_task/bad_req.json.golden",
			want: want{
				status: http.StatusBadRequest,
				rspFile: "testdata/add_task/bad_rsp.json.golden"
			},
		},
	}
	for n, tt := range test {
		tt := tt
		t.Run(n, func(t *testing.T) {
			t.Parallel()
			w := httptest.NewRecorder()
			r := httptest.NewRequest(
				http.MethodPost,
				"/tasks",
				bytes.NewReader(testutil,LoadFile(t, tt.reqFile)),
			)
			sut := AddTask{Store: &store.TaskStore{
				Tasks: map[entity.TaskID]*entity.Task{},
			}, Validator: validator.New()}
			sut.ServeHTTP(w, r)
			resp := w.Result()
			testutil.AssertResponse(t,
				resp, tt.want.status, testutil.LoadFile(t, tt.want.rspFile),	
			)
		})
	}
	
}

-> IDE에서 *.json.golden 확장자를 JSON 파일로 인식하도록 설정해야 함
Section 70. HTTP Handler를 Routing으로 설정
- 위에서 구현한 AddTask type, ListTask type의 HTTP handler를 HTTP 서버의 엔드 포인트로 설정
 - problem -> 표준 패키지인 http.ServerMux type의 routing 설정이 쉽지 않음 -> 아래 구현이 어려움
- /users/10처럼 url에 포함된 경로 파라미터의 해석
 - GET /users와 POST /users같이 HTTP method의 차이에 의한 다른 HTTP handler 구현
 
 - 이런 이유로 open source를 이용하여 routing 구현
 
1. Github.com/go-chi/chi를 사용한 유연한 routing 설정
- github.com/go-chi/chi: go 언어에서 라우팅 기능을 제공하는 오픈소스
 - chi.NewRoudter 함수의 *chi.Mux 타입값은 http.Handler 인터페이스를 충족 -> NewMux 함수의 시그니처를 변경하지 않고 내부 구현을 변경
 
package main
import (
	"net/http"
	
	"github.com/Imsyp/go_todo_app/chapter17/section70/entity"
	"github.com/Imsyp/go_todo_app/chapter17/section70/store"
	"github.com/Imsyp/go_todo_app/chapter17/section70/testutil"
	"github.com/go-chi/chi/v5"
	"github.com/go-playground/validator/v10"
)
func NewMux() http.Handler {
	mux := http.NewRouter()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		_, _ = w.Write([]bute(`{"status": "ok"}`))
	})
	v := validator.New()
	mux.Handle("/tasks", &handler.AddTask{Store: store:Tasks, Validator: v})
	at := &handler.AddTask{Store: store.Tasks, Validator: v}
	mux.Post("/tasks", at.ServeHTTP)
	lt := &handler.ListTask{Store: store.Tasks}
	mux.Get("/tasks", lt.serveHTTP)
	return mux
}
Section 71. 동작 검증

참고자료
728x90
    
    
  반응형
    
    
    
  'GDGoC KHU 1th' 카테고리의 다른 글
| Elasticsearch 기본 개념 + ELK stack (0) | 2025.04.12 | 
|---|---|
| gRPC란? (2) | 2024.11.16 | 
| [Go 언어로 배우는 웹 애플리케이션 개발] Chapter 18. RDBMS를 사용한 데이터베이스 처리 구현하기 (0) | 2024.11.14 | 
| [백엔드를 위한 GO 프로그래밍] Chapter 6. 상호 호환성 (6) | 2024.10.07 |