let's go를 배워보자 3일차 - Configuration and Error Handling

0

lets-go

목록 보기
3/15

Configuration and Error Handling

1. Managing Configuration Settings

우리는 여태까지 code에 여러가지 환경 변수들을 하드코딩 해놓고 개발해왔다.

  1. 서버에서 listen 중인 netowrk address인 :4000
  2. static file의 경로인 ./ui/static

여기에는 code와 configuration의 분리가 필요하다. 분리를 통해 해당 값들의 런타임 동안에 변화가 없어야 하거나, 사람들에게 코드 상으로 노출되지 않아야 하기도 해야한다.

configuration setting들을 관리하기 위해서 command-line flag를 사용하는 것은 매우 많이 사용되는 방법이다.

go run ./... -addr=":80"

-addr이라는 옵션으로 주어진 ":80이라는 network address port를 코드에서 가져오기 위해서는 다음과 같이 사용하면 된다.

addr := flag.String("addr", ":4000", "HTTP network address")

addr이라는 이름의 flag를 만들겠다는 것이고, 이 값의 default 값은 ":4000"이며, 짧은 설명은 help부분으로 어떤 flag인지를 알려주는 부분이다. 여기서는 help 내용으로 HTTP network address이라는 것을 알려주는 것이다. 해당 flag의 값은 addr변수에 할당된다. 단, *string타입으로 반환된다.

  • main.go
package main

import (
	"flag" // New import
	"log"
	"net/http"
)

func main() {
	// Define a new command-line flag with the name 'addr', a default value of ":4000"
	// and some short help text explaining what the flag controls. The value of the
	// flag will be stored in the addr variable at runtime.
	addr := flag.String("addr", ":4000", "HTTP network address")
	// Importantly, we use the flag.Parse() function to parse the command-line flag.
	// This reads in the command-line flag value and assigns it to the addr
	// variable. You need to call this *before* you use the addr variable
	// otherwise it will always contain the default value of ":4000". If any errors are
	// encountered during parsing the application will be terminated.
	flag.Parse()
	mux := http.NewServeMux()
	mux.HandleFunc("/", home)
	mux.HandleFunc("/snippet", showSnippet)
	mux.HandleFunc("/snippet/create", createSnippet)
	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	// The value returned from the flag.String() function is a pointer to the flag
	// value, not the value itself. So we need to dereference the pointer (i.e.
	// prefix it with the * symbol) before using it. Note that we're using the
	// log.Printf() function to interpolate the address with the log message.
	log.Printf("Starting server on %s", *addr)
	err := http.ListenAndServe(*addr, mux)
	log.Fatal(err)
}

이제 -addr 옵션을 넣어주어 잘 동작하는 지 확인해보자.

go run ./cmd/web -addr=":9999"
2022/12/08 17:36:37 Starting server on :9999

curl localhost:9999

서버의 응답을 확인할 수 있을 것이다.

addr := flag.String("addr", ":4000", "HTTP network address")

