let's go를 배워보자 2일차 - html template, Database 접근

0

lets-go

목록 보기
2/15

13. HTML Templating and Inheritance

몇 가지 ui들을 추가하여 application program을 실행해보도록 하자.

touch ui/html/home.page.tmpl

다음의 코드를 넣어보자.

  • ui/html/home.page.tmpl
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>Home - Snippetbox</title>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <nav>
            <a href="/">Home</a>
        </nav>
        <main>
            <h2>Latest Snippets</h2>
            <p>There's nothing to see here yet!</p>
        </main>
    </body>
</html>

해당 프로젝트에서는 template naming convention을 다음과 같이 한다.

<name>.<role>.tmpl

<role>page, partial 또는 layout으로 구성된다.

이제 template 파일을 만들었으니 이를 가지고 어떻게 실행하는 가가 관건이다.

이를 위해서 html/template 패키지를 사용한다. 이는 html template를 안전하게 파싱해주고, 렌더링해주는 기능들을 제공해준다.

cmd/web/handlers.go 파일을 열어서 다음의 코드를 넣자.

  • cmd/web/handlers.go
func home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	// Use the template.ParseFiles() function to read the template file into a
	// template set. If there's an error, we log the detailed error message and use
	// the http.Error() function to send a generic 500 Internal Server Error
	// response to the user.
	ts, err := template.ParseFiles("./ui/html/home.page.tmpl")
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}
	// We then use the Execute() method on the template set to write the template
	// content as the response body. The last parameter to Execute() represents any
	// dynamic data that we want to pass in, which for now we'll leave as nil.
	err = ts.Execute(w, nil)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

template.ParseFiles에 template의 경로를 부여하는 것은 굉장히 중요하다. 이를 통해 html파일을 파싱한 후에 ts.Execute를 통해서, 응답으로 html파일을 전달하고 브라우저는 응답으로 받은 html파일을 렌더링한다.

localhost:4000/home

다음의 페이지에 접속하면 Snippetbox 화면을 볼 수 있다.

14. Template Composition

template 코드를 만들다보면, 모든 html 파일에 들어가고 공유되는 부분이 있다. 가령 header, navigation, metadata 등이 있다.

이를 위해 우리는 layout template를 준비하여 공유되는 content를 제공하는 것이 좋다. 이를 통해 우리는 각 page들에 대한 page 구체적인 markup을 만들 수 있다.

ui/html/base.layout.tmpl 파일을 새로 만들어보자.

touch ui/html/base.layout.tmpl

그리고 다음의 markup을 추가하도록 하자.

{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <nav>
            <a href='/'>Home</a>
        </nav>
        <main>
            {{template "main" .}}
        </main>
    </body>
</html>
{{end}}

{{define "base"}} ... {{end}} action는 template의 이름을 base로 명명한 것을 의미한다. base는 우리의 모든 페이지에서 공유될 부분들을 정의한 것이다.

{{template "title" .}}{{template "main" .}} action은 다른 html파일로 우리가 invoke시키고 싶은 template 파일이 된다.

{{template "title" .}}와 같이 뒤에 .이 붙은 것은 어떠한 dynamic data든 invoked template(title)에 전송하겠다는 것이다.

ui/html/home.page.tmpl에 돌아가서 titlemain부분을 named template로 만들어 base template와 함께 사용해보도록 하자.

{{template "base" .}}

{{define "title"}}Home{{end}}

{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}

{{template "base" .}} action은 매우 중요한 부분인데, 이는 home.page.tmpl 파일이 실행 될 때 base template를 invoke 시키겠다는 것이다.

다시보면, base template는 titlemain으로 명명된 templates를 invoke하는 instruction들을 포함하고 있다. 조금 헷갈릴 수 있지만, 사용하다 보면 익숙해진다.

base.layout.tmpl 파일을 만들었으니 이를 파싱하고 렌더링하는 코드도 필요하다. cmd/web/handlers.go로 가보자.

  • cmd/web/handlers.go
func home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	// Initialize a slice containing the paths to the two files. Note that the
	// home.page.tmpl file must be the *first* file in the slice.
	files := []string{
		"./ui/html/home.page.tmpl",
		"./ui/html/base.layout.tmpl",
	}
	// Use the template.ParseFiles() function to read the files and store the
	// templates in a template set. Notice that we can pass the slice of file paths
	// as a variadic parameter?
	ts, err := template.ParseFiles(files...)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}
	err = ts.Execute(w, nil)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

딱히 추가한 부분은 없다. files에 template 경로를 더 추가한 것 뿐이다.

