[Spring] Spring Boot에서 DB Replication

1. DB Replication 

1)  Master-Slave 구조

  • Master DB: 데이터 변경(INSERT, UPDATE, DELETE) 담당
  • Slave DB: 읽기(SELECT) 요청 담당 

2) Replication 구현 흐름

  1. Master-Slave 데이터소스 설정
  2. 동적으로 DataSource 선택
  3. 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에서 업데이트하는 경우 -> 데이터 일관성 문제 발생 가능

 

  1. findById(id) → 기본적으로 Slave에서 실행될 가능성이 있음
  2. setTitle(newTitle) → Java 객체에서 값 변경
  3. save(entity) → Master에서 실행됨

하지만, Slave에서 읽은 데이터가 최신이 아닐 수도 있음!
(Replication 딜레이 문제 발생 가능)

 

예제:

  1. A 사용자가 findById(id) 실행 → Slave에서 읽음
  2. B 사용자가 같은 데이터를 Master에서 수정함
  3. 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