본문 바로가기
Golang/Tucker

[묘공단] 6주차 (2)

by 윤원용 2023. 11. 12.

이 글은 골든래빗 《Tucker의 Go 언어 프로그래밍》의 29, 30장 써머리입니다.

 

29장과 30장은 Golang으로 http server를 만드는 전형적인 back-end 개발자가 상식으로 알고 있어야 하는 내용을 다루고 있다. 29장은 Golang의 내장 api로 만드는 방법을 알려주고 30장은 RESTful 이론의 설명과 외부 라이브러리를 사용해 http server를 만드는 내용을 알려준다. 책에서 나오는 내용 중 아쉬운 내용은 많으나 숙제라 생각하고 봐주면 좋을 것 같다.

 

더보기
  • 29. Go 언어로 만드는 웹 서버
    • 29.1 HTTP 웹 서버 만들기
    • 29.2 HTTP 동작 원리
    • 29.3 HTTP 쿼리 인수 사용하기
    • 29.4 ServeMux 인스턴스 이용하기
    • 29.5 파일 서버
    • 29.6 웹 서버 테스트 코드 만들기
    • 29.7 JSON 데이터 전송
    • 29.8 HTTPS 웹 서버 만들기
  • 30. RESTful API 서버 만들기
    • 30.1 해법
    • 30.2 사전 지식: RESTful API
    • 30.3 RESTful API 서버 만들기
    • 30.4 테스트 코드 작성하기
    • 30.5 특정 학생 데이터 반환하기
    • 30.6 학생 데이터 추가/삭제하기

 

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

시작에 앞서 책과는 조금 다른 진행 방식으로 리뷰해 보겠다. 오늘은 책에 나와있는 내용들을  위주로 리뷰할 예정이다.

HTTP Server라는 것에 대해 쉽게 말하면 HTTP(HyperText Transfer Protocol)라는 protocol(프로토콜)을 사용하여 A sever와 B server가 통신한다는 말인데 A server와 B server가 공통의 약속(법 같은 것)을 기반으로 통신에 필요한 데이터들의 포맷이나 방식을 약속한다는 뜻이다.  HTTP protocol은 비연결성이기 때문에 이전에 통신한 정보를 알지 못하고 A server와 B server가 한 번의 통신이 끝나면 연결을 끊고 다시 통신이 필요한 상황이 오면 다시 연결한다. 이때 통신의 시작을 요청(request)이라 하고 끝을 응답(response)라고 하며, 하나의 request는 하나의 response를 받는 것을 잊어버리면 안 된다. 이 두 단어는 앞으로 계속 사용하니 외우는 게 좋을 것 같다.

HTTP는 비연결성이 특징이다 보니 로그인 정보를 저장하거나 특정 사용자의 권한을 저장해서 사용해야 하는 로직에는 불편한 점이 많다. 이를 개선하기 위해 cookie(쿠키), session(세션)이라는 곳에 데이터를 저장하여 사용하면 비연결성의 단점을 해결할 수 있다. cookie 같은 경우에는 브라우저에 저장하는 데이터(정보)이고 session은 서버에 저장하는 cookie라 생각하면 된다. (cookie랑 session은 보안이나 서버 부하나 확장에 취약한 부분이 있기 때문에 JWT라는 기법을 사용해 해결하기도 한다.)

참고로 HTTP는 TCP/IP기반이며, HTTPS는 HTTP에 보안기능을 추가한 것으로 SSL(TLS)가 추가된 것이다. 이런 내용은 스타트업이 아니고서야 실무에서 구성할 일은 별로 없을 것이기 때문에 교양처럼 알고 싶을 때 공부하면 되지만 서버를 처음 만들 땐 알아야 하는 개념이니 공부해 보는 것도 나쁘지 않다.

