본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 17. 14:17

42일 만에 백엔드 엔지니어의 기초를 완전히 이해하기 #4 - Go에서 DB에 접속하기

요약

Go 언어를 사용하여 PostgreSQL 데이터베이스에 접속하고 데이터를 영속화하는 방법을 다룹니다. Docker를 활용해 PostgreSQL 환경을 구축하고, Go 라이브러리를 통해 기본적인 CRUD 작업을 수행하는 과정을 설명합니다.

핵심 포인트

  • Docker를 이용한 PostgreSQL 컨테이너 실행 방법
  • 데이터 영속성(Persistence)의 개념과 필요성 이해
  • Go에서 PostgreSQL 드라이버 설치 및 연결 방법
  • SQL을 활용한 데이터 저장 및 조회 기초

42일 만에 백엔드 엔지니어의 기초를 완전히 이해하기 #4 - Go에서 DB에 접속하기

이 기사에서 배우는 것

지난 회차까지는 데이터를 메모리 상에서 다루었습니다. 서버를 재시작하면 데이터는 사라져 버립니다. 이 기사에서는 Go에서 데이터베이스(PostgreSQL)에 접속하여 데이터를 영속화(Persistence)하는 방법을 배웁니다.

이 기사를 마치면 다음과 같은 것들을 할 수 있게 됩니다.

Docker로 PostgreSQL을 실행할 수 있다

Go에서 DB에 접속할 수 있다

SQL로 테이블을 만들고, 데이터 저장(INSERT) 및 조회(SELECT)를 할 수 있다

데이터가 DB에 영속화되는 메커니즘을 이해할 수 있다

왜 DB가 필요한가

지난 회차까지 만들었던 API는 데이터를 Go의 메모리 상(변수나 슬라이스)에 가지고 있었습니다. 여기에는 큰 문제가 있습니다.

서버를 재시작하면 데이터가 전부 사라진다.

메모리는 프로그램이 동작하는 동안에만 존재합니다. Ctrl + C로 서버를 중단하면 그때까지 만든 데이터는 사라져 버립니다.

그래서 데이터베이스(DB)를 사용합니다. DB는 데이터를 디스크에 저장하므로, 서버를 재시작해도 데이터는 남습니다. 이를 영속화(Persistence)라고 합니다.

PostgreSQL을 Docker로 실행하기

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 \
-p 5432:5432 \
-d postgres:16

각 옵션의 의미:

옵션의미
--name pg-practice컨테이너에 이름을 붙임
-e POSTGRES_PASSWORD=passwordDB의 비밀번호를 설정 (환경 변수)
-e POSTGRES_DB=testdb초기 데이터베이스 이름을 설정
-p 5432:5432포트 5432를 공개 (PostgreSQL의 기본값)
-d백그라운드에서 실행
postgres:16사용할 이미지 (PostgreSQL 버전 16)

실행 확인:

docker ps

Go DB 라이브러리 설치

Go에서 PostgreSQL에 접속하기 위한 드라이버를 설치합니다.

cd ~/go-practice/day3-db

go get github.com/lib/pq

go get는 외부 라이브러리를 다운로드하고 go.mod에 의존 관계를 기록하는 명령어입니다.

【직접 해보기 ①】 Go에서 DB에 접속하여 CRUD API 만들기

다음 코드를 main.go에 작성하고 실행합니다. 코드는 길지만, 블록별로 나중에 해설하겠습니다.

포트에 관한 주의사항

이 코드에서는 8080번 포트를 사용하고 있습니다. 8080은 개발에서 자주 사용되는 포트이므로, 다른 앱(Python의 FastAPI 등)이 이미 사용 중일 수 있습니다. 그 경우에는 실행 시 충돌이 발생합니다. 자신의 환경에서 8080이 사용 중이라면 코드 끝의 :8080을 :8081 등으로 바꿔주세요. 어떤 포트가 사용 중인지는 lsof -i :8080으로 확인할 수 있습니다.

package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	_ "github.com/lib/pq"
)

var db *sql.DB

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func initDB() {
	var err error

db, err = sql.Open("postgres", "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable")

if err != nil {

log.Fatal(err)

}

_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
-- ...

}

