MySQL Master-Slave Replication 설정
문제풀이 플랫폼을 개발하면서 단일 MySQL을 사용하는 대신 쓰기 전용 DB(Master)와 읽기 전용 DB(Slave)를 분리하는 것이 더 적합하다고 판단했다.
이러한 구조를 적용하면 읽기 부하를 효과적으로 분산할 수 있고, 백업 및 장애 대응 능력을 향상할 수 있다. 다만, 설정이 복잡해지고 데이터 동기화 지연 등의 단점도 존재한다.
이 글에서는 MySQL Master-Slave Replication을 Docker 환경에서 설정하는 과정을 상세히 정리해 보았다.
현재 진행 중인 프로젝트의 경우 문제풀이 플랫폼을 진행 중이다. 해당 플랫폼의 경우 관리자가 문제를 만들고 업데이트하는(Input, Update) 상황보단, 사용자가 문제를 읽는 경우(Select)가 더 빈번하다고 판단하였다.
✅ 장점
- 읽기 성능 향상
- Slave 노드를 추가하여 읽기 부하를 분산할 수 있음
SELECT
쿼리를 Slave에서 처리하도록 하면 Master의 부담이 줄어 성능이 향상됨
- 백업 및 유지보수 용이
- Slave에서 데이터를 백업하면 Master에 영향을 주지 않고 백업 가능
- Master가 다운되더라도 Slave를 승격(Promotion)하여 운영 지속 가능
- 데이터 복구 및 장애 대응 가능
- Master가 장애 발생 시 Slave를 Master로 전환하여 서비스 다운타임을 줄일 수 있음
- 데이터 손실 가능성을 낮출 수 있음
- 확장성(Scalability) 향상
- 읽기 요청이 많아질 경우 Slave를 추가하여 로드 밸런싱 가능
- 대규모 서비스에서도 안정적인 데이터 처리를 지원
❌ 단점
- 데이터 동기화 지연(Lag Issue)
- Master에서 변경된 데이터가 Slave에 반영되기까지 시간이 걸림(Replication Delay)
- Slave에서 읽은 데이터가 최신 데이터가 아닐 수 있음
- 설정 및 운영 복잡성 증가
- 단일 MySQL에 비해 설정이 복잡하며,
server-id
,binlog
,replication
등 추가적인 설정 필요
- Slave를 추가하면 네트워크, 스토리지 등의 관리 부담 증가
- 단일 MySQL에 비해 설정이 복잡하며,
- 쓰기 성능 저하 가능성
- Master가 모든 쓰기 연산을 담당하기 때문에 트래픽이 증가하면 성능 저하 가능
- 쓰기 부하를 분산하려면 추가적인 샤딩(Sharding)이나 분산 데이터베이스 구성이 필요
- 장애 발생 시 복구 절차 필요
- Master가 장애 발생하면 Slave를 Master로 승격하는 과정이 필요함
- 자동 장애 복구(Auto Failover)를 위한 추가적인 설정이 필요함
판단 기준
장점의 이유와 단점의 상쇄를 기준으로 해서 판단하게 되었다.
- 실제로 단점에서의 데이터 동기화 지연의 경우 해당 서비스가 금융처럼 실시간 서비스가 중요한 요소가 아니기에 상쇄 가능하다고 생각하였다.
- 쓰기 성능 저하의 경우 문제를 업데이트, 사용자의 풀이 결과가 들어오면 반영이 되는 부분이다. 이때 해당 부분은 기존의 읽기 작업을 다른 DB에서 진행하므로 이점이 더 많다고 생각하였다.
그리고 위와 같은 이유로 이제 해당 Master, Slave DB를 분산하기로 하였다.
Docker 환경에서 MySQL Master-Slave Replication을 설정하는 과정을 정리해 보았다.
1. Master-Slave 환경 설정 (docker-compose.yml 기준)
다음은 docker-compose.yml
에서 MySQL Master-Slave 환경을 설정한 내용이다. 해당 docker compose에 해당 부분을 작성하였다.
mysql-master:
image: mysql:8.0
container_name: database-master
environment:
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: project
MYSQL_USER: user
MYSQL_PASSWORD: 1234
TZ: Asia/Seoul
ports:
- "3307:3306"
volumes:
- mysql_master_data:/var/lib/mysql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --server-id=1
- --log-bin=mysql-bin
- --binlog-format=row
networks:
- app_network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "user", "--password=1234"]
interval: 10s
timeout: 5s
retries: 5
mysql-slave:
image: mysql:8.0
container_name: database-slave
environment:
MYSQL_ROOT_PASSWORD: 1234
MYSQL_DATABASE: project
MYSQL_USER: user
MYSQL_PASSWORD: 1234
TZ: Asia/Seoul
ports:
- "3308:3306"
volumes:
- mysql_slave_data:/var/lib/mysql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --server-id=2
- --relay-log=mysql-relay-bin
depends_on:
mysql-master:
condition: service_healthy
networks:
- app_network
2. Master 서버에서 Slave 사용자 생성
2.1 Master 서버 접속
docker exec -it database-master mysql -u root -p
비밀번호: 1234
2.2 Replication 사용자 생성 및 권한 부여
CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password BY 'replicapassword';
GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%';
FLUSH PRIVILEGES;
보안 강화: IP를 지정하는 것이 더 안전함
CREATE USER 'replica'@'192.168.1.100' IDENTIFIED WITH mysql_native_password BY 'replicapassword';
GRANT REPLICATION SLAVE ON *.* TO 'replica'@'192.168.1.100';
3. Master 서버 상태 확인 및 Binary Log 정보 확인
3.1 Master 상태 확인
SHOW MASTER STATUS;
출력 예시:
File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
mysql-bin.000001 | 154 | project |
이 값을 Slave 서버에서 설정할 때 사용함
4. Slave 서버 설정 및 Replication 시작
4.1 Slave 서버 접속
docker exec -it database-slave mysql -u root -p
비밀번호: 1234
4.2 Master 정보 등록
CHANGE MASTER TO
MASTER_HOST='database-master',
MASTER_USER='replica',
MASTER_PASSWORD='replicapassword',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154,
GET_MASTER_PUBLIC_KEY=1;
4.3 Replication 시작
START SLAVE;
4.4 Slave 상태 확인
SHOW SLAVE STATUS\G;
출력 예시:
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: database-master
Master_User: replica
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 154
Relay_Log_File: database-slave-relay-bin.000002
Relay_Log_Pos: 367
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
확인할 사항:
항목 | 값 | 의미 |
Slave_IO_Running | Yes |
Master와 연결 성공 |
Slave_SQL_Running | Yes |
데이터를 정상 복제 중 |
Master_Log_File | mysql-bin.000001 |
Master의 Binary Log 파일 |
Read_Master_Log_Pos | 154 |
복제 시작 위치 |
5. Spring 작업 진행
다음은 Spring에서 해당 부분 적용을 위한 단계이다.
- application.yaml 수정이 설정은 Master-Slave 구조의 데이터베이스 연결을 정의한다
spring.datasource.master
→ 쓰기 작업을 담당하는 Master DB
spring.datasource.slave
→ 읽기 작업을 담당하는 Slave DB
datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/project?serverTimezone=UTC&characterEncoding=UTF-8 username: root password: 1234 slave: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/project?serverTimezone=UTC&characterEncoding=UTF-8 username: root password: 1234
- ReplicationRoutingDataSource 생성이 클래스는 현재 실행 중인 트랜잭션이 Master DB를 사용할지, Slave DB를 사용할지 결정하는 로직을 담당한다.
AbstractRoutingDataSource
를 상속받아 동적으로 데이터소스를 변경할 수 있도록 구현됨.
ThreadLocal<String> currentDataSource
를 사용해 현재 사용할 데이터베이스(Master/Slave)를 저장.
determineCurrentLookupKey()
→ 현재 설정된 데이터베이스 키(master
orslave
)를 반환.
setCurrentDataSource(String key)
→ 현재 요청에 대해 사용할 데이터베이스를 변경하는 메서드.
package com.example.be.config; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class ReplicationRoutingDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> currentDataSource = new ThreadLocal<>(); public static void setCurrentDataSource(String key) { currentDataSource.set(key); } @Override protected Object determineCurrentLookupKey() { return currentDataSource.get(); } }
- DataSourceConfig 생성이 설정 클래스는 Spring에서 사용할 데이터베이스 연결을 설정하는 역할을 한다.
@Configuration
→ Spring의 설정 클래스로 등록됨.
@Value
를 사용해application.yaml
에서 데이터베이스 연결 정보를 주입받음.
createDataSource()
메서드를 통해 HikariCP를 사용해DataSource
객체를 생성함.
dataSource()
메서드에서ReplicationRoutingDataSource
를 설정하고, Master/Slave를 매핑함.
@Primary
→ Spring에서 기본적으로 사용할 데이터 소스 지정.
package com.example.be.config; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { @Value("${spring.datasource.master.url}") private String masterUrl; @Value("${spring.datasource.slave.url}") private String slaveUrl; @Value("${spring.datasource.master.username}") private String masterUsername; @Value("${spring.datasource.slave.username}") private String slaveUsername; @Value("${spring.datasource.master.password}") private String masterPassword; @Value("${spring.datasource.master.password}") private String slavePassword; @Bean @Primary public DataSource dataSource() { ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource(); DataSource masterDataSource = createDataSource( masterUrl,masterUsername,masterPassword); DataSource slaveDataSource = createDataSource( slaveUrl,slaveUsername,slavePassword); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource); targetDataSources.put("slave", slaveDataSource); routingDataSource.setDefaultTargetDataSource(masterDataSource); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.afterPropertiesSet(); return routingDataSource; } private DataSource createDataSource(String url, String username, String password) { HikariConfig config = new HikariConfig(); config.setJdbcUrl(url); config.setUsername(username); config.setPassword(password); return new HikariDataSource(config); } }
- @Aspect를 통한 AOP 설정이 클래스는 AOP를 이용해 트랜잭션의 읽기 여부에 따라 자동으로 Master/Slave를 변경한다.
@Aspect
→ AOP(관점 지향 프로그래밍) 설정을 위한 어노테이션.
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
@Transactional
이 적용된 메서드를 감싸서 실행.
MethodSignature
를 사용해 현재 트랜잭션이readOnly
인지 확인.
readOnly
가true
이면slave
,false
이면master
데이터베이스를 사용하도록 설정.
- 트랜잭션 종료 후에는 기본값(
master
)으로 복원.
package com.example.be.common.domain.utils; import com.example.be.config.ReplicationRoutingDataSource; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Aspect @Component public class DataSourceAspect { @Around("@annotation(org.springframework.transaction.annotation.Transactional)") public Object setDataSource(ProceedingJoinPoint joinPoint) throws Throwable { Transactional transactional = ((MethodSignature) joinPoint.getSignature()) .getMethod() .getAnnotation(Transactional.class); if (transactional.readOnly()) { ReplicationRoutingDataSource.setCurrentDataSource("slave"); // 읽기 전용 -> Slave로 } else { ReplicationRoutingDataSource.setCurrentDataSource("master"); // 쓰기 -> Master로 } try { return joinPoint.proceed(); } finally { ReplicationRoutingDataSource.setCurrentDataSource("master"); // 기본값 복원 } } }
- 사용하는 기존의 Transactional ReadOnly 세팅 진행
@Transactional(readOnly = true) //Select @Transactional //Input, Update, Delete
6. 최종 정리
✅ Master 서버에서 수행할 작업
- Master 서버 접속
- Slave 사용자 생성 및 권한 부여
CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password BY 'replicapassword'; GRANT REPLICATION SLAVE ON *.* TO 'replica'@'%'; FLUSH PRIVILEGES;
- Master 상태 확인 (
SHOW MASTER STATUS;
)
✅ Slave 서버에서 수행할 작업
- Slave 서버 접속
- Master 서버 정보 설정
CHANGE MASTER TO MASTER_HOST='database-master', MASTER_USER='replica', MASTER_PASSWORD='replicapassword', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=154, GET_MASTER_PUBLIC_KEY=1;
- Replication 시작 (
START SLAVE;
)
- 상태 확인 (
SHOW SLAVE STATUS\G;
)
✅ Spring에서 수행할 작업
- application.yaml 세팅
- ReplicationRoutingDataSource 생성
- DataSourceConfig 설정
- Transactional(readOnly=True)를 통한 읽기/쓰기 작업 분리 - AOP
7. 결론
이제 MySQL Master-Slave 구조가 정상적으로 동작하며, SELECT
쿼리는 Slave에서 처리하여 읽기 성능을 향상할 수 있다.