Notice
Recent Posts
250x250
«   2026/04   »
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
관리 메뉴

일상 코딩

[윈도우 개발 환경 설정] 8편: PostgreSQL 컨테이너 설정 본문

Windows 개발환경 세팅

[윈도우 개발 환경 설정] 8편: PostgreSQL 컨테이너 설정

polarcompass 2026. 3. 31. 22:27
728x90

8편: PostgreSQL 컨테이너 설정

시리즈: 윈도우 네이티브 개발 환경 구축 A to Z — 새 PC부터 클라우드 배포까지
이전 편: 7편에서 Docker Desktop을 설치하고, 이미지·컨테이너·볼륨 기본 명령어와 Docker Compose 사용법을 익혔습니다.


들어가며

백엔드 서버가 아무리 잘 돌아가도 데이터를 저장할 곳이 없으면 의미가 없습니다. 사용자 정보, 게시글, 설정값 같은 구조화된 데이터를 안정적으로 저장하고 조회하려면 관계형 데이터베이스가 필요합니다. 이 시리즈에서는 PostgreSQL을 사용합니다.

PostgreSQL을 윈도우에 직접 설치할 수도 있지만, 7편에서 익힌 Docker Compose를 활용하면 훨씬 깔끔합니다. docker-compose.yml 한 줄 수정으로 버전을 바꿀 수 있고, 프로젝트마다 독립된 DB 인스턴스를 띄울 수 있으며, 안 쓰면 컨테이너를 내려서 시스템 리소스를 아낄 수 있습니다.

이번 편에서는 Docker Compose로 PostgreSQL을 구동하고, 초기 데이터베이스와 사용자를 생성하고, GUI 도구로 접속을 확인한 뒤, 마지막으로 5편의 Go(Gin) 서버와 6편의 Python(FastAPI) 서버에서 실제로 연결하는 것까지 다룹니다.


1. 프로젝트 폴더 구성

앞으로 PostgreSQL, MinIO, n8n 등 여러 인프라 컨테이너를 함께 관리할 것이므로, 인프라 전용 폴더를 하나 만들어 두겠습니다.

mkdir ~/dev-infra
cd ~/dev-infra

이 폴더 안에 docker-compose.yml을 만들고, 편이 진행될수록 서비스를 하나씩 추가하는 방식으로 진행합니다.


2. Docker Compose로 PostgreSQL 구동

2-1. docker-compose.yml 작성

~/dev-infra/docker-compose.yml 파일을 생성합니다.

# ~/dev-infra/docker-compose.yml

services:
  postgres:
    image: postgres:17
    container_name: dev-postgres
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpass123
      POSTGRES_DB: devdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./initdb:/docker-entrypoint-initdb.d

volumes:
  postgres_data:

각 설정을 설명하겠습니다.

image: postgres:17은 PostgreSQL 17 공식 이미지를 사용합니다. latest 대신 메이저 버전을 명시해서 예기치 않은 업그레이드를 방지합니다.

container_name: dev-postgres는 컨테이너에 고정된 이름을 부여합니다. docker logs dev-postgres처럼 이름으로 바로 접근할 수 있어서 편합니다.

restart: unless-stopped는 Docker Desktop이 시작되면 컨테이너도 자동으로 함께 올라오도록 합니다. 수동으로 docker stop을 실행한 경우에만 꺼진 상태를 유지합니다.

ports: "5432:5432"는 호스트(윈도우)의 5432 포트를 컨테이너의 5432 포트에 연결합니다. DBeaver 같은 GUI 도구나 애플리케이션 코드에서 localhost:5432로 접속할 수 있게 됩니다.

environment 블록의 세 변수는 PostgreSQL 공식 이미지가 제공하는 초기화 옵션입니다. POSTGRES_USER는 슈퍼유저 이름, POSTGRES_PASSWORD는 비밀번호, POSTGRES_DB는 컨테이너가 처음 시작될 때 자동으로 생성할 데이터베이스 이름입니다.

volumes에서 postgres_data:/var/lib/postgresql/data는 데이터베이스 파일을 Docker Named Volume에 저장합니다. 컨테이너를 삭제하고 다시 만들어도 데이터가 유지됩니다. ./initdb:/docker-entrypoint-initdb.d는 컨테이너가 최초로 시작될 때 이 폴더 안의 .sql 또는 .sh 파일을 자동 실행합니다. 초기 테이블이나 추가 유저를 생성할 때 사용합니다.

2-2. 초기화 SQL 작성

initdb 폴더를 만들고 초기화 스크립트를 작성합니다.

mkdir ~/dev-infra/initdb

~/dev-infra/initdb/01_init.sql 파일을 생성합니다.

-- ~/dev-infra/initdb/01_init.sql