HTTP든 HTTPS든 네트워크 관련 작업을 할 땐 기본적으로 알아야 할 내용이 있다.

  1. URL(Uniform Resource Locator)
    URL를 쉽게 말하면 복잡한 네트워크에서 본인들이 원하는 자원(웹 사이트, 파일 다운로드 등) 이 있는 곳을 식별할 수 있는 길라잡이라 생각하면 된다. 구조는 scheme://host:port/path {? query {&query}} 형식이다. 중괄호({})는 선택 사항이란 뜻이고 여기서 집중해야 할 부분은 scheme://host:port 이 부분이다. scheme는 http, https, ftp, smtp과 같은 protocol을 주로 사용한다. scheme 별로 기본 port 가 존재하는데, http는 80번 https 443번 ftp는 21번이다. port 같은 경우는 host에서 프로세스를 식별하기 위해 사용되는 번호이기 때문에 http에 3000번 포트를 사용하는 예를 들면 http://localhost:3000 이 되고 80번 포트를 사용하는 경우 http://localhost와 같이 생략할 수 있다. 이유는 http의 기본 port는 80번 이기 때문에 생략이 가능한 것이고 port가 겹치지 않으면 하나의 host에서 여러 프로세스를 port로 구분해 연결할 수 있다. 앞서 설명한 내용으로 host는 특정 서버를 가리키는 논리적 요소라는 것을 짐작했을 것이고 DNS에서 더 자세히 알아보겠다. 
  2. DNS(Domain Name System)
    앞서 설명한 URL에서 host부분이 있었을 것이다. host부분의 내용은 주로 사람이 이해하기 쉬운 영어로 만들어져 있고 이것을 domain이라 부르지만 컴퓨터 입장에선 무슨 뜻인지 알 수 없다. 때문에 domain을 컴퓨터가 알 수 있는 ip(internet protocol)로 변경해 주는 것이 DNS다. 예를 들어 https://google.com이라는 URL이 있으면 DNS에서 google.com이라는 도메인과 mapping(사상)된 8.8.8.8 ip에 요청한다. 사실 DNS를 깊이 파면 팔수록 재밌어지지만 너무 깊은 게 함정이다.
  3. HTTP Method
    이건 HTTP에만 해당되는 내용이다. (HTTP만 해당된다 해서 HTTPS는 해당 안된다 생각한다면 정말...) HTTP는 여러 동작을 지원하는데 Get, Post, Put, Delete, Fetch 등이 있다. 이 Method는 의미적으로 사용자끼리 api의 기능을 이해하기 쉽게 도와주는 역할을 하는 것이지 Method들 마다 특별히 강제하는 기능이 있는 것은 아니다. 하지만 이런 Method들의 의미적인 내용을 잘 사용하면 이점이 매우 많기 때문에 나온 것이 RESTful api다.

이제 본론으로 돌아와 Golang에서 내장 api를 사용하여 서버를 만들어 보자.

package main

import (
	// 1번
	"net/http"
)

func main() {

	// 2번
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		w.Write([]byte("hello world!!!"))
	})

	// 3번
	http.ListenAndServe(":3000", nil)
}

위 코드를 순번대로 설명하면

  1. http server를 만들기 위해 net/http 내장 package를 import 한다.
  2. http 객체의 HandleFunc 함수에 응답해야 하는 path와 path가 처리해야 할 로직을 매개변수로 넘겨준다.
    path라 하면 더 이해하기 쉬울 것 같았는데 헷갈릴 수 있으니 보강하면 앞서 설명한 URL에서 path가 여기에서 말하는 path이다. HandleFunc 함수의 정의에는 pattern이라 나오기 때문에 본인은 더 헷갈렸다.
  3. http 객체의 ListenAndServe 함수에 3000번 포트를 주시하도록 알려주는 address와 multiplex를 넘겨준다.
    ListenAndServe 함수는 이 앞장에 설명한 SOLID의 SRP를 지키지 못했다. ㅋㅋㅋ;;; 장난이고 여기서 address를 넘겨주는 부분에 host정보 없이 host와 port를 구분하기 위한 콜론(:)과 port만 넣어줬다. 기본 host는 localhost를 사용한다. multiplex는 책에 나온 내용이라 multiplex라 했지만 함수의 정의에는 handler라 나와있다.

