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:
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:
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를 자동으로 선택하도록 설정한다.
RoutingDataSource.class
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
@Override
protected Object determineCurrentLookupKey() {
return contextHolder.get();
}
}
contextHolder를 이용해 읽기/쓰기 요청을 구분하는 역할을 함.
DataSourceConfig.class
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Bean
@Qualifier("masterDataSource")
public DataSource masterDataSource(DataSourceProperties masterProperties) {
HikariDataSource dataSource = masterProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
return dataSource;
}
@Bean
@Qualifier("slaveDataSource")
public DataSource slaveDataSource(DataSourceProperties slaveProperties) {
HikariDataSource dataSource = slaveProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
return dataSource;
}
@Bean
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", masterDataSource);
dataSourceMap.put("slave", slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Bean
public DataSource dataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
- routingDataSource: master/slave를 동적으로 선택
- LazyConnectionDataSourceProxy: 실제 트랜잭션이 시작될 때 데이터 소스 선택
DataSourceAspect.class
AOP를 사용해 자동으로 읽기/쓰기 트랜잭션을 분리할 수 있음
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(org.springframework.transaction.annotation.Transactional) && execution(* com.example..*Repository.save*(..))")
public void useMaster() {
ReplicationRoutingDataSource.setDataSourceKey("master");
}
@Before("execution(* com.example..*Repository.find*(..))")
public void useSlave() {
ReplicationRoutingDataSource.setDataSourceKey("slave");
}
}
- 쓰기(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] Swagger (0) | 2025.02.10 |
---|---|
[Spring] Redis를 이용한 캐싱 기법 (0) | 2025.02.08 |
[Spring] Spring Boot Starter 이해하기 (0) | 2025.02.08 |
[Spring] Spring Security (0) | 2025.02.06 |
[Spring] N + 1 문제 (0) | 2025.02.06 |