지난 포스트
웹 프로그래밍 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 mainimport ( "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 go get golang.org/x/crypto/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 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 mainimport ( "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/
Author:
JeHwanYoo
License:
Copyright (c) 2022 CC-BY-NC-4.0 LICENSE