티스토리 뷰

postgresql 로고 이미지

 

처음 프로젝트 시작할 때는?

 

사이드 프로젝트 같은 작은 프로젝트를 시작할 때는 단일 서버로 DB를 구성하고 백업 두는 것도 굳이..? 라는 생각을 하였습니다.

사용자도 몇 없는데 굳이 이중화하고 Master-Slave 두고 할 필요는 없다고 생각합니다.

 

아래 사진은 Naver에서 소규모 웹사이트 일 때 그냥 참고만 하라는 아키텍처입니다.

[그림1] 을 봐도 Cloud DB를 1개만 사용하는 것을 볼 수 있습니다.

네이버 레퍼런스 아키텍처
[그림1] 네이버 레퍼런스 아키텍처

출처 : https://www.ncloud.com/v2/intro/architecture/3

 

NAVER CLOUD PLATFORM

cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification

www.ncloud.com

 

 

 

백엔드 개발자가 받고 싶어하는 대규모 트래픽이라면?

 

물론 대규모의 트래픽을 받으면 서버도 여러 대를 두어 로드 밸런서(Load Balancer)로 트래픽을 분산시킨다.

근데 DB도 이중화 할 뿐만 아니라 대규모 설계하면서 여러대로 세팅해 두어야하지 않을까? 생각을 하게 되었습니다.

 

그래서 검색과 AI 와 대화한 결과 Master-Slave 형태를 알게 되었고, 아래와 같이 설계를 해보고 직접 API를 쏴보고, 쿼리를 날려보면서 눈으로 보자 라는 생각을 하게 되었습니다.

 

Primary-Replica 아키텍처 설계
[그림2] Primary-Replica 아키텍처 설계

 

 

로컬에서 Docker Compose 세팅하기

 

프로젝트 Root에 Docker Compose 파일만 생성하는게 아니라,

그냥 Root에 Docker 폴더를 두고 SQL문과 shell script 을 추가하려고 합니다.

 

왜냐하면 AWS, NCP 서비스를 이용하는 것이 아니라 로컬에서 진행하는 것이고

하나하나 세팅해야하는데 그 작업이라고 보시면 됩니다.

 

굳이 운영에서는 똑같이 작업하는 것은 아니고 아~ 로컬이라서 귀찮은 작업을 하는구나 라고 보면 됩니다. 

 

Network + Volume 세팅  (docker-compose.yml)

# ──────────────────────────────────────────────
#  PostgreSQL Streaming Replication
#  Primary 1대 + Replica 3대
# ──────────────────────────────────────────────

networks:
  pg-network:
    driver: bridge

volumes:
  # pg-primary-storage: 볼륨의 이름 (도커 내부에서 부를 별칭)
  pg-primary-storage:
    driver: local  # 도커 엔진의 로컬 드라이버 사용
    driver_opts:   # 드라이버 설정 옵션
      type: none   # 별도의 파일 시스템 타입(nfs, cifs 등)을 지정하지 않음
      o: bind      # 호스트의 디렉토리를 컨테이너에 직접 바인딩(연결)하는 방식
      device: [로컬 디렉토리 절대 경로 - primary] # 실제 로컬 물리 경로

  pg-replica1-storage:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: [로컬 디렉토리 절대 경로 - replica1]

  pg-replica2-storage:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: [로컬 디렉토리 절대 경로 - replica2]

  pg-replica3-storage:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: [로컬 디렉토리 절대 경로 - replica3]

 

Docker DB 세팅 (docker-compose.yml)

이건 코드가 길어서 접기 처리를 하였습니다.

요약하면 primary, replica1,2,3 컨테이너 세팅한 것입니다.

replica1,2,3 은 컨테이너명, Port만 다르지 같은 코드라서 primary, replica1만 봐도 이해하기 쉬울 것입니다.