func createUserHandler(w http.ResponseWriter, r *http.Request) {

var user User

err := json.NewDecoder(r.Body).Decode(&user)

if err != nil {

http.Error(w, "Bad request", http.StatusBadRequest)
return
}

err = db.QueryRow(
"INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
user.Name, user.Age,

}

func getUsersHandler(w http.ResponseWriter, r *http.Request) {

rows, err := db.Query("SELECT id, name, age FROM users")

if err != nil {

http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}

defer rows.Close()

var users []User
for rows.Next() {
var u User

}

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)
}
실행:

bashgo run main.go

다른 터미널에서:

bash# 데이터 생성

curl -X POST -H "Content-Type: application/json" -d '{"name":"washi","age":30}' http://localhost:8080/users

curl -X POST -H "Content-Type: application/json" -d '{"name":"taro","age":25}' http://localhost:8080/users

curl http://localhost:8080/users

ID가 1, 2, 3...으로 자동으로 증가하며 GET 요청 시 모든 데이터가 배열 형태로 반환됩니다.

코드 해설

① import의 _ "github.com/lib/pq"
먼저 import에 대한 복습입니다. import는 "이 도구 상자(패키지)를 사용합니다"라는 선언이었습니다. 이번에는 3개의 새로운 도구 상자가 추가되었습니다.

goimport (

"database/sql" // DB 조작 도구 상자 (표준 라이브러리)

"encoding/json" // JSON 변환 도구 상자 (지난번에도 사용)

"fmt" // 화면 표시 도구 상자 (지난번에도 사용)

"log" // 로그 출력 및 에러 종료 도구 상자 (이번에 새로 등장)

"net/http" // HTTP 서버 도구 상자 (지난번에도 사용)

_ "github.com/lib/pq" // PostgreSQL 전용 드라이버 (이번에 새로 등장)

)

새로운 것은 database/sql, log, github.com/lib/pq 세 가지입니다.

database/sql → Go의 표준 라이브러리 (Standard Library). DB 조작을 위한 공통 인터페이스를 제공합니다.

lib/pq → PostgreSQL 전용 드라이버 (Driver) (DB와 실제로 통신하는 부품).

여기서 _ (언더스코어)에 주목해 주세요. 이것은 "이 패키지를 직접 사용하지는 않지만, 불러와 두겠다"라는 의미의 특별한 표기법입니다.

database/sql은 "DB와 대화하기 위한 공통 인터페이스"이지만, 실제로 PostgreSQL과 통신하려면 전용 부품 (드라이버)이 필요합니다. lib/pq가 바로 그 부품이며, 로드되는 순간 "나는 PostgreSQL 담당입니다"라고 database/sql에 백그라운드에서 등록합니다.

코드 내에서 pq.something()과 같이 직접 호출하는 일은 없으므로, _를 붙여 임포트(Import)만 합니다. Go는 "임포트했지만 사용하지 않는 패키지"가 있으면 에러를 발생시키지만, _를 붙이면 "사용하지는 않지만 필요하기 때문에 로드한다"라고 명시할 수 있어 에러가 발생하지 않습니다.

② 전역 변수 db

var db *sql.DB

이것은 db라는 변수를 함수 밖에서 선언하고 있습니다.

지난번에 변수에는 두 가지 선언 방법이 있다고 배웠습니다. var name string = "..." (타입을 명시)와 name := "..." (타입을 자동 추론)입니다. 다만 :=는 함수 내부에서만 사용할 수 있습니다. 이번에는 함수 밖에서 선언하고 있으므로 var를 사용하고 있습니다.

왜 함수 밖에서 선언할까요? 그것은 여러 함수 (나중에 나올 createUserHandlergetUsersHandler)에서 동일한 DB 연결을 사용하고 싶기 때문입니다.

지난번에 스코프 (Scope, 변수의 생존 범위)를 배웠습니다. 함수 내부에서 만든 변수는 그 함수 내부에서만 존재합니다. 여러 함수에서 사용하고 싶은 변수는 함수의 외부에 둠으로써 어디서든 접근할 수 있게 됩니다. db가 바로 그것입니다.