Golang에서는 내장 api를 사용해 세 단계로 아주 쉽게 http server를 만들 수 있다. 이제 실행시켜 어떻게 동작하는지 확인해 보자.

코드를 빌드 후 실행하면 위 이미지처럼 main routine이 끝나지 않고 계속 살아있는 것을 볼 수 있다. main routine이 끝나면 process가 끝나는 것이니 server는 일회성이 될 것이다. main routine이 뭔지 모르면 이전에 리뷰한 내용을 보고 오자.

실행이 정상적으로 됐으면 browser(크롬, 엣지, 파이어폭스 등등)나 reuqest tools(curl, postman 등등)을 사용해 결과를 확인해 보자. 

위 이미지처럼 hello world!!! 가 출력된다. 이제 hello world!!! 가 어떻게 출력되는지 상세하게 확인해 볼 건데 앞서 설명한 request와 response는 짝꿍이라는 것을 다시 기억하자.

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	// response 1번
	w.WriteHeader(200)
	// response 2번
	w.Write([]byte("hello world!!!"))
})
  1. WriteHeader
    이 함수의 기능은 request의 응답 코드를 명시하는 함수고 보통은 200이 아닌 http.StatusOK라는 상수를 사용하는 것이 맞다. 이 말은 즉 http 객체에서 여러 상태 코드를 상수로 갖고 있며, 우리가 만드는 애플리케이션에 request가 오면 상황에 따라 response의 statusCode를 알맞게 보낼 수 있다. 지금은 간단한 코드라 200을 썼지만 상황에 따라 404가 될 수 도 있고 500이 될 수도 있을 것이다.
  2. Write
    이 함수는 request에 대한 response에 특정 데이터를 바인딩해 전송할 수 있게 도와주는 함수이다. request든 response든 body라는 것이 존재하는데 key: value 형식으로 데이터를 저장하여 전송할 수 있게 도와주는 역할을 한다.  body에 무제한으로 데이터를 저장하여 전송할 순 없고 제한이 있기 때문에 찾아보는 것도 나쁘지 않을 것 같다.
    Golang에서는 Write함수의 인수로 []byte 형태의 데이터를 넣어줘야 한다.

기본적인 내용은 여기서 끝이고 응용할 차례이다.

첫 번째로 request에서 QueryParameter를 추출하여 사용해 보자.

QueryParameter는 주로 Get Method에서 사용하고 몇 가지 재한사항이 있다. browser마다 다른 부분들이 있지만 용량의 제한이 있고 URL이 많이 더러워진다. 즉 URL에 전부 보이기 때문에 사용자의 비밀번호 같은 데이터를 QueryParameter를 사용해 server에 전송하는 것은 충격과 공포이지만 가능하다. 이렇듯 보안에 취약하기 때문에 사용자를 특정할 수 있는 값이나 보안을 중시해야 하는 데이터인 경우에는 사용하면 안 되고 노출해도 문제없을 만한 간단한 데이터를 전송하기엔 좋다. 형식은 key=value로 이뤄지고 첫 QueryParameter는 ? 기호를 사용하고 아 후 QueryParameter들은 & 기호를 사용하여 구분한다. 예를 들면 scheme://host:port/path?name=wonyong&age=31 형식이고 value들은 전부 문자열로 취급되기 때문에 age(key)의 value인 31은 정수가 아닌 문자열임을 잊으면 안 된다. 이전 예제코드를 조금 수정해서 name와 age라는 QueryParameter를 받아 출력하는 코드로 만들어보자.

package main

import (
	"fmt"
	"net/http"
)

func main() {

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

		// 1번
		querys := r.URL.Query()
		// 2번
		name := querys.Get("name")
		ageStr := querys.Get("age")
		ageStr2 := querys.Get("Age")

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(fmt.Sprintf("name: %s, age: %s, Age: %s", name, ageStr, ageStr2)))
	})

	http.ListenAndServe(":3000", nil)
}