더보기
services:
  # ── Primary ──────────────────────────────────
  pg-primary:
    image: postgres:17
    container_name: pg-primary
    restart: unless-stopped # 컨테이너가 멈추면 자동으로 다시 시작 (수동 중지 제외)
    environment:
      POSTGRES_USER: postgres       # 기본 슈퍼유저 계정명
      POSTGRES_PASSWORD: "0000"     # 계정 비밀번호
      POSTGRES_DB: mydb             # 생성할 기본 데이터베이스 이름
    ports:
      - "5432:5432"
    volumes:
      - pg-primary-storage:/var/lib/postgresql/data       # 데이터 영구 저장 볼륨 연결
      - ./init/primary:/docker-entrypoint-initdb.d     # 초기 DB 설정 스크립트 마운트
    # [Primary 실행 옵션 설명]
    # wal_level=replica     : 복제를 위해 Write-Ahead Log 레벨을 replica로 설정
    # max_wal_senders=10    : 동시에 접속 가능한 Replica 대수 제한
    # wal_keep_size=512     : 복제 전송용으로 유지할 WAL 파일 크기(MB)
    # listen_addresses='*'  : 컨테이너 내 모든 인터페이스에서 수신 (Replica 간 내부 통신에 필요, 외부 노출은 ports에서 제어)
    # hot_standby=on        : Replica에서 읽기 전용 쿼리 허용
    command: >
      postgres
        -c wal_level=replica
        -c max_wal_senders=10
        -c wal_keep_size=512
        -c listen_addresses='*'
        -c hot_standby=on
    networks:
      - pg-network
    healthcheck: # 서비스 상태를 주기적으로 확인
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s   # 확인 주기
      timeout: 5s     # 응답 대기 시간
      retries: 5      # 실패 시 재시도 횟수

  # ── Replica 1 ────────────────────────────────
  pg-replica1:
    image: postgres:17
    container_name: pg-replica1
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: "0000"
      PGUSER: postgres      # pg_isready 등 도커 내부 명령어 실행 시 사용할 유저
      PGPASSWORD: "0000"    # 위 유저의 비밀번호
    ports:
      - "5433:5432"
    volumes:
      - pg-replica1-storage:/var/lib/postgresql/data
    command: >
      bash -c "
        # Primary가 준비될 때까지 대기
        until pg_isready -h pg-primary -U postgres; do
          echo 'Waiting for primary...' && sleep 2;
        done;
        # 데이터가 없을 경우에만 Primary로부터 데이터를 복제(백업)해옴
        if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then
          echo 'Running pg_basebackup for replica1...';
          PGPASSWORD=0000 pg_basebackup -h pg-primary -U replicator -D /var/lib/postgresql/data -Fp -Xs -P -R;
        fi;
        # pg_basebackup이 root 소유로 파일을 생성했을 수 있으므로 소유권 및 권한 정리
        chown -R postgres:postgres /var/lib/postgresql/data;
        chmod 0750 /var/lib/postgresql/data;
        # postgres 유저로 전환하여 읽기 전용 대기 서버 모드로 실행 (root 실행 불가)
        exec gosu postgres postgres -c hot_standby=on
      "
    depends_on:
      pg-primary:
        condition: service_healthy # Primary의 healthcheck가 성공해야 실행됨
    networks:
      - pg-network

  # ── Replica 2 ────────────────────────────────
  pg-replica2:
    image: postgres:17
    container_name: pg-replica2
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: "0000"
      PGUSER: postgres
      PGPASSWORD: "0000"
    ports:
      - "5434:5432"
    volumes:
      - pg-replica2-storage:/var/lib/postgresql/data
    command: >
      bash -c "
        # Primary가 준비될 때까지 대기
        until pg_isready -h pg-primary -U postgres; do
          echo 'Waiting for primary...' && sleep 2;
        done;
        # 데이터가 없을 경우에만 Primary로부터 데이터를 복제(백업)해옴
        if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then
          echo 'Running pg_basebackup for replica2...';
          PGPASSWORD=0000 pg_basebackup -h pg-primary -U replicator -D /var/lib/postgresql/data -Fp -Xs -P -R;
        fi;
        # pg_basebackup이 root 소유로 파일을 생성했을 수 있으므로 소유권 및 권한 정리
        chown -R postgres:postgres /var/lib/postgresql/data;
        chmod 0750 /var/lib/postgresql/data;
        # postgres 유저로 전환하여 읽기 전용 대기 서버 모드로 실행 (root 실행 불가)
        exec gosu postgres postgres -c hot_standby=on
      "
    depends_on:
      pg-primary:
        condition: service_healthy
    networks:
      - pg-network

  # ── Replica 3 ────────────────────────────────
  pg-replica3:
    image: postgres:17
    container_name: pg-replica3
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: "0000"
      PGUSER: postgres
      PGPASSWORD: "0000"
    ports:
      - "5435:5432"
    volumes:
      - pg-replica3-storage:/var/lib/postgresql/data
    command: >
      bash -c "
        # Primary가 준비될 때까지 대기
        until pg_isready -h pg-primary -U postgres; do
          echo 'Waiting for primary...' && sleep 2;
        done;
        # 데이터가 없을 경우에만 Primary로부터 데이터를 복제(백업)해옴
        if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then
          echo 'Running pg_basebackup for replica3...';
          PGPASSWORD=0000 pg_basebackup -h pg-primary -U replicator -D /var/lib/postgresql/data -Fp -Xs -P -R;
        fi;
        # pg_basebackup이 root 소유로 파일을 생성했을 수 있으므로 소유권 및 권한 정리
        chown -R postgres:postgres /var/lib/postgresql/data;
        chmod 0750 /var/lib/postgresql/data;
        # postgres 유저로 전환하여 읽기 전용 대기 서버 모드로 실행 (root 실행 불가)
        exec gosu postgres postgres -c hot_standby=on
      "
    depends_on:
      pg-primary:
        condition: service_healthy
    networks:
      - pg-network

 