*sql.DB는 DB 연결을 나타내는 타입입니다. *가 붙어 있는 것은 "포인터 타입 (Pointer Type)"이기 때문이지만, 지금은 깊게 생각하지 말고 "DB 연결을 담는 상자의 타입"이라고 생각하세요.

User 구조체 (지난번 복습)

type User struct {
    ID   int    `json:"id"` 
    Name string `json:"name"` 
    Age  int    `json:"age"` 
}

지난번에 배운 구조체 (Struct)입니다. 구조체는 "서로 다른 타입의 데이터를 모아놓은 프로필 카드"였습니다. 여기서는 ID (정수), Name (문자열), Age (정수)라는 서로 다른 타입을 모아두었습니다.

`json:"id"` 부분은 JSON 태그입니다. 이것도 지난번에 다루었습니다. Go의 필드명은 대문자로 시작하지만 (ID, Name), JSON으로 변환할 때는 소문자 (id, name)로 만들고 싶기 때문에 태그로 지정합니다. 태그를 감싸고 있는 것은 백틱 (Backtick, `)이며, 싱글 쿼트 (')와는 다른 것입니다.

지난번과 다른 점은 ID 필드가 추가되었다는 것입니다. 이는 DB가 자동으로 번호를 매기는 ID (식별 번호)를 넣기 위함입니다.

④ DB 연결 (sql.Open)

db, err = sql.Open("postgres", "host=localhost port=5432 user=postgres password=password dbname=testdb sslmode=disable")

이 한 줄을 분해해 보겠습니다. 먼저 왼쪽부터입니다.

db, err = → 좌변에 두 개의 변수. Go의 함수는 여러 개의 값을 반환할 수 있으므로, sql.Open의 결과를 db (DB 연결)와 err (에러) 두 가지로 받고 있습니다. db는 아까 함수 밖에서 선언한 변수이고, err는 바로 직전에 var err error로 준비한 변수입니다. 이미 선언된 변수에 대입하는 것이므로 :=가 아닌 =를 사용하고 있다는 점에 주의하세요.

sql.Open(...)database/sql 도구 상자의 Open 함수를 호출하고 있습니다. DB로의 연결을 준비합니다.

sql.Open에는 두 개의 인자 (Argument)를 전달합니다.

제1인자 `

<table> <thead> <tr> <th>파라미터 (Parameter)</th> <th>의미</th> </tr> </thead> <tbody> <tr> <td>host=localhost</td> <td>DB가 구동 중인 머신 (현재는 자신의 PC)</td> </tr> <tr> <td>port=5432</td> <td>PostgreSQL의 포트 번호</td> </tr> <tr> <td>user=postgres</td> <td>DB의 사용자 이름 (User Name)</td> </tr> <tr> <td>password=password</td> <td>비밀번호 (Docker 실행 시 설정한 값)</td> </tr> <tr> <td>dbname=testdb</td> <td>접속할 데이터베이스 이름</td> </tr> <tr> <td>sslmode=disable</td> <td>로컬이므로 SSL 암호화는 불필요</td> </tr> </tbody> </table>

이 host, port, user, password, dbname은 모두 docker run으로 PostgreSQL을 실행했을 때의 설정과 일치해야 합니다. 예를 들어, docker run에서 -e POSTGRES_PASSWORD=password라고 설정했기 때문에 여기에서도 password=password라고 적는 것입니다. 이 정보가 서로 다르면 접속에 실패합니다.

log.Fatal (치명적 에러로 중단)

if err != nil {
    log.Fatal(err)
}

if err != nil은 지난번에 배웠던 에러 체크 (Error Check)입니다. "에러가 발생했다면"이라는 의미입니다.

log.Fatal(err)는 에러 메시지를 출력하고 프로그램을 즉시 종료시키는 함수입니다. log 도구 상자의 Fatal (치명적)이라는 함수입니다.

여기서 이 함수를 사용하는 이유는, DB 접속에 실패한다면 서버를 구동해도 의미가 없기 때문입니다. DB에 연결되지 않는 API는 아무것도 할 수 없습니다. 따라서 기동 시 발생하는 이러한 치명적인 에러는 그 자리에서 프로그램 자체를 멈춰버립니다.

지난번에 나왔던 http.Error와의 차이점을 숙지해 두세요.

  • http.Error → 요청(Request) 1건에 대해 에러를 반환할 뿐, 서버는 계속 동작함
  • log.Fatal → 프로그램 전체를 강제 종료함

"요청 처리 중의 에러"는 http.Error, "기동 시의 치명적인 에러"는 log.Fatal로 구분해서 사용합니다.

⑥ 테이블 생성 (CREATE TABLE)

_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    age INTEGER NOT NULL
)`)

먼저 Go 부분부터 보겠습니다.

  • _, err =db.Exec는 두 개의 값 (실행 결과, 에러)을 반환합니다. 이번에는 실행 결과는 사용하지 않으므로 * (언더스코어)로 버리고, 에러만 err로 받습니다. *는 "이 반환값은 필요 없다"는 의미입니다.
  • db.Exec(...)db (DB 접속)의 Exec (execute = 실행) 함수입니다. SQL을 DB로 보내 실행합니다.
  • 백틱 `으로 둘러싸인 여러 줄 → 이것이 DB로 보내는 SQL 문입니다. 백틱으로 감싸면 줄바꿈을 포함한 문자열을 작성할 수 있습니다. SQL은 길어지는 경우가 많으므로 이렇게 작성하면 읽기 편합니다.