위 코드를 설명해 보면

  1. request 객체는 / path로 요청이 올 때 요청의 정보들을 내장 api에서 알맞게 구성해 우리의 작성한 함수에 넣어주기 때문에 r.URL.Query 함수를 호출하면 ? 기호부터 시작하는 정보들을 추출한 객체를 반환해 준다.
  2. 1번의 결과인 querys 객체를 참조해 Get함수에 QueryParameter로 넘긴 key들을 사용해 value를 찾을 수 있다. 이때 주의해야 하는 점은 key의 값은 hash 개념이기 때문에 대소문자를 구분한다. 즉 name, age는 값을 찾을 수 있지만 Age는 찾을 수 없기 때문에 빈문자열을 반환한다. Get함수는 잘못된 key값을 넣어도 빈문자열을 반환해 주기 때문에 꼭 값을 확인하는 코드를 넣어야 한다.

reuqest 객체에서 QueryParameter로 넘기 데이터들을 찾아 출력하는 예제의 결과를 확인해 보자.

 

두 번째로 request에서 Path를 추출하여 사용해 보자.

RESTful API를 만들 때 많이 사용하는 방식이고 비개발자들도 이해하기 쉬운 URL 구조가 장점이기도 한 path를 통해 값을 갖고 오는 방식을 알아보자. 책에서는 외부라이브러리를 사용해 설명한다..

내장 api는 path를 구분해서 handler를 호출해 주는 기능은 없기 때문에 해당 기능을 코딩해야 한다.

일단 왜 안되는지 확인 후 코딩해 보자.

package main

import (
	"fmt"
	"net/http"
)

func main() {
	// 1번
	http.HandleFunc("/*/*", func(w http.ResponseWriter, r *http.Request) {
		paths := r.URL.Path
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(fmt.Sprintf("pattern: /*/*, paths: %s", paths)))
	})

	// 2번
	http.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
		paths := r.URL.Path
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(fmt.Sprintf("pattern: /*, paths: %s", paths)))
	})

	// 3번
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		paths := r.URL.Path
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(fmt.Sprintf("pattern: /, paths: %s", paths)))
	})

	// 4번
	http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
		paths := r.URL.Path
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(fmt.Sprintf("pattern: /test, paths: %s", paths)))
	})

	http.ListenAndServe(":3000", nil)
}

위와 같은 코드가 있을 때 1번과 2번은 작동을 안 한다. 그 이유는 HandleFunc의 구현 함수의 내용을 살펴보면 알 수 있는데 첫 번째 인수인 pattern(path)를 key로 설정하고 두 번째 인수인 로직을 처리하는 함수는 value로 설정하는 map 형태의 데이터 스트럭처를 사용하기 때문에 별표(*)와 같은 특수문자의 기능은 없다.

 위 이미지는 HandleFunc 함수의 내부 구현 내용이고 빨간 밑줄 부분이 앞서 말한 내용이다. 그렇기 때문에 path를 추출해서 함수를 호출하도록 기능을 구현해야 한다. 간단하게 구현해 보자.

일단 선행지식이 필요한데 나만의 Handle 함수를 구현하거나 아니면 덕타이핑의 이점을 사용할 수 있도록 ServeMux의 정의에 있는 함수들을 모두 구현한 구조체를 구현해야 한다. 본인은 간단하게 끝내기 위해 나만의 Handle 함수를 구현하겠다.

package main

import (
	"fmt"
	"net/http"
	"strings"
)

type handlerFunc = func(r *http.Request) ([]byte, error)

var handlerTable = make(map[string]handlerFunc)

func main() {
	handlerTable["/"] = rootHandler
	handlerTable["/users"] = usersHandler

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		statusCode, responseBuff := routing(r)
		w.WriteHeader(statusCode)
		w.Write([]byte(responseBuff))
	})

	http.ListenAndServe(":3000", nil)
}