-- 예시 테이블: users
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 테스트 데이터 삽입
INSERT INTO users (username, email) VALUES
    ('alice', 'alice@example.com'),
    ('bob', 'bob@example.com')
ON CONFLICT DO NOTHING;

이 스크립트는 devdb 데이터베이스 안에 users 테이블을 만들고 테스트 데이터 두 건을 넣습니다. docker-entrypoint-initdb.d에 있는 파일은 볼륨이 비어 있는 최초 실행 시에만 동작합니다. 이미 데이터가 있는 상태에서 컨테이너를 재시작하면 이 스크립트는 다시 실행되지 않습니다.

2-3. 컨테이너 실행

cd ~/dev-infra
docker compose up -d
[+] Running 2/2
 ✔ Volume "dev-infra_postgres_data"  Created
 ✔ Container dev-postgres            Started

로그를 확인해서 정상 기동 여부를 봅니다.

docker logs dev-postgres

로그 마지막 부분에 아래와 비슷한 메시지가 나타나면 성공입니다.

... database system is ready to accept connections

2-4. CLI로 빠르게 확인

PowerShell에서 컨테이너 안의 psql을 실행해서 데이터를 조회해 봅니다.

docker exec -it dev-postgres psql -U devuser -d devdb

psql 프롬프트가 나타나면 아래 쿼리를 실행합니다.

SELECT * FROM users;
 id | username |       email        |          created_at
----+----------+--------------------+-------------------------------
  1 | alice    | alice@example.com  | 2026-03-31 12:00:00.000000+00
  2 | bob      | bob@example.com    | 2026-03-31 12:00:00.000000+00
(2 rows)

초기화 SQL이 제대로 실행된 것을 확인할 수 있습니다. \q를 입력하면 psql을 빠져나옵니다.

\q

3. DBeaver로 GUI 접속

커맨드라인에서 쿼리를 날릴 수 있지만, 테이블 구조를 시각적으로 탐색하거나 데이터를 편집할 때는 GUI 도구가 편합니다. 무료 데이터베이스 도구인 DBeaver Community를 설치합니다.

3-1. DBeaver 설치

winget install -e --id dbeaver.dbeaver

설치가 끝나면 DBeaver를 실행합니다.

3-2. 연결 설정

DBeaver 좌측 상단의 새 데이터베이스 연결 버튼(플러그 아이콘)을 클릭하거나 메뉴에서 Database → New Database Connection을 선택합니다. 데이터베이스 유형 선택 화면에서 PostgreSQL을 선택하고 Next를 클릭합니다.

연결 정보를 아래와 같이 입력합니다.

Host:     localhost
Port:     5432
Database: devdb
Username: devuser
Password: devpass123

좌측 하단의 Test Connection 버튼을 클릭합니다. 처음 연결할 때 PostgreSQL JDBC 드라이버 다운로드를 묻는 팝업이 나타날 수 있습니다. Download를 눌러 드라이버를 내려받으세요.

"Connected" 메시지가 나타나면 Finish를 눌러 연결을 저장합니다.

3-3. 데이터 확인

왼쪽 Database Navigator에서 devdb → Schemas → public → Tables → users를 펼치면 컬럼 구조를 볼 수 있습니다. users 테이블을 더블 클릭하면 Data 탭에서 alice와 bob 데이터가 표시됩니다.

이제 GUI에서 편하게 쿼리를 작성하고, 테이블을 탐색하고, 데이터를 수정할 수 있습니다.


4. Go(Gin)에서 PostgreSQL 연결 테스트

5편에서 만든 Go + Gin 프로젝트에서 PostgreSQL에 연결해 보겠습니다. Go에서 PostgreSQL에 접속할 때는 pgx 드라이버를 사용합니다. 현재 Go 생태계에서 가장 널리 쓰이는 PostgreSQL 드라이버입니다.

4-1. 의존성 추가

5편에서 만든 Go 프로젝트 폴더로 이동합니다. (예시 경로는 ~/go-api로 가정합니다. 실제 경로는 본인의 프로젝트에 맞게 바꿔 주세요.)

cd ~/go-api
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool

4-2. 연결 테스트 코드

main.go를 아래와 같이 수정합니다. 5편의 Hello World 라우트는 유지하고 DB 관련 코드를 추가합니다.

// main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/jackc/pgx/v5/pgxpool"
)