이제 우리는 직접적으로 html파일을 실행하는 것이 아니라, 3개의 templates인 base, title , main을 사용한다. 그리고 base template를 invoke하는 명령어로 이루어져 있다.

이렇게 함으로써 page에 대한 content들을 파일에 담을 수 있고, 어플리케이션이 커질수록 유지 보수성이 좋아질 수 밖에 없다.

15. Embedding Partials

html파일을 만들다보면 여러 page에서 부분적으로 일부만 떼어내어 다른 page나 layout에서 재사용하고 싶을 것이다. 이러한 상황을 상정하기위해 일부 footer content를 담고 있는 코드를 만들어보자.

touch ui/html/footer.partial.tmpl
  • touch ui/html/footer.partial.tmpl
{{define "footer"}}
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
{{end}}

다음의 footer html을 base layout에서 사용할 수 있도록 해보자.

마지막으로 언제나 만들어진 template는 parse되고 execute되어야 하므로, 파일 경로를 'home' handler에 달아주도록 하자.

func home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	// Initialize a slice containing the paths to the two files. Note that the
	// home.page.tmpl file must be the *first* file in the slice.
	files := []string{
		"./ui/html/home.page.tmpl",
		"./ui/html/base.layout.tmpl",
		"./ui/html/footer.partial.tmpl",
	}
	// Use the template.ParseFiles() function to read the files and store the
	// templates in a template set. Notice that we can pass the slice of file paths
	// as a variadic parameter?
	ts, err := template.ParseFiles(files...)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
		return
	}
	err = ts.Execute(w, nil)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", 500)
	}
}

이제 서버를 재시작해보고 화면을 보도록 하자, footer가 잘 있음을 확인할 수 있다.

추가적으로 우리가 {{templte}} action을 사용하였는데, go에서는 {{block}}...{{end}}도 제공한다. 이는 {{templte}}와 같은 기능을 하지만 {{block}}...{{end}}은 default content를 지정할 수 있다는 점에서 다르다.

{{define "base"}}
	<h1>An example template</h1>
	{{block "sidebar" .}}
		<p>My default sidebar content</p>
	{{end}}
{{end}}

{{block "sidebar" .}}에서 sidebar라는 웹 컴포넌트가 없다면 아래에 있는 <p>My default sidebar content</p>가 나오게 된다. 물론 이는 optional한 기능이므로, default component를 적어주지 않으면 기본적으로 아무것도 안나오고, 이에 해당하는 컴포넌트가 있다면 해당 컴포넌트가 나온다.

16. Serving Static Files

css image 파일을 우리의 프로젝트에 추가하여 보자.

다음의 명령어로 css를 다운받아보자.

curl https://www.alexedwards.net/static/sb.v130.tar.gz | tar -xvz -C ./ui/static/

./ui/static에 css, img, js 디렉터리들이 생겼을 것이다.

go의 net/http 패키지는 특정 디렉토리에 있는 file들을 http를 통해 전송할 수 있도록 하는 http.FileServer 핸들러를 내장하고 있다. 새로운 route를 만들어서 우리의 /static/ 디렉터리 아래의 파일들을 제공할 수 있도록 하자.

MethodPatternHandlerAction
ANY/homeDisplay the home page
ANY/snippet?id=1showSnippetDisplay a specific snippet
POST/snippet/createcreateSnippetCreate a new snippet
ANY/static/http.FileServerServe a specific static file

이제 file server를 만들어보자.

fileServer := http.FileServer(http.Dir("./ui/static/"))

다음의 handler는 요청을 받으면 URL path로부터 앞의 slash부분을 제거할 것이다. 그리고 유저에게 전송할 상응하는 파일에 대한 ./ui/static directory를 찾는다.

가령, http://10.251.72.203:4000/static/로 요청을 보내면 자동으로 http://10.251.72.203:4000은 삭제한다. 그러나 /static/부분은 삭제되지 않아, 현재 로컬 서버의 /staitc/.ui/static/에서 파일을 찾게된다.

이러한 문제를 해결하ㅣ 위해서, 요청 URL path로부터 앞의 /statichttp.FileServer에 전달하기 전에 제거해야한다. 그렇지 않으면 파일이 없다고 나오면서 404 page not found가 나오게 된다. 다행히고 go에서는 http.StripPrefix() helper가 있어 이와 같은 역할을 할 수 있다.

  • main.go
