Ham.Blog
블로그로 돌아가기
2025. 9. 28.6분

PostgreSQL Master-Slave Replication 구축기

홈서버와 EC2 간 실시간 동기화

Postgresqlec2nginx

배경

스프링 부트 + PostgreSQL로 개발한 TravelLight 서비스를 홈서버와 EC2 서버에 모두 배포하면서, 두 환경 간 데이터 동기화가 필요한 상황이 되었다. 단순한 백업/복원 방식이 아닌 실시간 동기화를 통해 안정성과 성능을 모두 확보하고 싶었다.

목표

  • 홈서버(Master): 모든 쓰기 작업 담당
  • EC2 서버(Slave): 읽기 작업과 백업 역할
  • 실시간 동기화로 데이터 일관성 유지
  • 스프링 부트에서 읽기/쓰기 자동 분리

환경 설정

현재 구성

  • 홈서버: Docker Compose로 PostgreSQL 14 + Spring Boot 배포
  • EC2 서버: 동일한 구성으로 배포 예정
  • 네트워크: 홈서버 공인 IP를 통한 연결

1단계: Master 서버 구성 (홈서버)

Docker Compose 수정

기존 PostgreSQL 서비스를 Master로 설정하고 Replication 기능을 활성화했다.

version: '3.8'

services:
  postgres-master:
    image: postgres:14-alpine
    container_name: travellight-postgres-master
    restart: always
    environment:
      - POSTGRES_DB=travellight
      - POSTGRES_USER=${DB_USERNAME:-postgres}
      - POSTGRES_PASSWORD=${DB_PASSWORD:-your_secure_password}
      - POSTGRES_REPLICATION_USER=replicator
      - POSTGRES_REPLICATION_PASSWORD=${REPLICATION_PASSWORD:-your_repl_password}
    volumes:
      - /mnt/data/postgresql:/var/lib/postgresql/data
      - ./master-config/postgresql.conf:/etc/postgresql/postgresql.conf
      - ./master-config/pg_hba.conf:/etc/postgresql/pg_hba.conf
      - ./master-config/init-master.sh:/docker-entrypoint-initdb.d/init-master.sh
    ports:
      - "5432:5432"
    command: >
      postgres
      -c config_file=/etc/postgresql/postgresql.conf

PostgreSQL Master 설정

master-config/postgresql.conf

# Basic Settings
listen_addresses = '*'
port = 5432
max_connections = 100
shared_buffers = 256MB

# WAL Settings for Replication
wal_level = replica
max_wal_senders = 3
max_replication_slots = 3
synchronous_commit = off
wal_keep_size = 128MB

# Archive Settings
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/data/archive/%f && cp %p /var/lib/postgresql/data/archive/%f'

master-config/pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust

# EC2 서버 연결 허용
host    all             all             YOUR_EC2_IP/32         md5
host    replication     replicator      YOUR_EC2_IP/32         md5

# 모든 IP 허용 (개발용)
host    all             all             0.0.0.0/0               md5
host    replication     replicator      0.0.0.0/0               md5

Replication 사용자 생성

Docker PostgreSQL의 초기화 스크립트가 기존 데이터 때문에 실행되지 않는 문제가 있었다. 수동으로 생성했다.

# replicator 사용자 생성
docker exec -it travellight-postgres-master psql -U postgres -c "CREATE USER replicator REPLICATION LOGIN PASSWORD 'your_repl_password';"

# 설정 확인
docker exec -it travellight-postgres-master psql -U postgres -c "SELECT rolname FROM pg_roles WHERE rolname = 'replicator';"

2단계: Slave 서버 구성 (EC2)

EC2 Docker Compose 설정

postgres-slave:
  image: postgres:14-alpine
  container_name: travellight-postgres-slave
  restart: always
  environment:
    - POSTGRES_DB=travellight
    - POSTGRES_USER=${DB_USERNAME:-postgres}
    - POSTGRES_PASSWORD=${DB_PASSWORD:-your_secure_password}
    - POSTGRES_MASTER_HOST=${MASTER_HOST:-your_home_server_ip}  # 홈서버 공인 IP
    - POSTGRES_MASTER_PORT=5432
    - POSTGRES_REPLICATION_USER=replicator
    - POSTGRES_REPLICATION_PASSWORD=${REPLICATION_PASSWORD:-your_repl_password}
  volumes:
    - postgres_slave_data:/var/lib/postgresql/data
    - ./slave-config/postgresql.conf:/etc/postgresql/postgresql.conf
    - ./slave-config/init-slave.sh:/docker-entrypoint-initdb.d/init-slave.sh
  ports:
    - "5432:5432"

Slave 초기화 스크립트

slave-config/init-slave.sh

#!/bin/bash
set -e

# Master 연결 대기
echo "Waiting for master database..."
until pg_isready -h $POSTGRES_MASTER_HOST -p $POSTGRES_MASTER_PORT -U $POSTGRES_USER; do
  sleep 2
done

