1. DB Replication
1) Master-Slave 구조
- Master DB: 데이터 변경(INSERT, UPDATE, DELETE) 담당
- Slave DB: 읽기(SELECT) 요청 담당
2) Replication 구현 흐름
- Master-Slave 데이터소스 설정
- 동적으로 DataSource 선택
- Repository에서 @Transactional 사용하여 자동 분배
2. 데이터베이스 설정
1) Master DB 설정
1. cnf 파일 수정
server-id=1
log-bin=mysql-bin
binlog-do-db=mydatabase # 복제할 DB 지정
2. Master DB에서 Replication 계정 생성
CREATE USER 'replica_user'@'%' IDENTIFIED BY 'replica_password';
GRANT REPLICATION SLAVE ON *.* TO 'replica_user'@'%';
SHOW MASTER STATUS;
→ SHOW MASTER STATUS; 결과에서 File과 Position 값 확인 (Slave 설정 시 필요)
2) Slave DB 설정
1. cnf 파일 수정
[mysqld]
server-id=2
relay-log=relay-bin
2. Slave에서 Master 연결
CHANGE MASTER TO
MASTER_HOST='master_db_ip',
MASTER_USER='replica_user',
MASTER_PASSWORD='replica_password',
MASTER_LOG_FILE='mysql-bin.000001', -- Master에서 확인한 파일명
MASTER_LOG_POS=1234; -- Master에서 확인한 Position 값
START SLAVE;
SHOW SLAVE STATUS\G;
→ Slave_IO_Running: Yes, Slave_SQL_Running: Yes 면 정상 작동
3. Spring Boot 설정 (JPA + HikariCP)
1) application.yml 설정
spring:
datasource:
master:
jdbc-url: jdbc:mysql://master-db:3306/mydatabase?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
jdbc-url: jdbc:mysql://slave-db:3306/mydatabase?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
2) 동적 DataSource 설정
Spring에서 Master/Slave를 자동으로 선택하도록 설정한다.
DataSourceConfig.class
package com.example.idusexam.config;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.persistence.EntityManagerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@ConfigurationProperties(prefix = "spring.datasource.master")
@Bean
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@ConfigurationProperties(prefix = "spring.datasource.slave")
@Bean
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@DependsOn({"masterDataSource", "slaveDataSource"})
@Bean
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("MASTER", master);
dataSourceMap.put("SLAVE", slave);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(master);
return routingDataSource;
}
@DependsOn({"routingDataSource"})
@Primary
@Bean
public DataSource dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
return jpaTransactionManager;
}
}
- routingDataSource: master/slave를 동적으로 선택
- LazyConnectionDataSourceProxy: 실제 트랜잭션이 시작될 때 데이터 소스 선택
- 쓰기(save(), update()) → Master 사용
- 읽기(findById(), findAll()) → Slave 사용
Repository 사용 예제
@Repository
public interface WikiRepository extends JpaRepository<Wiki, Long> {
List<Wiki> findByAuthor(String author); // 자동으로 Slave 사용
}
4. Master에서 읽기
문제상황
Slave에서 읽고 Master에서 업데이트하는 경우 -> 데이터 일관성 문제 발생 가능
- findById(id) → 기본적으로 Slave에서 실행될 가능성이 있음
- setTitle(newTitle) → Java 객체에서 값 변경
- save(entity) → Master에서 실행됨
하지만, Slave에서 읽은 데이터가 최신이 아닐 수도 있음!
(Replication 딜레이 문제 발생 가능)
예제:
- A 사용자가 findById(id) 실행 → Slave에서 읽음
- B 사용자가 같은 데이터를 Master에서 수정함
- A 사용자가 업데이트 실행 (save) → 기존 값 덮어씌움
문제 발생 → 데이터 정합성 깨짐! (B의 수정 사항이 사라짐)
해결방법
@Service
public class WikiService {
private final WikiRepository wikiRepository;
public WikiService(WikiRepository wikiRepository) {
this.wikiRepository = wikiRepository;
}
// 📌 Master에서 강제로 읽고 수정
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Wiki getAndUpdateWiki(Long id, String newTitle) {
Wiki wiki = wikiRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 위키 없음"));
wiki.setTitle(newTitle);
return wikiRepository.save(wiki);
}
}
'Spring' 카테고리의 다른 글
[Spring] Spring Boot에서 OAuth 사용 (0) | 2025.02.13 |
---|---|
[Spring] Swagger (0) | 2025.02.10 |
[Spring] Redis를 이용한 캐싱 기법 (1) | 2025.02.08 |
[Spring] Spring Boot Starter 이해하기 (0) | 2025.02.08 |
[Spring] Spring Security (0) | 2025.02.06 |