다음은 SQL 내용입니다. 이것은 "users라는 테이블을 만들어라"라는 명령입니다.

<table> <thead> <tr> <th>SQL</th> <th>의미</th> </tr> </thead> <tbody> <tr> <td>`CREATE TABLE IF NOT EXISTS`</td> <td>테이블을 생성합니다. `IF NOT EXISTS`는 "이미 존재하면 만들지 않고 건너뛰기"입니다. 이것이 없으면 두 번째 실행 시 에러가 발생합니다.</td> </tr> <tr> <td>`id SERIAL PRIMARY KEY`</td> <td>`id`라는 열(Column). `SERIAL`은 자동 채번 (1, 2, 3... 순으로 자동 증가). `PRIMARY KEY`는 기본 키 (해당 행을 고유하게 식별하는 중복되지 않는 값).</td> </tr> <tr> <td>`name TEXT NOT NULL`</td> <td>`name`이라는 열. `TEXT`는 문자열 타입. `NOT NULL`은 "빈 값을 허용하지 않음"을 의미합니다.</td> </tr> <tr> <td>`age INTEGER NOT NULL`</td> <td>`age`라는 열. `INTEGER`는 정수 타입. `NOT NULL`로 빈 값을 허용하지 않습니다.</td> </tr> </tbody> </table>

db.Exec는 결과 행(Row)을 필요로 하지 않는 SQL에 사용합니다. CREATE TABLE, INSERT, UPDATE, DELETE 등이 이에 해당합니다 (데이터를 가져오는 것이 아니라, 무언가를 실행하기만 하는 SQL).

INSERT (데이터 저장)

err = db.QueryRow(
    "INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
    user.Name, user.Age,
).Scan(&user.ID)

조금 복잡하므로 먼저 SQL 부분부터 보겠습니다.

INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id

INSERT INTO users (name, age)users 테이블의 nameage 열에

VALUES ($1, $2) → 값을 넣는다. $1과 $2는 "나중에 채울 구멍" (Placeholder, 플레이스홀더)

RETURNING id → 삽입한 행의 id (자동 생성된 값)를 반환받는다

플레이스홀더 (Placeholder) $1, $2란

$1, $2는 값을 직접 쓰지 않고 "나중에 채울 구멍"으로 비워두는 메커니즘입니다. 실제 값은 SQL 문 뒤에 이어서 전달합니다.

db.QueryRow(
    "INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id",
    user.Name, // ← $1에 들어감
    user.Age, // ← $2에 들어감
)

user.Name이 $1에, user.Age가 $2에 순서대로 들어갑니다.

왜 SQL에 값을 직접 매립하지 않을까요? 바로 SQL 인젝션 (SQL Injection) 공격을 방지하기 위해서입니다.

