원티드에서 제공하는 프리온보딩 챌린지 과정을 수강하면서 데이터베이스 구조를 주 데이터베이스(Master DB)와 서브 데이터베이스(Slave DB)로 나눠 쓰기 연산(Insert, Update, Delete)과 읽기 연산(Find)을 분기 처리하는 방법을 알게 되어 정리한 글입니다.
서론
대부분의 애플리케이션은 쓰기 연산보다 읽기 연산의 비중이 훨씬 높다. 따라서 더 나은 성능을 위해 데이터베이스 상태를 변경하는 생성, 수정, 삭제는 주 데이터베이스(Master DB)에서 처리하고 읽기 연산은 서브 데이터베이스(Slave DB)에서 처리한다.
연산 종류에 따라 DB를 구분하는 방법은 @Transactional 어노테이션을 사용한다. @Transactional의 readOnly 속성이 true면 읽기 연산이므로 Slave DB에서 처리하고 false면 Master DB에서 처리한다.
- @Transactional(readOnly = true) : 읽기 연산, Slave DB에서 처리
- @Transactional(readOnly = false) : 쓰기 연산, Master DB에서 처리
DB Replication
코드를 작성하기 전에 먼저 Master DB와 Slave DB를 동기화 시켜주는 작업이 필요하다. (동기화 방법은 위 블로그를 참조했습니다.)
docker-compose.yml
version: "3"
services:
db-master:
build:
context: ./
dockerfile: master/Dockerfile
restart: always
environment:
MYSQL_DATABASE: "db"
MYSQL_USER: "user"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
ports:
- "3307:3306"
# Where our data will be persisted
container_name: "master_db"
volumes:
- my-db-master:/var/lib/mysql
- my-db-master:/var/lib/mysql-files
networks:
- net-mysql
db-slave:
build:
context: ./
dockerfile: slave/Dockerfile
restart: always
environment:
MYSQL_DATABASE: "db"
MYSQL_USER: "user"
MYSQL_PASSWORD: "password"
MYSQL_ROOT_PASSWORD: "password"
ports:
- "3308:3306"
# Where our data will be persisted
container_name: "slave_db"
volumes:
- my-db-slave:/var/lib/mysql
- my-db-slave:/var/lib/mysql-files
networks:
- net-mysql
# Names our volume
volumes:
my-db-master:
my-db-slave:
networks:
net-mysql:
driver: bridge
master/Dockerfile
FROM mysql:8.0.32
ADD ./master/my.cnf /etc/mysql/my.cnf
master/my.cnf
[mysqld]
log_bin = mysql-bin
server_id = 10
default_authentication_plugin=mysql_native_password
slave/Dockerfile
FROM mysql:8.0.32
ADD ./slave/my.cnf /etc/mysql/my.cnf
slave/my.cnf
[mysqld]
log_bin = mysql-bin
server_id = 11
relay_log = /var/lib/mysql/mysql-relay-bin
log_slave_updates = 1
read_only = 1
default_authentication_plugin=mysql_native_password
위와 같이 DB Replication에 필요한 설정 파일을 작성하고 Docker-compose.yml 파일이 있는 폴더 경로로 접근해 아래 순서대로 명령어를 수행한다.
docker-compose up -d
docker inspect {MASTER DB CONTAINER ID}
여기서 IPAddress 값 저장!!
docker exec -it {SLAVE DB CONTAINER ID} mysql -u root -p
위 명령어로 SLAVE DB CONTAINER에 접속 후 아래 명령어로 SLAVE DB를 세팅해 준다.
이때 'MASTER DB IP Address' 값은 위에서 저장한 IPAddress 값을 적용해 준다.
stop slave;
CHANGE MASTER TO
MASTER_HOST='MASTER DB IP Address',
MASTER_USER='root',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=0,
GET_MASTER_PUBLIC_KEY=1;
start slave;
SLAVE DB STATUS 확인
show slave status\G;
상태를 확인했을 때 Slave_IO_Running과 Slave_SQL_Running 속성이 Yes로 되있다면 DB 동기화 성공!!
만약 둘중 하나가 NO라면 Last_Errno를 확인 후 적절한 조취를 취하면 된다.
application.yml
spring:
datasource:
master:
hikari:
#mysql setting
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
username: root
password: password
slave:
hikari:
#mysql setting
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3308/db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
username: root
password: password
Master DB와 Slave DB를 나누는 설정은 SpringBoot의 application.yml 파일에서 쉽게 할 수 있다.
설정은 Docker-compose.yml 파일에서 생성한대로 Master DB는 3307 port로 Slave DB는 3308 port로 연결했다.
하나의 데이터소스를 사용하는 경우 스프링에서 자동으로 데이터소스를 생성하지만 위와 같이 2개 이상의 데이터소스를 사용하면 추가 코드를 작성하여 데이터소스를 직접 작성해야 한다.
AbstractRoutingDataSource.class
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master";
}
}
AbstractRoutingDataSource.class를 상속받아 determineCurrentLookupKey() 메서드를 재정의 했다.
현재 트랜잭션의 readOnly 속성이 true면 "slave", false면 "master" 문자열을 리턴
DataSourceConfig.class
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean
@DependsOn({"masterDataSource", "slaveDataSource"})
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
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;
}
@Bean
@Primary
@DependsOn("routingDataSource")
public LazyConnectionDataSourceProxy dataSource(@Qualifier("routingDataSource") DataSource routingDataSource){
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
DataSourceConfig.class 파일을 작성하여 Master DB와 Slave DB DataSource를 직접 빈으로 등록해 주었다.
- @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) : JPA에서 default로 설정하는 DataSource Config 파일을 제외
- @EnableTransactionManagement : TransactionManager에 data source를 넣는 config를 호출
- masterDataSource() : application.yml 파일에서 설정한 spring.datasource.master.hikari 프로퍼티를 사용하여 MASTER DB에 대한 데이터 소스를 설정
- slaveDataSource() : application.yml 파일에서 설정한 spring.datasource.slave.hikari 프로퍼티를 사용하여 SLAVE DB에 대한 데이터 소스를 설정
- routingDataSource() : masterDataSource와 slaveDataSource 빈을 인자로 받아 라우팅 데이터 소스를 생성
- dataSource() : 트랜잭션 동기화 시점에 커넥션을 얻기 위한 설정, 만약 LazyConnectionDataSourceProxy 설정을 하지 않으면 트랜잭션 진입 전에 DataSource가 정해지고 커넥션을 연결하기 때문에 readOnly 속성으로 DataSource를 선택하는 것이 불가능하다.
테스트
왼쪽은 MASTER DB의 sql 로그를 실시간으로 출력한 이미지이고 오른쪽은 SLAVE DB의 sql 로그이다.
save 로직을 수행했을 때 MASTER DB 로그에서 insert query가 찍힌 것을 확인할 수 있었다.
반대로 select 로직을 수행했을 때는 SLAVE DB에 select query가 찍혀 readOnly 값에 따라 DataSource가 다르게 적용된 것을 확인할 수 있었다.
'Spring' 카테고리의 다른 글
[Spring] 추상 팩토리 패턴을 사용하여 소셜 로그인 구현하기 (1) | 2024.09.16 |
---|---|
헥사고날 아키텍처(Hexagonal Architecture) (0) | 2024.01.19 |
[Spring] 디자인 패턴 - 전략 패턴(Strategy Pattern) (0) | 2023.07.01 |
[Spring] 디자인 패턴 - 템플릿 메서드 패턴 (0) | 2023.06.30 |
[Spring] 트랜잭션 전파(Transaction Propagation) (0) | 2023.06.24 |