# Slave 초기화
if [ ! -f /var/lib/postgresql/data/standby.signal ]; then
    echo "Initializing slave from master..."
    
    rm -rf /var/lib/postgresql/data/*
    
    # Master에서 베이스 백업
    PGPASSWORD=$POSTGRES_REPLICATION_PASSWORD pg_basebackup \
        -h $POSTGRES_MASTER_HOST \
        -D /var/lib/postgresql/data \
        -U $POSTGRES_REPLICATION_USER \
        -v -P -W
    
    # Standby 모드 설정
    touch /var/lib/postgresql/data/standby.signal
    
    # Primary 연결 정보 설정
    cat >> /var/lib/postgresql/data/postgresql.conf <<EOF
primary_conninfo = 'host=$POSTGRES_MASTER_HOST port=$POSTGRES_MASTER_PORT user=$POSTGRES_REPLICATION_USER password=$POSTGRES_REPLICATION_PASSWORD'
restore_command = 'cp /var/lib/postgresql/data/archive/%f %p'
EOF

    echo "Slave initialization completed"
fi

3단계: 트러블슈팅

권한 문제 해결

처음에는 permission denied 오류가 발생했다. PostgreSQL 데이터 디렉토리의 소유자가 dnsmasq로 되어 있어서였다.

# 현재 권한 확인
sudo ls -la postgresql/

# sudo로 접근
sudo su - dnsmasq  # 또는 적절한 사용자

pg_hba.conf 설정 문제

볼륨 마운트한 설정 파일이 적용되지 않는 문제가 있었다. 기존 데이터가 있으면 초기화 스크립트가 실행되지 않기 때문이었다.

# 직접 설정 파일 수정
sudo nano /mnt/data/postgresql/pg_hba.conf

# 설정 리로드
docker exec -it travellight-postgres-master psql -U postgres -c "SELECT pg_reload_conf();"

연결 테스트

# EC2에서 Master 연결 테스트
docker run --rm -e PGPASSWORD=your_repl_password postgres:14-alpine psql -h your_home_server_ip -p 5432 -U replicator postgres -c "SELECT 1;"

4단계: 스프링 부트 읽기/쓰기 분리

Multiple DataSource 설정

application-production.yml

spring:
  datasource:
    master:
      url: ${MASTER_DB_URL:jdbc:postgresql://your_home_server_ip:5432/travellight}
      username: ${MASTER_DB_USERNAME:postgres}
      password: ${MASTER_DB_PASSWORD:your_secure_password}
      driver-class-name: org.postgresql.Driver
    slave:
      url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://postgres-slave:5432/travellight}
      username: ${SPRING_DATASOURCE_USERNAME:postgres}
      password: ${SPRING_DATASOURCE_PASSWORD:your_secure_password}
      driver-class-name: org.postgresql.Driver

데이터소스 라우팅

DatabaseConfig.java

@Configuration
public class DatabaseConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource routingDataSource() {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource());
        dataSourceMap.put("slave", slaveDataSource());
        
        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());
        
        return routingDataSource;
    }
    
    @Primary
    @Bean
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(routingDataSource());
    }
}

RoutingDataSource.java

public class RoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "slave" : "master";
    }
}

Service 레이어 수정

@Service
@Transactional
public class TravelService {
    
    // 읽기 작업 - Slave DB 사용
    @Transactional(readOnly = true)
    public List<Travel> getAllTravels() {
        return travelRepository.findAll();
    }
    
    // 쓰기 작업 - Master DB 사용
    @Transactional
    public Travel saveTravel(Travel travel) {
        return travelRepository.save(travel);
    }
}

5단계: 동기화 확인

Replication 상태 확인

# Master에서 Slave 연결 상태 확인
docker exec -it travellight-postgres-master psql -U postgres -c "SELECT * FROM pg_stat_replication;"

# Slave에서 WAL 수신 상태 확인
docker exec -it travellight-postgres-slave psql -U postgres -c "SELECT * FROM pg_stat_wal_receiver;"

동기화 테스트

# Master에서 데이터 생성
docker exec -it travellight-postgres-master psql -U postgres -d travellight -c "INSERT INTO test_table (message) VALUES ('Replication Test');"

# Slave에서 확인
docker exec -it travellight-postgres-slave psql -U postgres -d travellight -c "SELECT * FROM test_table;"

성공 로그

travellight-postgres-slave  | 2025-09-27 15:38:03.784 UTC [25] LOG:  redo starts at 0/6000028
travellight-postgres-slave  | 2025-09-27 15:38:03.789 UTC [25] LOG:  consistent recovery state reached at 0/60010C0
travellight-postgres-slave  | 2025-09-27 15:38:03.790 UTC [1] LOG:  database system is ready to accept read-only connections
travellight-postgres-slave  | 2025-09-27 15:38:03.879 UTC [32] LOG:  started streaming WAL from primary at 0/7000000 on timeline 1

최종 결과

성취한 것들:

  • 홈서버와 EC2 간 실시간 PostgreSQL Master-Slave Replication 구축
  • 스프링 부트에서 트랜잭션 기반 자동 읽기/쓰기 분리
  • Docker Compose 환경에서 안정적인 배포 구성

장점:

  • 성능 향상: 읽기 작업은 로컬 Slave에서 처리
  • 가용성 증대: Master 장애 시 Slave에서 읽기 서비스 유지 가능
  • 백업: 실시간 백업 효과
  • 확장성: 필요시 Multiple Slave 추가 가능

⚠️ 고려사항:

  • 네트워크 지연에 따른 복제 지연 가능성
  • Master 장애 시 Failover 메커니즘 필요
  • 보안 강화 (VPN, SSL 인증서 등)

다음 단계

  1. 모니터링 구축: Replication Lag 모니터링
  2. 자동 Failover: Master 장애 시 Slave 승격 자동화
  3. 보안 강화: SSL/TLS 통신, 방화벽 정책 최적화
  4. 성능 튜닝: Connection Pool, Query 최적화

마치며

이번 작업을 통해 단순한 서비스 배포를 넘어 고가용성 데이터베이스 아키텍처를 경험할 수 있었다. 특히 Docker 환경에서의 PostgreSQL Replication 구성과 스프링 부트의 Multiple DataSource 활용은 실무에서도 충분히 활용할 수 있는 귀중한 경험이었다.