지난 포스트


웹 프로그래밍 with Golang 1 - Hello, World!

웹 프로그래밍 with Golang 2 - 템플릿 문법

웹 프로그래밍 with Golang 3 - 정적 파일

웹 프로그래밍 with Golang 4 - 라우팅

웹 프로그래밍 with Golang 5 - Form & JSON

회원 서비스 개발


간단한 회원 서비스를 구현해보겠습니다.

회원가입을 구현하기 위해서는 아래와 같은 지식이 필요합니다.

  • 데이터베이스
  • 패스워드 해싱 (보안)

유저 정보에 대한 스키마를 정의하여 데이터베이스에 저장할 것 입니다.

또한, 패스워드는 반드시 “단방향 해싱 함수”를 이용해 암호화 해야합니다.

이 튜토리얼에서는 오늘날 흔하게 사용되는 bcrypt 방식으로 패스워드를 암호화 할 것 입니다.

bcrypt가 무엇인지 궁금하다면 이 링크를 참고해주시기 바랍니다.

sqlite3 driver 설치


데이터베이스를 관리하기 위한 소프트웨어로 DBMS(Database Management System)가 존재합니다.

SQLite는 SQL 기반의 다른 DBMS들에 비해 매우 가볍게 설계되어, 테스트용이나 작은 규모의 데이터베이스를 관리하는데 유용합니다.

보안이 중요하거나, 더 큰 규모의 데이터베이스를 운용하려면 다른 DBMS를 사용하세요.

DMBS와 Golang간의 통신을 하기 위해서는 드라이버(Driver)가 필요합니다.

go-sqlite3 패키지를 설치해봅시다.

1
go get github.com/mattn/go-sqlite3

sqlite 연결 코드 작성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)

func connectDB(dbFile string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", dbFile)

if err != nil {
return nil, err
}

if db.Ping() != nil {
return nil, err
}

return db, nil
}

database/sql 패키지는 SQL 사용을 위한 golang의 표준 인터페이스입니다.

"github.com/mattn/go-sqlite3" 패키지를 불러올 때 _를 사용하는 이유는 사용하지 않는 패키지를 포함하기 위함입니다. 패키지를 직접적으로 사용하진 않지만 부수효과(Side Effect)를 이용하기 위해서 입니다. 참고 링크

users 테이블 생성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func createUsersTable(db *sql.DB) (sql.Result, error) {
query := `
CREATE TABLE users (
user_id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)
`

result, err := db.Exec(query)

if err != nil {
return nil, err
}

return result, nil
}

해당 튜토리얼은 SQL에 대한 어느정도의 이해가 있다고 가정하겠습니다.

user_id를 기본 키(Primary key)로 하는 테이블을 생성해줍니다. email도 중복이 되면 안되므로 유니크 제약을 설정해줍니다.

db.Exec 메서드를 이용하여 쿼리를 실행할 수 있습니다.

user 생성


우선 유저를 만들기 위해 추가적으로 필요한 패키지들을 설치하겠습니다.

1
2
go get github.com/google/uuid     # unique id 알고리즘 패키지
go get golang.org/x/crypto/bcrypt # bcrypt 알고리즘 패키지

id는 기본 키이기 때문에 절대로 중복 되어서는 안됩니다. 그래서 Auto Increment를 사용하거나 uuid와 같은 unique id 생성 알고리즘을 이용하여 키를 만들게 됩니다.

bcrypt는 위에서 설명한대로 유저의 패스워드를 해싱하는데 이용하겠습니다.

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
31
32
33
34
35
36
/** 아래 두 패키지를 추가적으로 import 해주시기 바랍니다.
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
*/

func generateHashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}

func insertUser(db *sql.DB, email string, password string) (sql.Result, error) {
user_id, err := uuid.NewRandom()

if err != nil {
return nil, err
}

hash, err := generateHashPassword(password)

if err != nil {
return nil, err
}

query := `
INSERT INTO users
(user_id, email, password)
VALUES (?, ?, ?)
`
result, err := db.Exec(query, user_id, email, hash)

if err != nil {
return nil, err
}

return result, nil
}

User 조회


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type user_raw struct {
user_id string
email string
password string
}