func routing(r *http.Request) (statusCode int, responseBuff []byte) {
	statusCode = http.StatusInternalServerError
	responseBuff = []byte("server error")
	path := r.URL.Path
	if !strings.Contains(path, ":") {
		handler, exists := handlerTable[path]
		if !exists {
			statusCode = http.StatusNotFound
			responseBuff = []byte(fmt.Sprintf("not found handler. path: %s", path))
			return
		}
		buff, err := handler(r)
		if err != nil {
			return
		}
		statusCode = http.StatusOK
		responseBuff = buff
		return
	}

	paths := strings.Split(r.URL.Path, "/")

	responseBuff = []byte(strings.Join(paths, ", "))
	return statusCode, responseBuff
}

func rootHandler(r *http.Request) ([]byte, error) {
	return []byte("hello world!!!"), nil
}

func usersHandler(r *http.Request) ([]byte, error) {
	return []byte("users"), nil
}

간단한 예제를 만들기 위해 이상한 부분이 있지만 path를 기준으로 handlerTable 변수에 저장된 handler를 찾아 있으면 호출하고 없으면 not found를 반환하도록 만들었다. 이제 path에 콜론(:) 기호가 있으면 그 부분을 잘라 handler 함수에 인자로 전달할 수 있도록 수정해 보겠다. 본인이 만든 예제 코드를 세세하게 설명하진 않지만 궁금하면 댓글을 달아 물어보길 바란다.

type (
	handlerFunc      func(r *http.Request) (int, []byte, error)
	handlerTableType map[string]handlerFunc
)

var handlerTable = make(handlerTableType)

func (ht handlerTableType) Regist(path string, handler handlerFunc) {

	handlerKey := ht.GetPathByHandlerKey(path)

	if ht.DuplicatePathCheck(handlerKey) {
		panic(fmt.Sprintf("duplicate handler path: %s", path))
	}

	ht[handlerKey] = handler
}

func (ht handlerTableType) GetPathByHandlerKey(path string) string {
	handlerKey := path
	if ht.PathParamCheck(path) {
		handlerKey = ht.GetPathParamHandlerKey(path)
	}
	return handlerKey
}

func (ht handlerTableType) DuplicatePathCheck(handlerKey string) bool {
	_, exists := ht[handlerKey]
	return exists
}

func (ht handlerTableType) PathParamCheck(path string) bool {
	return strings.Contains(path, ":")
}

func (ht handlerTableType) GetPathParamHandlerKey(path string) string {
	pathParams := strings.Split(path, ":")
	return fmt.Sprintf("%s:%d", pathParams[0], len(pathParams)-1)
}

func (ht handlerTableType) GetPathByHandlerFunc(path string) handlerFunc {
	handlerFunc, exists := ht[path]
	if exists {
		return handlerFunc
	}

	paths := strings.Split(path, "/")
	temp := make([]string, 0)
	start := len(paths) - 1

	for start > -1 {
		temp = append(temp, paths[start])
		handlerKey := strings.Join(paths[0:start], "/")
		pathParamHandlerKey := fmt.Sprintf("%s/:%d", handlerKey, len(temp))
		hander, exists := ht[pathParamHandlerKey]
		if exists {
			return hander
		}
		start--
	}

	return func(r *http.Request) (int, []byte, error) {
		return http.StatusNotFound, []byte(fmt.Sprintf("not found handler. path: %s", path)), nil
	}
}

위 코드는 url path에서 path parameter를 구분하여 handlerParamter를 저장할 별도의 key를 생성하여 handlerTable에 저장한다. 그 후 호출될 때 path에 따라 알맞은 handler를 반환하는 코드이다. 만약 path에 알맞은 handler가 없을 경우 not found handler를 반환하도록 만들었다.

이제 handlerTable 객체를 사용하는 코드를 보자.