type User struct {
    ID        int       `json:"id"`
    Username  string    `json:"username"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

var db *pgxpool.Pool

func main() {
    // DB 연결
    dsn := os.Getenv("DATABASE_URL")
    if dsn == "" {
        dsn = "postgres://devuser:devpass123@localhost:5432/devdb?sslmode=disable"
    }

    var err error
    db, err = pgxpool.New(context.Background(), dsn)
    if err != nil {
        log.Fatalf("Unable to connect to database: %v\n", err)
    }
    defer db.Close()

    // 연결 확인
    if err := db.Ping(context.Background()); err != nil {
        log.Fatalf("Unable to ping database: %v\n", err)
    }
    log.Println("Connected to PostgreSQL!")

    // Gin 라우터
    r := gin.Default()

    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
    })

    r.GET("/users", getUsers)

    r.Run(":8080")
}

func getUsers(c *gin.Context) {
    rows, err := db.Query(context.Background(), "SELECT id, username, email, created_at FROM users")
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.CreatedAt); err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
        users = append(users, u)
    }

    c.JSON(http.StatusOK, users)
}

코드를 간단히 설명하겠습니다. pgxpool.New로 커넥션 풀을 생성합니다. 커넥션 풀은 여러 요청이 동시에 들어올 때 DB 연결을 효율적으로 재사용하기 위한 것입니다. DATABASE_URL 환경 변수가 있으면 그 값을 쓰고, 없으면 로컬 Docker PostgreSQL에 연결하는 기본값을 사용합니다. /users 엔드포인트에서 users 테이블의 전체 데이터를 JSON으로 반환합니다.

4-3. 실행 & 확인

PostgreSQL 컨테이너가 실행 중인 상태에서 Go 서버를 시작합니다.

cd ~/go-api
go run .
... Connected to PostgreSQL!
... Listening and serving HTTP on :8080

브라우저 또는 새 PowerShell 탭에서 API를 호출합니다.

curl http://localhost:8080/users
[
  {
    "id": 1,
    "username": "alice",
    "email": "alice@example.com",
    "created_at": "2026-03-31T12:00:00Z"
  },
  {
    "id": 2,
    "username": "bob",
    "email": "bob@example.com",
    "created_at": "2026-03-31T12:00:00Z"
  }
]

Go 서버가 PostgreSQL 컨테이너에 정상적으로 연결되어 데이터를 가져오는 것을 확인했습니다. 확인이 끝났으면 Ctrl+C로 Go 서버를 종료합니다.


5. Python(FastAPI)에서 PostgreSQL 연결 테스트

6편에서 만든 FastAPI 프로젝트에서도 PostgreSQL에 연결해 보겠습니다. Python에서 PostgreSQL에 접속할 때는 비동기를 지원하는 asyncpgdatabases 라이브러리를 사용할 수도 있지만, 여기서는 가장 보편적인 동기 드라이버인 psycopg를 사용하겠습니다. FastAPI는 비동기 프레임워크이지만, 동기 DB 드라이버도 함께 쓸 수 있습니다.

5-1. 의존성 추가

6편에서 만든 FastAPI 프로젝트 폴더로 이동합니다. (예시 경로는 ~/fastapi-app으로 가정합니다.)

cd ~/fastapi-app

가상 환경을 활성화합니다.

.\.venv\Scripts\Activate.ps1

psycopg 바이너리 패키지를 설치합니다. psycopg[binary]는 별도의 C 컴파일러 없이 바로 사용할 수 있는 버전입니다.

pip install "psycopg[binary]"

5-2. 연결 테스트 코드

main.py를 아래와 같이 수정합니다.

# main.py
import os
from contextlib import asynccontextmanager
from datetime import datetime

import psycopg
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel


DATABASE_URL = os.getenv(
    "DATABASE_URL",
    "postgresql://devuser:devpass123@localhost:5432/devdb",
)


class User(BaseModel):
    id: int
    username: str
    email: str
    created_at: datetime


def get_connection():
    return psycopg.connect(DATABASE_URL)


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 시작 시 연결 확인
    with get_connection() as conn:
        print("Connected to PostgreSQL!")
    yield


app = FastAPI(lifespan=lifespan)


@app.get("/")
def read_root():
    return {"message": "Hello, World!"}


@app.get("/users", response_model=list[User])
def read_users():
    try:
        with get_connection() as conn:
            with conn.cursor() as cur:
                cur.execute("SELECT id, username, email, created_at FROM users")
                rows = cur.fetchall()
                return [
                    User(id=row[0], username=row[1], email=row[2], created_at=row[3])
                    for row in rows
                ]
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

코드를 설명하겠습니다. lifespan 함수에서 서버가 시작될 때 DB 연결을 한 번 확인합니다. 연결에 실패하면 서버가 바로 에러를 내며 이를 통해 DB가 정상인지 빠르게 알 수 있습니다. /users 엔드포인트에서 psycopg.connect로 연결하고, users 테이블을 조회한 뒤 Pydantic 모델로 변환해서 반환합니다. Go 코드와 마찬가지로 DATABASE_URL 환경 변수가 없으면 로컬 Docker PostgreSQL에 연결하는 기본값을 사용합니다.

5-3. 실행 & 확인

PostgreSQL 컨테이너가 실행 중인 상태에서 FastAPI 서버를 시작합니다. Go 서버가 8080 포트를 사용했으므로, FastAPI는 8000 포트로 띄웁니다.

uvicorn main:app --reload --port 8000
Connected to PostgreSQL!
INFO:     Uvicorn running on http://127.0.0.1:8000

새 PowerShell 탭에서 API를 호출합니다.

curl http://localhost:8000/users
[
  {
    "id": 1,
    "username": "alice",
    "email": "alice@example.com",
    "created_at": "2026-03-31T12:00:00Z"
  },
  {
    "id": 2,
    "username": "bob",
    "email": "bob@example.com",
    "created_at": "2026-03-31T12:00:00Z"
  }
]

FastAPI에서도 PostgreSQL 연결이 정상적으로 동작합니다. FastAPI가 자동 생성하는 API 문서도 확인해 보세요. 브라우저에서 http://localhost:8000/docs를 열면 Swagger UI에서 /users 엔드포인트를 대화형으로 테스트할 수 있습니다.

확인이 끝났으면 Ctrl+C로 FastAPI 서버를 종료하고, deactivate로 가상 환경을 빠져나옵니다.


6. 환경 변수 관리 팁

위 코드에서 DATABASE_URL의 기본값에 비밀번호가 하드코딩되어 있는 것을 눈치채셨을 것입니다. 로컬 개발 환경에서는 괜찮지만, 프로덕션에서는 절대 이렇게 하면 안 됩니다. 지금 단계에서는 두 가지만 기억해 두세요.

첫째, .env 파일을 사용하세요. 프로젝트 루트에 .env 파일을 만들고 환경 변수를 넣어 둡니다. Docker Compose에서는 env_file 옵션으로, 애플리케이션에서는 dotenv 라이브러리로 읽을 수 있습니다.

둘째, .env 파일은 반드시 .gitignore에 추가하세요. 비밀번호가 담긴 파일이 GitHub에 올라가면 큰 보안 사고로 이어집니다.

프로덕션 환경의 시크릿 관리는 13편(클라우드 서버)과 14편(통합 Docker Compose)에서 더 자세히 다룹니다.


7. 자주 겪는 문제와 해결

포트 충돌: "port is already allocated"

윈도우에 PostgreSQL이 이미 설치돼 있으면 5432 포트가 겹칠 수 있습니다. 이 경우 docker-compose.yml의 포트 매핑을 "5433:5432"로 바꾸고, 접속 시에도 포트를 5433으로 지정하면 됩니다.

초기화 SQL이 실행되지 않는 경우

docker-entrypoint-initdb.d의 스크립트는 볼륨이 비어 있는 최초 실행 시에만 동작합니다. 이미 한번 컨테이너를 띄운 적이 있다면 볼륨에 데이터가 남아 있어 초기화 스크립트가 무시됩니다. 초기화를 다시 하고 싶으면 볼륨을 삭제하고 처음부터 시작해야 합니다.

cd ~/dev-infra
docker compose down -v
docker compose up -d

-v 플래그가 볼륨까지 삭제합니다. 당연히 기존 데이터도 전부 날아가므로 주의하세요.

연결 시 "password authentication failed"

POSTGRES_USERPOSTGRES_PASSWORD는 볼륨이 비어 있는 최초 시작 시에만 적용됩니다. 기존 볼륨이 남아 있는 상태에서 비밀번호만 바꾸면 적용되지 않습니다. 마찬가지로 docker compose down -v 후 다시 시작해야 합니다.


최종 확인 체크리스트

아래 항목을 모두 확인했다면 8편은 완료입니다.

✅ docker-compose.yml에 PostgreSQL 서비스가 정의되어 있다
✅ docker compose up -d 로 PostgreSQL 컨테이너가 정상 기동된다
✅ initdb/01_init.sql이 최초 실행 시 자동 적용되어 users 테이블과 테스트 데이터가 생성된다
✅ DBeaver에서 localhost:5432 / devuser / devpass123 으로 접속하여 데이터를 확인할 수 있다
✅ Go(Gin) 서버의 /users 엔드포인트가 PostgreSQL 데이터를 JSON으로 반환한다
✅ Python(FastAPI) 서버의 /users 엔드포인트가 PostgreSQL 데이터를 JSON으로 반환한다

다음 편 예고

데이터베이스가 준비되었으니, 다음은 파일 저장소입니다. 9편: MinIO 컨테이너 설정에서는 S3 호환 오브젝트 스토리지인 MinIO를 Docker Compose에 추가하고, 웹 콘솔에서 버킷을 만들고, Go·Python·JavaScript 세 가지 언어에서 파일을 업로드하고 다운로드하는 것까지 확인해 보겠습니다.

728x90