예를 들어 값을 직접 연결해서 작성하면, 악의적인 사용자가 이름란에 '; DROP TABLE users; --와 같은 문자열을 입력했을 때, 그것이 SQL의 일부로 실행되어 테이블 전체가 삭제될 위험이 있습니다.

플레이스홀더를 사용하면 전달된 값은 "단순한 데이터"로 취급되어, 절대로 SQL 명령어로 실행되지 않습니다. 그래서 안전합니다.

이것은 보안의 기본 중의 기본입니다. 값을 문자열 연결로 SQL에 삽입하는 것은 절대로 해서는 안 됩니다. 반드시 플레이스홀더를 사용하세요.

db.QueryRow(...).Scan(&user.ID) 란

db.QueryRow("... RETURNING id", user.Name, user.Age).Scan(&user.ID)

이 또한 . (도트)로 두 개의 처리가 연결되어 있습니다.

db.QueryRow(...) → SQL을 실행하여 "1개의 행 결과"를 받는다. 이번에는 RETURNING id를 통해 "삽입된 행의 id"가 1행 반환된다. QueryRow는 "단 하나의 행만 가져오는" 함수입니다.

.Scan(&user.ID) → 반환된 1행의 데이터를 변수에 쓴다. Scan (스캔 = 읽기)을 통해 반환된 iduser.ID에 넣고 있다.

&user.ID&에 주목하세요. 지난번에 배웠듯이 &는 "변수의 위치 (주소)를 전달한다"는 의미입니다. Scan은 "전달받은 위치에 직접 데이터를 쓴다"이므로, &를 붙여 "여기에 써줘"라고 위치를 알려주는 것입니다.

결과적으로 DB가 자동 생성한 id (예를 들어 1)가 user.ID에 들어갑니다. 그래서 응답에 {"id":1,...}와 같이 id가 포함되는 것입니다.

⑧ SELECT (여러 행 가져오기)

rows, err := db.Query("SELECT id, name, age FROM users")
if err != nil {
    http.Error(w, "Internal server error", http.StatusInternalServerError)
    return
}
defer 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)
}

하나씩 살펴보겠습니다.

rows, err := db.Query("SELECT id, name, age FROM users")

db.Query(...) → SQL을 실행하여 "여러 행의 결과"를 받는 함수. SELECT로 여러 건을 가져오고 싶을 때 사용한다.

SELECT id, name, age FROM usersusers 테이블에서 id, name, age를 전체 조회하는 SQL.

rows → 가져온 여러 행이 들어있다. 다만 이 시점에서는 "결과로 가는 입구"를 받은 것일 뿐이며, 내용은 한 행씩 꺼내올 필요가 있다.

err → 에러.

defer rows.Close()

defer는 "이 함수가 끝나기 직전에 실행해줘"라는 예약입니다.

rows (조회 결과)는 사용이 끝나면 닫아야 (Close) 합니다. 닫지 않으면 DB와의 연결 리소스가 해제되지 않고 무의미하게 계속 점유된 상태로 남게 됩니다.

defer를 붙여두면 함수가 어디서 끝나더라도 (중간에 return 하더라도, 에러로 빠져나가더라도) 반드시 마지막에 rows.Close()가 실행됩니다. "열었으면 닫는다"를 확실히 하는 메커니즘으로, 닫는 것을 잊어버리는 실수를 방지할 수 있습니다.

var users []User

[]User → "User 타입의 슬라이스 (가변 길이 배열)"라는 타입. 여러 개의 User를 묶어서 담을 수 있습니다.

조회한 모든 사용자를 여기에 쌓아갑니다. 처음에는 비어 있습니다.

for rows.Next()

rows.Next() → 다음 행(row)이 있으면 true, 없으면 false를 반환합니다.

for와 조합함으로써 "행이 있는 한 루프를 돌다" = "한 행씩 마지막까지 처리한다"라는 동작이 됩니다.

DB에서 가져온 여러 행을 한 행씩 순서대로 처리해 나가는 이미지입니다.

루프 내부

govar u User // 1행 분량을 담을 빈 User를 준비

err := rows.Scan(&u.ID, &u.Name, &u.Age) // 현재 행의 데이터를 u에 읽어들임

...