func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", home)
	mux.HandleFunc("/snippet", showSnippet)
	mux.HandleFunc("/snippet/create", createSnippet)
	// Create a file server which serves files out of the "./ui/static" directory.
	// Note that the path given to the http.Dir function is relative to the project
	// directory root.
	fileServer := http.FileServer(http.Dir("./ui/static/"))
	// Use the mux.Handle() function to register the file server as the handler for
	// all URL paths that start with "/static/". For matching paths, we strip the
	// "/static" prefix before the request reaches the file server.
	mux.Handle("/static/", http.StripPrefix("/static", fileServer))
	log.Println("Starting server on :4000")
	err := http.ListenAndServe(":4000", mux)
	log.Fatal(err)
}

다음과 같이 한 번만 설정해놓으면 http://localhost:4000/static/을 요청할 때마다 ui/static에 있는 파일들을 확인할 수 있다.

http.FileServer()는 static file의 모든 경로가 다 보이는 것은 보안상의 이슈가 있다. 이를 해결하기 위해서 가장 간단한 방법은 경로에 대해서 index.html를 추가하는 방법이다. 매번 모든 경로마다 index.html을 추가할 수는 없으므로 다음과 같이 middleware를 추가하는 방법도 있다.

https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings

간단하게 설명하자면, static file로 전송된 URL path 가장 마지막이 /이면 Error 404 응답을 보내는 것이다.

추가적으로 go의 file server에 대한 여러가지 특정들을 정리해두었다.

  1. file server는 요청으로 들어오는 path에 path.Clean() 함수를 file 탐색 이전에 사용한다. 이를 통해서 ., ..과 같은 것들을 URL path로 부터 지워주는데 이는 directory 순회 공격(directory traversal attack)을 막는데 도움을 준다. 이러한 특정인 특히 URL path들을 자동적으로 깨끗하게(sanitize) 만들어주지 못하는 router와 fileserver를 함께 사용할 때 매우 유용하다.

  2. Range requests가 완전히 지원된다. 이는 application이 큰 파일을 지원할 때 좋은데, 사용자가 파일의 부분부분을 가져와서 다시 재조합할 때 좋다. 가령 logo.png라는 사진이 너무 크다면 바이트 range를 두어서 요청을 하는 것이다.

$ curl -i -H "Range: bytes=100-199" --output - http://localhost:4000/static/img/logo.png
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 100
Content-Range: bytes 100-199/1075
Content-Type: image/png
Last-Modified: Thu, 04 May 2017 13:07:52 GMT
Date: Wed, 08 Aug
  1. Last-ModifiedIf-Modified-Since 헤더들이 투명하게 제공된다. 만약 파일이 유저가 마지막으로 요청한 이후로 변경된 것이 없다면 http.FileServer304 Not Modified 상태 코드를 파일 대신에 보낸다. 이는 latency를 줄이고 client와 server간의 overhead를 줄인다.

  2. Content-Type이 자동적으로 file extension인 mime.TypeByExtension()함수에 의해 설정된다. 개발자가 직접 custom한 extension과 content type을 추가할 수 있는데 mime.AddExtensionType() 함수를 사용하면 된다.

또한, 재밌게도 http.FileServer는 처음 읽을 때는 서버의 disk로 부터 파일을 읽지만 이후에는 OS에 상관없이 최근에 사용된 파일에 대해서 RAM에 저장하여 caching한다. 때문에 성능면으로 매번 disk를 통해 파일을 가져오는 코드보다 훨씬 더 효율적이다.

하나의 파일을 가져오게 만들고 싶을 때가 있다. 이때에는 http.ServeFile() 함수를 사용하면 된다.

func downloadHandler(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "./ui/static/file.zip")
}

단, http.ServeFile()은 자동적으로 file path를 지워주지 않으므로, filepath.Clean()을 file을 제공하기 이전에 한 번 해주어야 한다.

17. Using the Static Files

이제 static 파일들을 사용해보자. 먼저 ui/html/base.layout.tmpl에 css와 이미지파일을 적용시켜보자.

  • ui/html/base.layout.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
        <!-- Link to the CSS stylesheet and favicon -->
        <link rel='stylesheet' href='/static/css/main.css'>
        <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
        <!-- Also link to some fonts hosted by Google -->
        <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <nav>
            <a href='/'>Home</a>
        </nav>
        <main>
            {{template "main" .}}
        </main>
        <!-- Invoke the footer template -->
        {{template "footer" .}}
        <!-- And include the JavaScript file -->
        <script src="/static/js/main.js" type="text/javascript"></script>
    </body>
</html>
{{end}}

http://localhost:4000에 접속하면 아주 멋진 화면이 나오게 된다.

18. Handler Functions

fileServer 핸들러를 추가한 부분을 보자.

mux.Handle("/static/", http.StripPrefix("/static", fileServer))

mux.Handle의 함수 부분이 어떻게 되어있는 지 확인해보자.