01_init_replication.sql

-- Primary 초기화 스크립트 (유저생성 담당)
-- Docker가 처음 실행될 때 한 번만 실행됩니다

-- 복제 전용 유저 생성
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD '0000';

 

02_setup_hba.sh

#!/bin/bash
# Primary 초기화 스크립트  (시스템 설정 담당)
# pg_hba.conf에 replicator 접속 허용 규칙 추가

set -e

PG_HBA="${PGDATA}/pg_hba.conf"

echo "==> [Primary Init] pg_hba.conf에 replication 허용 규칙 추가 중..."

# 이미 추가되어 있으면 중복 추가 방지
if ! grep -q "replicator" "${PG_HBA}"; then
  cat >> "${PG_HBA}" <<EOF

# Streaming Replication: Replica 컨테이너 허용
host    replication     replicator      0.0.0.0/0       md5
EOF
  echo "==> [Primary Init] pg_hba.conf 업데이트 완료"
else
  echo "==> [Primary Init] 이미 replicator 규칙이 존재합니다. 스킵."
fi

 

 

 

도커 실행

 

아래와 같은 명령어로 도커를 기동시켜 줍니다.

docker compose up -d

 

그럼 이제 잘 기동이 되었는지 확인해 줍니다.

실행 중인 컨테이너
[그림2] 실행 중인 컨테이너

# 현재 실행 중인 컨테이너 조회
docker ps

# 컨테이너로 실행된 DB log 조회
docker logs pg-primary
docker logs pg-replica1

 

실행중인 primary db log
[그림3] 실행중인 primary db log

 

 

 

 

Replica DB에 쿼리문 날려보기

 

Dbeaver에 붙어서보면 아래와 같이 DB로 붙으면 됩니다.

연결된 DB
[그림4] 연결된 DB

CREATE table members (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

 

이 해당 쿼리문으로 읽기 전용인 Replica DB에 CREATE문을 날려보았습니다.

Replica DB CREATE문 에러
[그림5] Replica DB CREATE문 에러

 

 

 

 

Primary DB에 쿼리문 날려서 확인하기

 

이번엔 users로 했습니다!

CREATE table users (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

복제된 Replica DB 데이터
[그림6] 복제된 Replica DB 데이터

 

Primary DB에 데이터를 넣으면 Docker에서 옵션 세팅한 "wal_level=replica" 를 주어서 자동으로 데이터를 복제하게 됩니다.

 

 

 

 

 

그럼 운영에는 도커로 띄우지 않는데 따로 옵션주는게 있나요?

 

지금은 "로컬"에서 Master-Slave, Primary-Replica를 따라하려고 도커로 세팅하고 옵션을 준 것이지

AWS, NCP를 보면 자동적으로 복제해주는 서비스가 있습니다.

 

예시로 NCP (Naver Cloud Platform) 을보면 복제해서 사용할 건지 옵션 선택이 있습니다.

NCP Cloud DB
[그림7] NCP Cloud DB

 

그래서 그냥 딸깍해서 생성해두고 Spring 에서 어떻게 세팅해서 운영에서도 사용할지 알려드리도록하겠습니다.

 

 

 

 

마무리

 

쿼리문도 날렸을 때 정상적으로 데이터가 복제가 되니, 다음챕터로 넘어가겠습니다.

 

이제 Spring Boot에서 세팅을 하고 기동을 하고 API를 만들어서 1000번이든 여러번 호출했을 때,

내가 의도한대로 DB를 읽기/쓰기 하도록 하는 것을 보여드리도록 하겠습니다.

 

 

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/05   »
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
글 보관함