PostgreSQL Master-Slave Replication 구축기
홈서버와 EC2 간 실시간 동기화
배경
스프링 부트 + 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 인증서 등)
다음 단계
- 모니터링 구축: Replication Lag 모니터링
- 자동 Failover: Master 장애 시 Slave 승격 자동화
- 보안 강화: SSL/TLS 통신, 방화벽 정책 최적화
- 성능 튜닝: Connection Pool, Query 최적화
마치며
이번 작업을 통해 단순한 서비스 배포를 넘어 고가용성 데이터베이스 아키텍처를 경험할 수 있었다. 특히 Docker 환경에서의 PostgreSQL Replication 구성과 스프링 부트의 Multiple DataSource 활용은 실무에서도 충분히 활용할 수 있는 귀중한 경험이었다.