func (mux *ServeMux) Handle(pattern string, handler Handler)

Handler 인터페이스를 받고 있는데, 이를 확인하면 다음과 같다.

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

ServeHTTP(ResponseWriter, *Request)만 구현하면 된다. 재밌는 것은 우리가 만든 핸들러 부분들을 보자.

mux.HandleFunc("/", home)
mux.HandleFunc("/snippet", showSnippet)
mux.HandleFunc("/snippet/create", createSnippet)

home, showSnippet, createSnippet는 그냥 개발자가 만든 일반 함수이지, Handler 인터페이스의 구현체가 아니다. 그럼 어떻게 해당 함수들이 핸들러로 사용될 수 있는 것일까??

HandleFunc의 내부를 살펴보면 다음과 같다.

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.Handle(pattern, HandlerFunc(handler))
}

handler func(ResponseWriter, *Request)을 받고 이를 HandlerFunc(handler)로 변환하고 있다. HandlerFunc 부분을 살펴보면 다음과 같다.

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

HandlerFunc 함수 시그니처는 우리가 만든 함수와 같다. 그래서 타입 변환이 가능하다. 기본적으로 golang은 duck typing을 지원하기 때문이다.

이렇게 타입이 바뀌면서 ServeHTTP 메서드가 붙게된다. 문제는 이 메서드 안의 기능이 바로 http요청이 왔을 때 처리하는 핸들러 부분인데 어떻게 처리했는 지 살펴보면 아주 재밌다.

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

자신인 f 인스턴스인 함수 자체를 호출하는 것이다. 이 인스턴스 자체가 바로 우리가 만든 일반 함수이다. 그렇기 때문에 우리가 만든 함수가 ServeHTTP에서 실행되어 핸들러가 되는 것이다.

이러한 사실을 알게되면 HandlerFunc으로 Handler을 구현할 수 있고, 우리가 만든 핸들러 함수가 HandlerFunc 타입으로 호환가능하다는 것을 알 수 있다.

따라서, mux.HandleFunc이 아닌 mux.Handle에도 우리가 만든 핸들러를 사용할 수 있다. 바로 다음과 같이 말이다.

mux.Handle("/home", http.HandlerFunc(home))

HandleHandler 인터페이스를 두 번째 인자로 받는데, 우리가 만든 함수는 Handler인터페이스의 ServeHTTP 메서드를 구현하지 않아서 호환되지 않는다. 그렇다면 http.HandlerFunc 타입으로 우리의 메서드를 형변환하면 ServeHTTP가 생기므로 Handler에 호환가능하게 된다.

아주 재밌는 부분이다.

더 재밌는 부분이 있다.

우리가 서버를 구동시키고 있는 http.ListenAndServe부분을 보자.

err := http.ListenAndServe(":4000", mux)

별로 신기할게 없어보이지만, http.ListenAndServe을 잘보면

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

두번째 인자가 Handler이다. 즉, 우리가 넣어주는 두 번째 인자인 muxHandler라는 것이다. 이는 아주 재밌는 사실인데, muxHandler의 구현체라면 mux는 그저 특별한 일을 해주는 핸들러에 불과하는 것이다. 즉 사용자의 요청이 들어면 ServeHTTP()를 실행하여 해당 요청을 알맞는 핸들러 쪽으로 라우팅해주어 ServeHTTP()를 실행해주는 것이 전부라는 것이다.

따라서, go web application은 ServeHTTP() 메서드를 다른 곳에서 호출하거나, 자신이 부르거나 등 Handler의 체인으로 이루어져 있다는 것을 알 수 있다.

중요한 사실이 하나 더 있다. 모든 HTTP request들은 서버에서 소유한 goroutine으로 처리된다는 것이다. 이는 핸들러가 goroutine으로 동작하면서 핸들러에서 참조하고, 호출하고 있는 코드들이 concurrent한 상황에 놓일 수 있다는 것이다. 이는 golang 서버의 성능을 엄청나게 크게 만들었지만 한편으로는 핸들러가 공유 자원에 접근할 때는 race condition에 대해서도 고민해야 한다.

type ExampleModel struct {
	DB *sql.DB
}

