backend

Go에서 PostgreSQL 연동하기: 실무 기준 DB 연결과 Migration 설계

mirabo01 2026. 2. 5. 22:33

JWT 인증까지 구현했다면,
이제 서버는 **“진짜 운영 환경을 전제로 한 단계”**로 넘어갈 준비가 됐다.
그 핵심이 바로 PostgreSQL 연동과 스키마 관리(Migration) 다.

이번 챕터에서는

  • PostgreSQL을 Go 서버에 연결하는 기본 흐름
  • database/sql 기준의 실무 설정
  • Migration을 왜, 어떻게 관리하는지

과하지 않게, 하지만 실무 기준으로 정리한다.


왜 SQLite가 아니라 PostgreSQL인가

이전 챕터에서는 구조 설명을 위해
SQLite나 메모리 저장소를 사용했다.
하지만 운영 환경에서는 대부분 다음 이유로 PostgreSQL을 선택한다.

  • 동시성 처리에 강함
  • 트랜잭션 안정성
  • JSON, 인덱스, 확장 기능 풍부
  • 클라우드 환경과 궁합이 좋음

즉,
“Go 서버 실무”를 이야기하려면 PostgreSQL은 사실상 기본 전제다.


PostgreSQL 연동의 전체 흐름

Go 서버에서 PostgreSQL을 사용하는 기본 흐름은 항상 같다.

  1. DB 드라이버 로드
  2. 커넥션 풀 설정
  3. Ping으로 연결 확인
  4. Repository에 *sql.DB 주입
  5. Migration으로 스키마 관리

[이미지: Go + PostgreSQL 연동 전체 흐름]

이 중에서 4번 이후가 구조를 가르는 포인트다.


PostgreSQL 드라이버 선택

가장 많이 쓰이는 드라이버는 다음 두 가지다.

  • lib/pq
  • pgx

이번 챕터에서는 database/sql과 궁합이 좋은 pgx 드라이버 기준으로 설명한다.

import (
    "database/sql"
    _ "github.com/jackc/pgx/v5/stdlib"
)

⚠️ 드라이버는 import만 해도 된다
실제 사용은 database/sql이 담당한다


DB 연결 문자열 구성

PostgreSQL 연결 문자열은 보통 환경 변수로 관리한다.

postgres://user:password@localhost:5432/mydb?sslmode=disable
dsn := os.Getenv("DATABASE_URL")
db, err := sql.Open("pgx", dsn)
if err != nil {
    log.Fatal(err)
}

⚠️ 주의할 점

  • sql.Open은 실제 연결을 바로 만들지 않는다
  • 반드시 Ping()으로 연결 확인
if err := db.Ping(); err != nil {
    log.Fatal(err)
}

커넥션 풀 설정 (실무에서 매우 중요)

*sql.DB는 단순한 커넥션이 아니라
커넥션 풀(pool) 이다.

db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)

실무 기준 감각은 이렇다.

  • 너무 크면 DB가 먼저 죽는다
  • 너무 작으면 서버가 막힌다
  • 기본값에 의존하지 않는다

이 설정은
서버 성능 이슈의 1차 원인이 되는 경우가 정말 많다.


Repository 구조에 DB 주입하기

이전 챕터에서 만든 구조를 그대로 사용한다.

type UserRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepository {
    return &UserRepository{db: db}
}
  • DB는 main에서 한 번만 생성
  • repository에 주입
  • service는 repository 인터페이스만 의존

이 흐름이 깨지면
DB 의존성이 전파되기 시작한다.


PostgreSQL 기준 SQL 예시

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    password TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT now()
);

PostgreSQL에서는 보통

  • SERIAL / BIGSERIAL
  • TIMESTAMP
  • UNIQUE 제약

을 적극적으로 활용한다.


SQL 실행 예시 (Insert)

func (r *UserRepository) Create(email, password string) (User, error) {
    var user User

    err := r.db.QueryRow(`
        INSERT INTO users (email, password)
        VALUES ($1, $2)
        RETURNING id, email, created_at
    `, email, password).
        Scan(&user.ID, &user.Email, &user.CreatedAt)

    return user, err
}

PostgreSQL의 RETURNING 문법은
Go + SQL 조합에서 굉장히 자주 쓰인다.


이제 중요한 문제: 스키마를 어떻게 관리할 것인가

여기서 반드시 짚고 넘어가야 한다.

SQL 파일을 수동으로 실행하는 방식은
팀 개발, 운영 환경에서 바로 한계가 온다

그래서 필요한 게 Migration이다.


Migration이 필요한 이유

Migration은 단순히 “테이블 생성” 문제가 아니다.

  • 개발/스테이징/운영 환경 동기화
  • 변경 이력 추적
  • 롤백 가능성 확보

즉,
DB 스키마도 코드처럼 관리하기 위한 장치다.


Migration 기본 개념

Migration은 보통 다음 형태를 가진다.

0001_create_users_table.up.sql
0001_create_users_table.down.sql
  • up: 적용
  • down: 되돌리기

이 숫자가
DB 변경 이력의 순서가 된다.

[이미지: DB Migration 적용 흐름]


실무 기준 Migration 관리 원칙

⚠️ 이 기준은 정말 중요하다

  • 이미 배포된 migration은 수정하지 않는다
  • 변경이 필요하면 새로운 migration을 추가한다
  • 로컬에서만 테스트 후 커밋한다

이 원칙이 깨지면
환경 간 스키마 불일치가 발생한다.


Migration은 애플리케이션 코드와 분리한다

Migration은 보통

  • CLI 도구
  • 별도 실행 단계

로 관리한다.

즉,

  • 서버 실행 = API 제공
  • Migration 실행 = 스키마 관리

명확히 분리한다.

이 분리가 안 되면
운영 중 예측 불가능한 문제가 생긴다.


이 챕터에서 가져가야 할 감각

이 단계에서 중요한 건 문법이 아니다.

  • DB는 “연결”보다 “운영”이 중요하다
  • 커넥션 풀 설정은 필수다
  • 스키마는 코드처럼 관리해야 한다

이 감각이 생기면
ORM을 쓰든, SQL을 쓰든
DB 관련 실수가 눈에 띄게 줄어든다.


정리

PostgreSQL 연동의 핵심은
연결 성공이 아니라, 운영 가능한 구조를 만드는 것이다.

  • database/sql + pgx 조합
  • 명시적인 커넥션 풀 설정
  • Repository 패턴 유지
  • Migration으로 스키마 관리

이제 서버는
“돌아가는 코드”를 넘어서
“운영 가능한 서비스”의 형태
를 갖추기 시작했다.

다음 챕터에서는 이 DB 구조 위에서
👉 트랜잭션 설계 (service 단위 트랜잭션)
👉 또는 API 에러 응답 규약 통일하기

중 하나로 이어가면 흐름이 딱 좋다.