users = append(users, u) // u를 users 슬라이스에 추가

rows.Scan(&u.ID, &u.Name, &u.Age) → 현재 행의 id, name, age를 각각 u.ID, u.Name, u.Age에 씁니다. &를 붙이는 이유는 "쓸 위치를 전달하기" 위해서입니다 (INSERT를 할 때와 동일).

append(users, u) → users 슬라이스의 끝에 u를 추가합니다. append는 "추가한다"라는 내장 함수(built-in function)입니다.

루프가 끝나면 users에는 모든 사용자가 들어있습니다. 이를 JSON으로 변환하여 반환하면 [{...},{...}] 형태의 배열 응답이 됩니다.

에러 처리 패턴 (지난 시간 복습)

이 코드 곳곳에 등장하는 아래의 패턴은 지난 시간에 배운 Go의 에러 처리 방식입니다.

goresult, err := 어떤 처리()

if err != nil {

// 에러 처리

return
}

// 정상 처리

err != nil은 "에러가 발생했다 (err가 nil이 아니다)"라는 의미입니다. nil은 "아무것도 없음"을 나타내는 값이었습니다. DB 조작은 실패할 가능성(연결이 끊기거나, SQL이 틀리는 등)이 있으므로, 매번 if err != nil로 체크하여 에러가 있으면 500 에러를 반환하고 return으로 처리를 중단합니다. return을 잊으면 에러가 발생했음에도 처리가 계속 진행되므로 주의해야 합니다.

⑨ main 함수와 라우팅 (지난 시간 복습 + 응용)

gofunc 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를 통해 "이 URL에 접속하면 이 처리를 실행한다"라고 라우팅(routing)을 설정합니다. http.ListenAndServe(":8080", nil)로 포트 8080에서 서버를 시작합니다 (nil은 기본 설정을 사용한다는 의미였습니다).

이번에 새로운 점은 /users라는 하나의 URL에 대해 HTTP 메서드(GET/POST)로 처리를 나누고 있다는 점입니다. 지난번에도 r.Method로 메서드를 확인하여 분기하는 패턴을 다루었습니다.

GET /users → getUsersHandler (목록 조회)

POST /users → createUserHandler (신규 생성)

그 외 → 405 에러

서두의 initDB()는 서버 시작 전에 DB 연결과 테이블 생성을 완료하기 위한 호출입니다.

db.Exec와 db.Query와 db.QueryRow의 구분 사용

메서드용도반환값
db.Exec결과 행이 필요 없는 SQL (CREATE/INSERT/UPDATE/DELETE) 실행실행 결과 (영향을 받은 행 수 등)
db.Query여러 행을 가져오는 SELECT여러 행 (rows)
db.QueryRow단 한 행만 가져오는 SELECT 또는 RETURNING이 포함된 INSERT1개 행

메모리와 DB의 차이 (영속화 (Persistence) 확인)

여기까지 완료했다면, 서버를 한 번 중지하고 (Ctrl + C), 다시 실행해 보세요.

go run main.go

그리고 다시 GET으로 데이터를 가져옵니다:

curl http://localhost:8080/users

메모리상의 데이터라면 서버 재시작 시 사라지지만, DB에 저장한 데이터는 남습니다. 이것이 영속화 (Persistence) 입니다.

뒷정리 (컨테이너 중지 및 삭제)

학습이 끝났다면, 실행 중인 PostgreSQL 컨테이너를 중지합시다. 계속 켜두면 PC의 리소스를 계속 소비하게 됩니다.

먼저 실행 중인 컨테이너를 확인합니다.

docker ps

일시적으로 중지할 경우

docker stop pg-practice

중지만 한다면 데이터는 남습니다. 다음에 다시 사용할 때는 아래 명령어로 재개할 수 있습니다.

docker start pg-practice

완전히 삭제할 경우

더 이상 사용하지 않거나, 완전히 초기화하고 싶다면 삭제합니다.

docker stop pg-practice # 먼저 중지

docker rm pg-practice # 컨테이너 삭제

주의: 삭제하면 컨테이너 내부의 데이터도 사라집니다. 다음에 사용할 때는 처음의 docker run 단계부터 다시 시작해야 합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0