
# 42일 만에 백엔드 엔지니어의 기초를 완전히 이해하기 #3- Go 기초 편 (후편)
요약
Go 언어를 사용하여 PostgreSQL 데이터베이스에 접속하고 데이터를 영속화하는 방법을 다루는 튜토리얼입니다. Docker를 활용해 PostgreSQL 환경을 구축하고, SQL을 통해 데이터를 저장 및 조회하는 기초 과정을 설명합니다.
핵심 포인트
- Docker를 이용한 PostgreSQL 컨테이너 실행 및 관리 방법
- Go 언어에서 데이터베이스 드라이버 설치 및 연결 과정
- 데이터 영속화(Persistence)의 개념과 필요성 이해
- SQL을 활용한 테이블 생성, 데이터 삽입 및 조회 실습
지난번까지는 데이터를 메모리 상에서 다루었습니다. 서버를 재시작하면 데이터는 사라져 버립니다. 이 기사에서는 Go에서 데이터베이스(PostgreSQL)에 접속하여 데이터를 영속화(Persistence)하는 방법을 배웁니다.
이 기사를 마치면 다음과 같은 것들을 할 수 있게 됩니다.
- Docker로 PostgreSQL을 실행할 수 있다
- Go에서 DB에 접속할 수 있다
- SQL로 테이블을 만들고, 데이터 저장(INSERT) 및 조회(SELECT)를 할 수 있다
- 데이터가 DB에 영속화되는 메커니즘을 이해할 수 있다
지난번까지 만들었던 API는 데이터를 Go의 메모리 상(변수나 슬라이스)에 가지고 있었습니다. 여기에는 큰 문제가 있습니다.
서버를 재시작하면 데이터가 전부 사라진다.
메모리는 프로그램이 동작하는 동안에만 존재합니다. Ctrl + C로 서버를 중단하면 그때까지 만든 데이터는 사라져 버립니다.
그래서 데이터베이스(DB)를 사용합니다. DB는 데이터를 디스크에 저장하므로, 서버를 재시작해도 데이터가 남습니다. 이를 **영속화 (Persistence)**라고 합니다.
DB를 사용하려면 먼저 DB 소프트웨어를 실행해야 합니다. 이번에는 PostgreSQL(포스그레)이라는 대표적인 RDB(Relational Database, 관계형 데이터베이스)를 사용합니다.
Docker에 대하여
Docker는 "앱을 상자(컨테이너)에 가두어 실행하는 기술"입니다. 자세한 내용은 이후 회차에서 다루겠지만, 지금은 "docker run이라는 명령어 한 번으로, PostgreSQL을 자신의 PC에 설치하지 않고도 실행할 수 있는 편리한 도구"라고 생각하세요. 필요 없게 되면 상자째로 버릴 수 있으므로 PC를 더럽히지 않습니다.
자신의 PC에 직접 설치해도 되지만, Docker를 사용하면 한 줄로 실행할 수 있고 필요 없으면 깔끔하게 삭제할 수 있어 편리합니다.
docker run --name pg-practice \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=testdb \
...
각 옵션의 의미:
| 옵션 | 의미 |
|---|---|
--name pg-practice | 컨테이너에 이름을 붙임 |
-e POSTGRES_PASSWORD=password | DB의 비밀번호를 설정 (환경 변수) |
-e POSTGRES_DB=testdb | 초기 데이터베이스 이름을 설정 |
-p 5432:5432 | 포트 5432를 공개 (PostgreSQL의 기본값) |
-d | 백그라운드에서 실행 |
postgres:16 | 사용할 이미지 (PostgreSQL 버전 16) |
실행 확인:
docker ps
# pg-practice 가 Up 상태로 표시되면 OK
Go에서 PostgreSQL에 접속하기 위한 드라이버(Driver)를 설치합니다.
cd ~/go-practice/day3-db
go get github.com/lib/pq
go get은 외부 라이브러리를 다운로드하여 go.mod에 의존 관계를 기록하는 명령어입니다.
다음 코드를 main.go에 작성하여 실행합니다. 코드는 길지만, 블록별로 나중에 해설하겠습니다.
포트에 관한 주의사항
이 코드에서는 8080번 포트를 사용하고 있습니다. 8080은 개발에서 자주 사용되는 포트이므로, 다른 앱(Python의 FastAPI 등)이 이미 사용 중일 수 있습니다. 그 경우에는 실행 시 충돌이 발생합니다. 자신의 환경에서 8080이 사용 중이라면 코드 끝부분의 :8080을 :8081 등으로 변경해 주세요. 어떤 포트가 사용 중인지는 lsof -i :8080으로 확인할 수 있습니다.
package main
import (
"database/sql"
...
실행:
go run main.go
# → Database connected and table created
# → Server started on :8080
다른 터미널에서:
# 데이터 생성
curl -X POST -H "Content-Type: application/json" -d '{"name":"washi","age":30}' http://localhost:8080/users
# → {"id":1,"name":"washi","age":30}
...
ID가 1, 2, 3… 순으로 자동으로 증가하며, GET 요청 시 전체 데이터가 배열 형태로 반환됩니다.
먼저 import에 대해 복습해 보겠습니다. import는 "이 도구 상자(패키지)를 사용하겠습니다"라는 선언이었습니다. 이번에는 3개의 새로운 도구 상자가 추가되었습니다.
import (
"database/sql" // DB 조작 도구 상자 (표준 라이브러리)
"encoding/json" // JSON 변환 도구 상자 (지난번에도 사용)
...
새로 추가된 것은 database/sql, log, github.com/lib/pq 세 가지입니다.
database/sql→ Go의 표준 라이브러리. DB 조작을 위한 공통 인터페이스를 제공합니다.lib/pq→ PostgreSQL 전용 드라이버 (DB와 실제로 통신하는 부품)
여기서 _ (언더스코어)에 주목해 주세요. 이것은 **"이 패키지를 직접 사용하지는 않지만, 로드해 두겠다"**라는 의미를 가진 특별한 표기법입니다.
database/sql은 "DB와 대화하기 위한 공통 인터페이스"이지만, 실제로 PostgreSQL과 통신하려면 전용 부품(드라이버)이 필요합니다. lib/pq가 바로 그 부품이며, 로드되는 순간 "나는 PostgreSQL 담당입니다"라고 database/sql에 내부적으로 등록합니다.
코드 내에서 pq.무언가()와 같이 직접 호출하는 일은 없으므로, _를 붙여 임포트만 수행합니다. Go는 "임포트했지만 사용하지 않는 패키지"가 있으면 에러를 발생시키지만, _를 붙이면 "사용하지는 않지만 필요하므로 로드한다"라고 명시할 수 있어 에러가 발생하지 않습니다.
var db *sql.DB
이것은 db라는 변수를 함수 외부에서 선언한 것입니다.
지난 시간에 변수에는 두 가지 선언 방법이 있다는 것을 배웠습니다. var name string = "..." (타입 명시)와 name := "..." (타입 자동 추론)입니다. 단, :=는 함수 내부에서만 사용할 수 있습니다. 이번에는 함수 외부에서 선언하고 있으므로 var를 사용했습니다.
왜 함수 외부에서 선언할까요? 그것은 여러 함수(나중에 나올 createUserHandler와 getUsersHandler)에서 동일한 DB 연결을 사용하고 싶기 때문입니다.
지난 시간에 스코프 (Scope, 변수의 생존 범위)를 배웠습니다. 함수 내부에서 만든 변수는 해당 함수 내부에서만 존재합니다. 여러 함수에서 사용하고 싶은 변수는 함수 외부에 둠으로써 어디서든 접근할 수 있게 됩니다. db가 바로 그런 사례입니다.
*sql.DB는 DB 연결을 나타내는 타입입니다. *가 붙어 있는 이유는 "포인터 타입 (Pointer Type)"이기 때문이지만, 지금은 깊게 생각하지 말고 "DB 연결을 담는 상자의 타입"이라고 이해해 주세요.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
...
지난번에 배운 구조체(Struct)입니다. 구조체는 "서로 다른 타입의 데이터를 하나로 묶는 프로필 카드"였습니다. 여기서는 ID (정수), Name (문자열), Age (정수)라는 서로 다른 타입을 하나로 묶고 있습니다.
json:"id" 부분은 JSON 태그입니다. 이것도 지난번에 다루었습니다. Go의 필드명은 대문자로 시작(ID, Name)하지만, JSON으로 변환할 때는 소문자(id, name)로 만들고 싶기 때문에 태그로 지정합니다. 태그를 감싸고 있는 것은 백틱(backtick, `)이며, 작은따옴표(')와는 다른 것입니다.
...
db, err = sql.Open("postgres", "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable")
sql.Open...
if err != nil {
log.Fatal(err)
}
log.Fatal...
db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL
)
`)
이것은 SQL입니다. DB에 "users 테이블을 만들어라"라고 명령하고 있습니다.
...
err = db.QueryRow(
"INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
user.Name, user.Age,
).Scan(&user.ID)
INSERT INTO users (name, age) VALUES ($1, $2)
...
rows, err := db.Query("SELECT id, name, age FROM users")
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
dafer rows.Close()
var users []User
for rows.Next() {
var u User
err := rows.Scan(&u.ID, &u.Name, &u.Age)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
users = append(users, u)
}
db.Query
...
func example() {
rows, _ := db.Query("...")
dafer rows.Close() // 함수 마지막에 반드시 실행됨
// 여기서 어떤 처리를 하든, return을 하든 에러가 나든
// 함수를 나갈 때 rows.Close()가 호출됩니다
}
"열었으면 닫는다"를 세트로 작성할 수 있어, 잊어버리는 것을 방지합니다.
...
result, err := 어떤 처리()
if err != nil {
// 에러 처리
return
}
// 정상 처리
err != nil
...
func main() {
initDB()
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
getUsersHandler(w, r)
} else if r.Method == http.MethodPost {
createUserHandler(w, r)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
fmt.Println("Server started on :8080")
http.ListenAndServe(":8080", nil)
}
지난번 배운 대로, http.HandleFunc
...
go run main.go
그리고 다시 GET으로 데이터를 가져오기:
curl http://localhost:8080/users
# → 전에 만든 데이터가 그대로 남아있어요!
메모리상의 데이터라면 서버 재시작 시 사라지지만, DB에 저장한 데이터는 남습니다. 이것이 영속성(Persistence)입니다.
...
docker ps
# pg-practice 가 표시됩니다
docker stop pg-practice
그냥 멈추기만 한다면 데이터는 남아있습니다. 다음에 다시 사용할 때는 아래로 재개할 수 있습니다.
docker start pg-practice
더 이상 사용하지 않아 깨끗하게 만들고 싶다면 삭제합니다.
docker stop pg-practice # 먼저 멈추기
docker rm pg-practice # 컨테이너를 삭제
주의: 삭제하면 컨테이너 내부의 데이터도 사라집니다. 다음에 사용할 때는 처음의 docker run
...
docker run --name pg-app \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=appdb \
-p 5432:5432 \
-d postgres:16
14. products 테이블을 생성하는 SQL
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL
)
**15. `products`에 INSERT하는 Go 코드**
```go
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price int `json:"price"`
}
var product Product
product.Name = "Coffee"
product.Price = 500
err := db.QueryRow(
"INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id",
product.Name, product.Price,
).Scan(&product.ID)
if err != nil {
log.Fatal(err)
}
플레이스홀더 $1, $2를 사용하고, RETURNING id로 생성된 ID를 가져와 .Scan(&product.ID)로 기록하고 있다.
다음 시간: DB 기초편 - SQL, 테이블 설계, 정규화, 트랜잭션
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기