func (m *ExampleModel) ExampleTransaction() error {
	// Calling the Begin() method on the connection pool creates a new sql.Tx
	// object, which represents the in-progress database transaction.
	tx, err := m.DB.Begin()
	if err != nil {
		return err
	}

	// Call Exec() on the transaction, passing in your statement and any
	// parameters. It's important to notice that tx.Exec() is called on the
	// transaction object just created, NOT the connection pool. Although we're
	// using tx.Exec() here you can also use tx.Query() and tx.QueryRow() in
	// exactly the same way.
	_, err = tx.Exec("INSERT INTO ...")
	if err != nil {
		// If there is any error, we call the tx.Rollback() method on the
		// transaction. This will abort the transaction and no changes will be
		// made to the database.
		tx.Rollback()
		return err
	}

	// Carry out another transaction in exactly the same way
	_, err = tx.Exec("UPDATE ...")
	if err != nil {
		tx.Rollback()
		return err
	}

	// If there are no errors, the statements in the transaction can be committed
	// to the database with the tx.Commit() method. It's really important to ALWAYS
	// call either Rollback() or Commit() before your function returns. If you
	// don't the connection will stay open and not be returned to the connection
	// pool. This can lead to hitting your maximum connection limit/running out of
	// resources.
	err = tx.Commit()
	return err
}

transaction은 또한 single atomic action으로 여러 개의 statement를 실행하고 싶을 때 굉장히 유용하다. tx.Rollback() 메서드를 어떠한 error 이벤트의 상황에서 쓰이는 한, transcation은 다음의 사항들을 보장해준다.

  1. 모든 statement들은 성공적으로 수행되거나
  2. 어떠한 statement들도 실행되지 않는다. 이는 database가 전혀 변동된 것이 없다는 것이다.

19. Managing Connections

sql.DB connection pool은 idle 또는 in-use connection들을 여러 개로 이루어진다. 기본적으로, 한번에 가지는 open connections(idle + in-use)의 최대 수는 제한이 없다. 그러나 기본적인 pool의 idle connection 최대 개수는 2이다. 이를 SetMaxOpenConns()SetMaxIdleConns() 메서드를 통해서 변경할수 있다.

db, err := sql.Open("mysql", *dsn)
if err != nil {
log.Fatal(err)
}
// Set the maximum number of concurrently open (idle + in-use) connections. Setting this
// to less than or equal to 0 will mean there is no maximum limit. If the maximum
// number of open connections is reached and all are in-use when a new connection is
// needed, Go will wait until one of the connections is freed and becomes idle. From a
// user perspective, this means their HTTP request will hang until a connection
// is freed.
db.SetMaxOpenConns(100)
// Set the maximum number of idle connections in the pool. Setting this
// to less than or equal to 0 will mean that no idle connections are retained.
db.SetMaxIdleConns(5)

다음의 메서드를 사용할 때는 한 가지 경고가 있는데, 사용하는 database 그 자체가 최대 connection의 수의 hard limit을 얼마나 갖고 있냐는 것이다.

가령, 기본적으로 mysql의 connection limit은 151이다. 때문에 SetMaxOpenConns()을 151 이상으로 설정하면 too many connections 에러가 발생한다. 이러한 에러가 발생하지 않도록, 151 이하로 connection 개수를 설정해놓는 것이 좋다.

그러나, 결국 또 다른 문제가 있는데, SetMaxOpenConns() 제한에 다다를 때, application이 실행할 필요가 있는 어떠한 새로운 database의 task는 여유분의 connection이 풀어질 때까지 기다리게 된다.

이러한 상황이 있을 때, 일부 application들은 별 문제없이 기다리면 되지만 web application에서는 주장하건대 즉시 too many connections 에러 메시지와 500 Internal Server Error를 user에게 전달하는 것이 좋다. http request를 hang하거나 free connection을 기다리다가 timeout이 발생하는 것보다 이 해결책이 더 좋은 방법이다.

이러한 이유 때문에 SetMaxOpenConns()SetMaxIdleConns() 메서드를 우리의 application에서 사용하지 않는 이유이다.

20. Prepared statements

앞서 이야기했듯이, Exec(), Query(), QueryRow() 메서드들은 모두 sql inejction 공격을 막기위해서 동작 뒤에서 prepared statement를 사용한다. 이들은 database connection에 prepared statement를 설정하고, 제공된 parameter와 함께 prepared statement를 실행한다. 그리고 prepared statement를 종료한다.

이것은 다소 불필요해보이는데 매번 호출될 때마다 같은 prepared statement를 생성하기 때문이다.

이론적으로 더 좋은 접근 방법은 DB.Prepare() 메서드를 사용하여 우리가 직접 우리의 prepared statement를 한 번만 만들고, 이를 대신 사용하는 것이다. 특별히 복잡한 sql statement(여러 개의 JOINS이 있는)과 자주 호출되는 statement가 있을 때는 이것을 사용하는 것이 매우 좋다.

아래에 우리의 prepared statement를 만드는 기본적은 패턴을 확인해보자.

0개의 댓글