func main() {

	handlerTable.Regist("/", rootHandler)
	handlerTable.Regist("/users", usersHandler)
	handlerTable.Regist("/users/:id", usersPathParamsHandler)
	handlerTable.Regist("/users/:id/:name", usersPathParamsHandler2)

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		statusCode, responseBuff := routing(r)
		w.WriteHeader(statusCode)
		w.Write([]byte(responseBuff))
	})

	http.ListenAndServe(":3000", nil)
}

func routing(r *http.Request) (statusCode int, responseBuff []byte) {
	path := r.URL.Path
	handler := handlerTable.GetPathByHandlerFunc(path)
	var err error = nil
	statusCode, responseBuff, err = handler(r)
	if err != nil {
		statusCode = http.StatusInternalServerError
		responseBuff = []byte("server error")
	}
	return
}

func rootHandler(r *http.Request) (int, []byte, error) {
	return http.StatusOK, []byte("hello world!!!"), nil
}

func usersHandler(r *http.Request) (int, []byte, error) {
	return http.StatusOK, []byte("users"), nil
}

func usersPathParamsHandler(r *http.Request) (int, []byte, error) {
	paths := strings.Split(r.URL.Path, "/")
	return http.StatusOK, []byte(fmt.Sprintf("path: %s, pathsParams: %s", r.URL.Path, strings.Join(paths[2:], ","))), nil
}

func usersPathParamsHandler2(r *http.Request) (int, []byte, error) {
	paths := strings.Split(r.URL.Path, "/")
	return http.StatusOK, []byte(fmt.Sprintf("path: %s, pathsParams: %s", r.URL.Path, strings.Join(paths[2:], ","))), nil
}

위 코드의 main함수에서 /, /users, /users/:id, /users/:id/:name path들을 handlerTable에 Regist 하고 routing 함수에서 path를 handlerTable.GetPathByHandlerFunc 함의 인자로 넘겨 결과로 알맞은 handler를 호출하고 있다.

이제 실행 결과를 보면

위 이미지처럼 path parameter에 맞는 handler를 호출한 후 나온 결과가 browser에 잘 표출되고 있는 것을 확인할 수 있다. 개념을 이해하도록 간단하게 만든 코드지만 실무나 포트폴리오를 만들 땐 기능이 훨씬 풍부하고 여러 경험을 통해 잘 만들어진 echo, gin과 같은 외부 라이브러리를 갖다 쓰도록 하자.

 

책에는 더 좋은 내용들이 많으니 책을 사서 읽어보길 추천한다. 테스트하는 코드도 있고 본인이 만든 예제보다 더 좋은 예제들과 https에 대해 더 자세히 알려준다.

 

30. RESTful API 서버 만들기

본인은 문제만 풀고 개념은 skip!!! 하겠다. 책에는 RESTful API에 대해 잘 설명하고 있으니 사서 읽어보삼 ㅋㅋ;;;

문제는 HTTP Method의 종류에 맞도록 학생 정보를 관리하는 RESTful API를 만드는 것이다. 책에서 gorilla/mux 외부 라이브러리를 사용하기 때문에 본인도 사용하겠다.

핵심 적인 코드만 간단하게 리뷰하고 나머지는 예제코드가 있는 github에 가서 확인하면 좋을 것 같다.

package main

import (
	student_router "gt/chap30/ex/student/router"
	"net/http"

	"github.com/gorilla/mux"
)

func main() {
	mux := mux.NewRouter()
	mux.Use(MimeContentTypeJSONMiddleware)
	student_router.Load(mux)
	http.ListenAndServe(":3000", mux)
}
func MimeContentTypeJSONMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")

		next.ServeHTTP(w, r)
	})
}

main.go 파일의 내용이다. 본인은 response 객체의 Header를 매번 Set함수를 호출하기 싫어서 Middleware에서 Content-Type을 application/json 형식으로 변경하는 코드를 넣었고 나머지는 이전과 똑같은 개념이다.

package student_router

import (
	student_handler "gt/chap30/ex/student/handler"
	"net/http"

	"github.com/gorilla/mux"
)