현재는 flag.String()만을 사용했지만, flag.Int(), flag.Bool()등 다양한 타입의 대한 type conversion들이 있다. 이를 통해 잘못된 입력이 주어졌을 때 런타임 중에 에러를 반환할 수 있도록 한다. 참고로boolean타입의 경우는 flag에 truefalse`를 넣어주면 된다.

go run example.go -flag=true
go run example.go -flag

만약 아무것도 -flag 뒤에 쓰지 않았다면 이는 false를 의미한다.

help 부분을 호출하는 방법은 다음과 같다.

go run ./cmd/web -help
Usage of /tmp/go-build2289204724/b001/exe/web:
  -addr string
        HTTP network address (default ":4000")

자동으로 help함수 기능을 해주어 좋다.

Environment를 통해서 값을 가져오는 방법도 있다. 이는 os.Getenv()를 통해 사용할 수 있다.

addr := os.Getenv("SNIPPETBOX_ADDR")

SNIPPETBOX_ADDR env값을 가져오는 것이다. 그러나 이렇게 사용하는 것은 command-line flag를 사용하는 것보다 더 위험하다. 이유는 default value를 설정하지 못하여 만약 값이 없다면 empty string("")이 나오고, -help함수도 자동으로 설정되지 않는다. 또한 os.Getenv()의 반환값은 항상 문자열이다. 즉, 자동적으로 type conversion을 수행하지 못한다는 것이다.

대신에 이 둘을 함께 사용해서 더 좋은 방법을 만들 수 있다.

export SNIPPETBOX_ADDR=":9999"
go run ./cmd/web -addr=$SNIPPETBOX_ADDR

command-line flag에 environment variable을 넘기는 것이다.

flag에는 이미 있는 변수의 포인터를 받아 자동으로 할당해주는 방법도 존재한다. 이는 flag.StringVar(), flag.IntVar(), flag.BoolVar() 등이 있다.

type Config struct {
    Addr string
    StaticDir string
}
...
cfg := new(Config)
flag.StringVar(&cfg.Addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.StaticDir, "static-dir", "./ui/static", "Path to static assets")
flag.Parse()

Config 타입의 인스턴스인 cfg를 만들고 cfg의 값을 command-line flag로 채우는 방법이다.

2. Leveled Logging

우리는 여태까지 log messageslog.Printf, log.Fatal 메서드를 사용하였다.

두 메서드 모두 기본적으로 local date와 시간이 형식화된 메시지를 전송한다. 또한, log.Fatal 메서드는 메시지를 쓴 후에 os.Exit(1)를 호출한다. 이는 application이 즉시 종료되게 만든다.

우리는 우리의 log message를 분명한 두 개의 타입으로 나눴었다. 하나는 informational messages이고, 하나는 error message이다.

log.Printf("Starting server on %s", *addr) // informational message
err := http.ListenAndServe(*addr, mux)
log.Fatal(err) // Error message

leveled logging 기능을 추가함으로서 우리의 application을 개선시켜 보자, 때문에 우리의 informational message와 error message들은 조금 다르게 관리될 것이다. 구체적으로,

  • 우리는 informational message들에 INFO라고 접두사를 추가할 것이고, 메시지를 stdout으로 전달할 것이다.
  • 우리는 error message들에 ERROR라는 접두사를 추가할 것이고, 메시지를 stderr로 전달할 것이다. 그리고 해당 에러와 관련된 파일 이름과 라인 번호를 추가하여 디버깅에 도움이 되도록 할 것이다.

위 방법을 구현하기 위해서는 여러가지 방법이 있지만, 가장 간단하고 깔끔한 방법으로는 log.New() 함수를 사용하여 새로운 커스텀 logger를 만드는 방법이 있다.

main.go 함수로가서 새로운 logger를 추가해보자.

  • main.go
package main

import (
	"flag" // New import
	"log"
	"net/http"
	"os"
)

func main() {
	addr := flag.String("addr", ":4000", "HTTP network address")
	flag.Parse()
	// Use log.New() to create a logger for writing information messages. This takes
	// three parameters: the destination to write the logs to (os.Stdout), a string
	// prefix for message (INFO followed by a tab), and flags to indicate what
	// additional information to include (local date and time). Note that the flags
	// are joined using the bitwise OR operator |.
	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
	// Create a logger for writing error messages in the same way, but use stderr as
	// the destination and use the log.Lshortfile flag to include the relevant
	// file name and line number.
	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)

	mux := http.NewServeMux()
	mux.HandleFunc("/", home)
	mux.HandleFunc("/snippet", showSnippet)
	mux.HandleFunc("/snippet/create", createSnippet)

	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	// The value returned from the flag.String() function is a pointer to the flag
	// value, not the value itself. So we need to dereference the pointer (i.e.
	// prefix it with the * symbol) before using it. Note that we're using the
	// log.Printf() function to interpolate the address with the log message.
	infoLog.Printf("Starting server on %s", *addr)
	err := http.ListenAndServe(*addr, mux)
	errorLog.Fatal(err)
}

실행해보도록 하자,

INFO    2022/12/12 10:42:35 Starting server on :4000
ERROR   2022/12/12 10:42:35 main.go:30: listen tcp :4000: bind: address already in use
exit status 1

다음과 같은 log가 나왔다면 성공한 것이다.

어떻게 메시지들이 다르게 prefixed되었는 지 살펴보자, 이들은 log level을 표시하게 되어 log들을 구분하기가 쉬워졌고, 날짜도 나온다. ERROR의 경우는 어떤 파일에 몇번째 라인에서 문제가 발생했는 지도 알 수 있다.

만약 단순한 file name 대신에, log output의 full file path를 포함하고 싶다면 log.Llongfile flag를 log.Lshortfile 대신에 사용하면 된다. 또한 UTC datetimes를 강제하고 싶다면, log.LUTC flag를 사용하면 된다.

3. Decoupled Logging

stdout, stderr에 전송하는 메시들을 logging하는 가장 큰 이점은 우리의 application과 logging이 decoupled되었다는 것이다. 우리의 application 그자체는 routing 또는 log의 storage과 관련이 없으며, 이는 environment에 따라 log들을 관리하기가 더 쉽다는 것을 의미한다.

development 중에는 log output이 terminal에서 stdout으로 나오기 때문에 보기쉽다.

staging 또는 production 환경에서는 stdout과 같은 stream들을 아카이브와 관찰하기 위한 최종 목적지로 리다이렉트 시킬 수 있다. 이 최종 목적지는 disk 파일들일 수도 있고, 또는 Splunk와 같은 logging 서비스들 일 수도 있다. 이와 같이 log들의 최종 목적지는 application과 독립적으로 실행 환경(environment)에 의해 관리되어 질 수 있다.

가령, stdout과 stderr 스트림을 disk 파일로 다음과 같이 전달 할 수 있다.

go run ./cmd/web >> /tmp/info.log 2>>/tmp/error.log

>>은 현재 존재하는 파일에 log를 append하는 기능이다. 만약 이전 파일을 삭제하고 싶다면 >을 사용하면 된다.

4. The http.Server Error Log

go의 http에서 에러가 발생한다면 해당 error는 standard logger를 사용하여 log할 것이다. 일관성을 위해서 이 부분도 errorLog를 대신 사용하게 만드는 것이 더 좋다.

http.ListenAndServe() 대신에, 이를 위해서 http.Serve 구조체에 configuration setting들을 우리의 서버에 initialize하도록 하자.

  • main.go
package main

import (
	"flag" // New import
	"log"
	"net/http"
	"os"
)

func main() {
	addr := flag.String("addr", ":4000", "HTTP network address")
	flag.Parse()

	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)

	mux := http.NewServeMux()
	mux.HandleFunc("/", home)
	mux.HandleFunc("/snippet", showSnippet)
	mux.HandleFunc("/snippet/create", createSnippet)

	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	// Initialize a new http.Server struct. We set the Addr and Handler fields so
	// that the server uses the same network address and routes as before, and set
	// the ErrorLog field so that the server now uses the custom errorLog logger in
	// the event of any problems.
	srv := &http.Server{
		Addr:     *addr,
		ErrorLog: errorLog,
		Handler:  mux,
	}

	infoLog.Printf("Starting server on %s", *addr)
	// Call the ListenAndServe() method on our new http.Server struct.
	err := srv.ListenAndServe()
	errorLog.Fatal(err)
}

참고로 log.New()로 만들어진 logger들은 concurrency-safe하다. 즉, 여러 개의 goroutine이 single logger 하나를 가지고 사용할 수 있다는 것이다. 따라서 race condition을 걱정할 필요가 없다.

이는, 만약 같은 목적지에 대해서 여러 개의 logger들이 log를 남기고 있다면 반드시 해당 목적지에 대해서 Write() 메서드가 동시성에 대해서 안전한 지 보장하고, 조심해야한다.

logging한 내용을 파일에 저장할 수도 있는데, 다음과 같다.

f, err := os.OpenFile("/tmp/info.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
    log.Fatal(err)
}

defer f.Close()

infoLog := log.New(f, "INFO\t", log.Ldata|log.Ltime)

다음과 같이 file에 logging하는 방법을 추천한다. 이렇게하면 여러 개의 log를 stdout이 아니라 파일에 저장할 수 있는 장점이 있다.

5. Dependency Injection

아직 logging에 대한 문제가 하나 있는데, handlers.go 파일을 열어보면 home 햄들러에 아직도 log.Println이 사용되는 것을 확인할 수 있다.

그런데 우리가 만든 errorLog logger를 어떻게 home handler에서 사용할 수 있도록 만들 수 있을까??

단순 logger의 문제를 떠나서 대부분의 web application에서 핸들러들은 여러 의존성들을 가지는데, database connection pool이나 중앙화된 error handlers, template cache들이 있다. 그럼 이러한 의존성들을 어떻게 핸들러에서 사용할 수 있도록 만들 수 있을까??

가장 간단한 방법은 전역 변수로 선언하는 방법이다. 그러나, 일반적으로 가장 좋은 방법으로 뽑히는 것은 handler로 inject dependencies이다. 이는 전역변수를 사용하는 것보다 코드를 매우 명백하게(explicit) 만들어주고, test에 용이해지며 error를 밝히기 좋다.

application 구조체를 만들어서 의존성을 관리하고 handler를 가지고 있도록 만들자. main.goapplication 구조체를 다음과 같이 만들도록 하자.

  • main.go
type application struct {
	errorLog *log.Logger
	infoLog  *log.Logger
}

그리고 handlers.go 파일을 업데이트하여 함수들을 application구조체의 메서드로 변경하도록 하자.

  • handlers.go
package main

import (
	"fmt"
	"html/template"
	"net/http"
	"strconv"
)

// Change the signature of the home handler so it is defined as a method against
// *application.
func (app *application) home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}

	files := []string{
		"./ui/html/home.page.tmpl",
		"./ui/html/base.layout.tmpl",
		"./ui/html/footer.partial.tmpl",
	}

	ts, err := template.ParseFiles(files...)
	if err != nil {
		// Because the home handler function is now a method against application
		// it can access its fields, including the error logger. We'll write the log
		// message to this instead of the standard logger.
		app.errorLog.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}
	err = ts.Execute(w, nil)
	if err != nil {
		// Also update the code here to use the error logger from the application
		// struct.
		app.errorLog.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

// Change the signature of the showSnippet handler so it is defined as a method
// against *application.
func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil || id < 1 {
		http.NotFound(w, r)
		return
	}
	fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

// Change the signature of the createSnippet handler so it is defined as a method
// against *application.
func (app *application) createSnippet(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", http.MethodPost)
		http.Error(w, "Method Not Allowed", 405)
		return
	}
	w.Write([]byte("Create a new snippet..."))
}

app.errorLog.Println(err.Error())을 주목하자. log패키지에서 사용한 logger를 우리가 만든 custom logger로 변경하였다. 이렇게 우리의 의존성을 주입(inject dependency)한 것이다.

이제 main.go 파일에서 application 구조체의 인스턴스를 만들어주도록 하자.

  • main.go
package main

import (
	"flag" // New import
	"log"
	"net/http"
	"os"
)

type application struct {
	errorLog *log.Logger
	infoLog  *log.Logger
}

func main() {
	addr := flag.String("addr", ":4000", "HTTP network address")
	flag.Parse()

	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)

	app := &application{
		errorLog: errorLog,
		infoLog:  infoLog,
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", app.home)
	mux.HandleFunc("/snippet", app.showSnippet)
	mux.HandleFunc("/snippet/create", app.createSnippet)

	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	// Initialize a new http.Server struct. We set the Addr and Handler fields so
	// that the server uses the same network address and routes as before, and set
	// the ErrorLog field so that the server now uses the custom errorLog logger in
	// the event of any problems.
	srv := &http.Server{
		Addr:     *addr,
		ErrorLog: errorLog,
		Handler:  mux,
	}

	infoLog.Printf("Starting server on %s", *addr)
	// Call the ListenAndServe() method on our new http.Server struct.
	err := srv.ListenAndServe()
	errorLog.Fatal(err)
}

전역변수를 사용하는 것보다도 이렇게 logger를 사용하는 방식이 프로그램이 커지고, 테스트가 많아지면서 훨씬 더 가치있게 될 것이다.

6. Adding a Deliberate Error

의도적인 에러를 하나 추가하여 application이 잘 동작하는 지 확인해보자.

가장 간단한 방법으로 template 파일의 위치를 변경하도록 하자, ui/html/home.page.tmplui/html/home.page.bak으로 옮기자. 우리가 application을 구동하고 home page를 요청한다면 이는 ui/html/home.page.tmpl가 더이상 없기 때문에 에러를 발생시킬 것이다.

mv ui/html/home.page.tmpl ui/html/home.page.bak
go run ./cmd/web

curl localhost:4000/

클라이언트에게는 Internal Server Erorr가 발생하고 터미널에서는 다음과 같은 로그가 발생한다.

INFO    2022/12/12 11:48:08 Starting server on :4000
ERROR   2022/12/12 11:48:25 handlers.go:29: open ./ui/html/home.page.tmpl: no such file or directory

커스텀 로깅이 핸드러에서도 잘 된 것을 확인할 수 있다. 이는 main.go에서만든 errorLog 인스턴스가 핸들러에 잘 주입되었기 때문에 가능한 일이다.

7. Centralized Error Handling

일부 error handling 코드를 helper 메서드들로 옮김으로서 우리의 application을 정리해보자. 이는 separate our concerns에 도움을 주고, 빌드를 통해서 우리가 개발하면서 코드를 반복하는 것을 막는다.

cmd/web/helpers.go 파일을 하나 만들자.

package main

import (
	"fmt"
	"net/http"
	"runtime/debug"
)

func (app *application) serverError(w http.ResponseWriter, err error) {
	trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
	app.errorLog.Println(trace)

	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

func (app *application) clientError(w http.ResponseWriter, status int) {
	http.Error(w, http.StatusText(status), status)
}

func (app *application) notFound(w http.ResponseWriter) {
	app.clientError(w, http.StatusNotFound)
}

serverError() helper에는 debug.Stack() 함수를 사용하여 stack trace를 현재의 goroutine에 보여주며 log message를 추가해준다. 에러를 디버깅할 때 stack trace를 확인하는 것은 굉장히 큰 도움이 될 것이다.

clientError() helper는 http.StatusText() 함수를 사용하여 자동으로 인간이 읽기 편한 HTTP status code의 문자 표현을 만들어낸다. 가령, http.StatusText(400)Bad Request를 반환한다.

이제 위의 helper함수들을 핸들러에서 사용해보자.

  • handlers.go
package main

import (
	"fmt"
	"html/template"
	"net/http"
	"strconv"
)

func (app *application) home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}

	files := []string{
		"./ui/html/home.page.tmpl",
		"./ui/html/base.layout.tmpl",
		"./ui/html/footer.partial.tmpl",
	}

	ts, err := template.ParseFiles(files...)
	if err != nil {
		app.serverError(w, err)
		return
	}
	err = ts.Execute(w, nil)
	if err != nil {
		app.serverError(w, err)
	}
}

// Change the signature of the showSnippet handler so it is defined as a method
// against *application.
func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil || id < 1 {
		app.notFound(w)
		return
	}
	fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

// Change the signature of the createSnippet handler so it is defined as a method
// against *application.
func (app *application) createSnippet(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", http.MethodPost)
		app.clientError(w, http.StatusMethodNotAllowed)
		return
	}
	w.Write([]byte("Create a new snippet..."))
}

이제 서버를 실행하고, /home으로 request를 보내보자. 현재 의도한 에러가 있기 때문에 stack trace가 나오는 것이 정상이다.

 curl localhost:4000/

ERROR   2022/12/12 13:24:36 helpers.go:11: open ./ui/html/home.page.tmpl: no such file or directory
goroutine 6 [running]:
runtime/debug.Stack()
        /usr/lib/go-1.18/src/runtime/debug/stack.go:24 +0x65
main.(*application).serverError(0xc000012ce0, {0x7e9358, 0xc0000f41c0}, {0x7e7120?, 0xc00009d2c0?})
        /p4ws/gy95.park/golang_project/lets-go/snippetbox/cmd/web/helpers.go:10 +0x66
main.(*application).home(0x0?, {0x7e9358?, 0xc0000f41c0}, 0x4e3549?)
        /p4ws/gy95.park/golang_project/lets-go/snippetbox/cmd/web/handlers.go:24 +0x16c
net/http.HandlerFunc.ServeHTTP(0x7f6dc5ef9ea8?, {0x7e9358?, 0xc0000f41c0?}, 0x40ee05?)
        /usr/lib/go-1.18/src/net/http/server.go:2084 +0x2f
net/http.(*ServeMux).ServeHTTP(0x0?, {0x7e9358, 0xc0000f41c0}, 0xc000194000)
        /usr/lib/go-1.18/src/net/http/server.go:2462 +0x149
net/http.serverHandler.ServeHTTP({0xc00009d1a0?}, {0x7e9358, 0xc0000f41c0}, 0xc000194000)
        /usr/lib/go-1.18/src/net/http/server.go:2916 +0x43b
net/http.(*conn).serve(0xc0000aae60, {0x7e95b0, 0xc00009d0b0})
        /usr/lib/go-1.18/src/net/http/server.go:1966 +0x5d7
created by net/http.(*Server).Serve
        /usr/lib/go-1.18/src/net/http/server.go:3071 +0x4db

에러 로그를 잘본다면 파일이름과 line number가 log message를 통해 report되어있는 것을 확인할 수 있다.

그러나, 문제는 맨처음에 helpers.go에 대한 정보가 나온다는 것이다. 이는 코드를 작성한 부분으로 여기서 부터 에러 로그가 작성되는 것은 원치않다. 우리가 원하는 것은 에러가 발생한 함수와 거기서의 라인 number이다.

이를 위해 우리는 serverError() helper함수의 depth를 결정할 수 있다. 다음과 같이 helpers.go 파일을 업데이트하도록 하자.

  • helpers.go
func (app *application) serverError(w http.ResponseWriter, err error) {
	trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
	app.errorLog.Output(2, trace)

	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

app.errorLog.Output(2, trace)통해 2번째 라인부터 출력을 시작하는 것이다. 결과는 다음과 같다.

 curl localhost:4000/

INFO    2022/12/12 13:31:55 Starting server on :4000
ERROR   2022/12/12 13:32:00 handlers.go:24: open ./ui/html/home.page.tmpl: no such file or directory
goroutine 6 [running]:
runtime/debug.Stack()
        /usr/lib/go-1.18/src/runtime/debug/stack.go:24 +0x65
main.(*application).serverError(0xc000012ce0, {0x7e9358, 0xc0000f41c0}, {0x7e7120?, 0xc00009d2c0?})
        /p4ws/gy95.park/golang_project/lets-go/snippetbox/cmd/web/helpers.go:10 +0x58
main.(*application).home(0x0?, {0x7e9358?, 0xc0000f41c0}, 0x4e3549?)
        /p4ws/gy95.park/golang_project/lets-go/snippetbox/cmd/web/handlers.go:24 +0x16c
net/http.HandlerFunc.ServeHTTP(0x7fca30a91ea8?, {0x7e9358?, 0xc0000f41c0?}, 0x40ee05?)
        /usr/lib/go-1.18/src/net/http/server.go:2084 +0x2f
net/http.(*ServeMux).ServeHTTP(0x0?, {0x7e9358, 0xc0000f41c0}, 0xc000194000)
        /usr/lib/go-1.18/src/net/http/server.go:2462 +0x149
net/http.serverHandler.ServeHTTP({0xc00009d1a0?}, {0x7e9358, 0xc0000f41c0}, 0xc000194000)
        /usr/lib/go-1.18/src/net/http/server.go:2916 +0x43b
net/http.(*conn).serve(0xc0000aae60, {0x7e95b0, 0xc00009d0b0})
        /usr/lib/go-1.18/src/net/http/server.go:1966 +0x5d7
created by net/http.(*Server).Serve
        /usr/lib/go-1.18/src/net/http/server.go:3071 +0x4db

error의 첫 라인이 handlers.go임을 확인할 수 있다. 이제 우리는 helper에서부터 에러가 나는 것이 아닌, 진짜 에러가 발생하는 곳을 확인할 수 있다.

이제 의도적으로 에러를 발생시켰던 문제를 다시 고치도록 하자.

mv ui/html/home.page.bak ui/html/home.page.tmpl

8. Isolating the Application Routes

우리의 main 함수는 꽤나 복잡하다. 이를 깔끔하게 만들기 위해 route 선언 코드를 routes.go 파일로 옮기자.

touch cmd/web/routes.go

먼저 파일을 만들고

  • routes.go
package main

import "net/http"

func (app *application) routes() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/", app.home)
	mux.HandleFunc("/snippet", app.showSnippet)
	mux.HandleFunc("/snippet/create", app.createSnippet)

	fileServer := http.FileServer(http.Dir("./ui/static/"))
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))

	return mux
}

main.go에 있는 route 선언 쪽을 옮긴 것 뿐이다. main.go 코드는 다음과 같다.

  • main.go
package main

import (
	"flag" // New import
	"log"
	"net/http"
	"os"
)

type application struct {
	errorLog *log.Logger
	infoLog  *log.Logger
}

func main() {
	addr := flag.String("addr", ":4000", "HTTP network address")
	flag.Parse()

	infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)

	app := &application{
		errorLog: errorLog,
		infoLog:  infoLog,
	}

	mux := app.routes()
	srv := &http.Server{
		Addr:     *addr,
		ErrorLog: errorLog,
		Handler:  mux,
	}

	infoLog.Printf("Starting server on %s", *addr)
	err := srv.ListenAndServe()
	errorLog.Fatal(err)
}

이는 굉장히 정갈해 보인다. application을 위한 routes 부분이 드디어 분리되었고, app.routes() 메서드에 캡슐화되었다. 또한, main 함수의 책임이 다음과 같이 줄어들었다.

  • application 구동을 위한 runtime configuration setting 파싱
  • handlers를 위한 의존성 구축
  • http server 구동

0개의 댓글