func getUser(db *sql.DB) (*user_raw, error) {
var raw user_raw

query := `SELECT * FROM users`
err := db.QueryRow(query).Scan(&raw.user_id, &raw.email, &raw.password)

if err != nil {
return nil, err
} else {
return &raw, nil
}
}

db.QueryRow는 조회문을 실행하고 1개의 row을 가져오는 메서드입니다. 이후 row.Scan 메서드를 이용하여 변수에 할당하면 됩니다.

DB Test


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
31
32
33
34
35
36
37
38
39
func main() {
var db *sql.DB
var err error
var raw *user_raw

db, err = connectDB(":memory:")

if err != nil {
panic("데이터베이스가 연결되지 않았습니다.")
}

fmt.Println("DB Ready.")

_, err = createUsersTable(db)

if err != nil {
panic("유저 테이블이 생성되지 않았습니다.")
}

fmt.Println("Table Created.")

_, err = insertUser(db, "abc@example.com", "12345678")

if err != nil {
panic("유저가 생성되지 않았습니다.")
}

fmt.Println("User Created.")

raw, err = getUser(db)

if err != nil {
panic("유저를 불러오는데 실패했습니다.")
}
fmt.Println()
fmt.Println("user_id", raw.user_id)
fmt.Println("email", raw.email)
fmt.Println("password", raw.password)
}
1
2
3
4
5
6
7
DB Ready.
Table Created.
User Created.

user_id 4bb5c95e-bd30-414b-b025-8ba6f1c38e93
email abc@example.com
password $2a$14$MQ6ciEWVRKKg6vrzrrbMJOzPZpeqQ3X6Yc91qIW5Su3St6bNEWnZy

sqlite에 연결할 때 db파일이 아닌 :memory:를 사용하면 데이터베이스 저장소로 메모리(RAM)를 사용하게 됩니다. 따라서, 프로그램을 종료하면 런타임동안 저장한 데이터가 모두 삭제됩니다. :memory:방식은 변경사항이 누적되지 않기 때문에 유닛 테스트에서 활용 할 수 있습니다.

코드 전문


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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package main

import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"fmt"
)

func connectDB(dbFile string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", dbFile)

if err != nil {
return nil, err
}

if db.Ping() != nil {
return nil, err
}

return db, nil
}

func createUsersTable(db *sql.DB) (sql.Result, error) {
query := `
CREATE TABLE users (
user_id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)
`

result, err := db.Exec(query)

if err != nil {
return nil, err
}

return result, nil
}

func generateHashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}

func insertUser(db *sql.DB, email string, password string) (sql.Result, error) {
user_id, err := uuid.NewRandom()

if err != nil {
return nil, err
}

hash, err := generateHashPassword(password)

if err != nil {
return nil, err
}

query := `
INSERT INTO users
(user_id, email, password)
VALUES (?, ?, ?)
`
result, err := db.Exec(query, user_id.String(), email, hash)

if err != nil {
return nil, err
}

return result, nil
}

type user_raw struct {
user_id string
email string
password string
}

func getUser(db *sql.DB) (*user_raw, error) {
var raw user_raw

query := `SELECT * FROM users`
err := db.QueryRow(query).Scan(&raw.user_id, &raw.email, &raw.password)

if err != nil {
return nil, err
} else {
return &raw, nil
}
}

func main() {
var db *sql.DB
var err error
var raw *user_raw

db, err = connectDB(":memory:")

if err != nil {
panic("데이터베이스가 연결되지 않았습니다.")
}

fmt.Println("DB Ready.")

_, err = createUsersTable(db)

if err != nil {
panic("유저 테이블이 생성되지 않았습니다.")
}

fmt.Println("Table Created.")

_, err = insertUser(db, "abc@example.com", "12345678")

if err != nil {
panic("유저가 생성되지 않았습니다.")
}

fmt.Println("User Created.")

raw, err = getUser(db)

if err != nil {
panic("유저를 불러오는데 실패했습니다.")
}
fmt.Println()
fmt.Println("user_id", raw.user_id)
fmt.Println("email", raw.email)
fmt.Println("password", raw.password)
}

참고


https://gowebexamples.com/mysql-database/