func Load(router *mux.Router) {
	router.HandleFunc("/student", student_handler.List).Methods(http.MethodGet)
	router.HandleFunc("/student/{id}", student_handler.Read).Methods(http.MethodGet)
	router.HandleFunc("/student", student_handler.Regist).Methods(http.MethodPost)
	router.HandleFunc("/student/{id}", student_handler.Modify).Methods(http.MethodPut)
	router.HandleFunc("/student/{id}", student_handler.Destroy).Methods(http.MethodDelete)
}

main.go 파일에서 student_router.Load 함수의 구현 내용이다. 여기서 주위 깊게 봐야 하는 부분은 Methods 함수에 인수로 넣은 http.Method***이다. RESTful은 HTTP Method를 활용하기 때문에 위 코드처럼 api별로 Method를 구분한다.

student_handler package의 내용들을 너무 간단해 건너뛰고 student.go 파일을 보는 것으로 끝내겠다.

package student

import "github.com/pkg/errors"

type (
	Student struct {
		Id    string `json:"id"`
		Name  string `json:"name"`
		Age   int64  `json:"age"`
		Score int64  `json:"score"`
	}

	studentManager struct {
		info map[string]*Student
	}
)

var Manager *studentManager

func init() {
	Manager = new(studentManager)
	Manager.info = make(map[string]*Student)
	Manager.info["1"] = &Student{
		Id:    "1",
		Name:  "A",
		Age:   20,
		Score: 84,
	}

	Manager.info["2"] = &Student{
		Id:    "2",
		Name:  "B",
		Age:   20,
		Score: 70,
	}
}

func (m *studentManager) FindAll() []Student {
	list := make([]Student, 0, len(m.info))
	for _, student := range m.info {
		list = append(list, *student)
	}
	return list
}

func (m *studentManager) Find(id string) (Student, error) {
	student, exists := m.info[id]
	if exists {
		return *student, nil
	}

	return Student{}, errors.Errorf("student id: %s not found ", id)
}

func (m *studentManager) Insert(student *Student) error {
	_, err := m.Find(student.Id)
	if err == nil {
		return errors.Errorf("duplicate student id: %s", student.Id)
	}
	m.info[student.Id] = student
	return nil
}

func (m *studentManager) Update(student *Student) error {
	oldStudent, err := m.Find(student.Id)
	if err != nil {
		return err
	}

	if len(student.Name) > 0 {
		oldStudent.Name = student.Name
	}

	if student.Age > 0 && oldStudent.Age != student.Age {
		oldStudent.Age = student.Age
	}

	if student.Score > 0 && oldStudent.Score != student.Score {
		oldStudent.Score = student.Score
	}
	m.info[student.Id] = &oldStudent
	return nil
}

func (m *studentManager) Delete(id string) error {
	_, err := m.Find(id)
	if err != nil {
		return err
	}
	delete(m.info, id)
	return nil
}

예전에 리뷰했던 init 함수를 통해 main 함수가 실행되기 전 student package의 Manager 변수를 초기화하고 기본 Student들을 넣는 코드이다. 나머지는 함수명과 기능이 동일하니 모르는 게 있으면 댓글을 달아주면 된다.

 

요즘 회사일로 바쁘다 보니 공부를 너무 안 해서 저번주에 해야 하는 리뷰를 이번주에 했다. ㅠㅠ;; 사실 todo list를 만드는 내용을 이번주에 리뷰했어야 했는데... 다음 주에 짬짬이 시간 내서 리뷰를 작성하여 마무리하겠뜨아!!!

날씨도 추워졌으니 다들 감기 조심하시고 다음 주에 봅시돵~ ㅋㅋㅋ

'Golang > Tucker' 카테고리의 다른 글

[묘공단] 7주차 (1)  (0) 2023.11.17
[묘공단] 6주차 (1)  (0) 2023.11.05
[묘공단] 5주차 (3)  (0) 2023.10.29
[묘공단] 5주차 (2)  (0) 2023.10.29
[묘공단] 5주차 (1)  (1) 2023.10.29