본문 바로 가기

[MySQL, Spring] 간단한 로컬 Replication 세팅 가이드

들어가며

MySQL 8.0과 Spring Boot 3를 이용하여 Master-Slave Replication 환경을 구축하고, Spring의 트랜잭션 특성에 따라 적절한 데이터베이스로 라우팅하는 방법을 설명합니다.

목표

  • MySQL 8.0을 이용하여 Master 1대와 Slave 2대로 구성된 Replication 환경 구축
  • Spring Boot에서 읽기 전용 트랜잭션(@Transactional(readOnly = true))은 Slave DB에 접근
  • 쓰기 트랜잭션(기본 @Transactional)은 Master DB에 접근하도록 설정
  • Master DB: 쓰기 작업 전담 (mysql_three, 포트 3308)
  • Slave DB: 읽기 작업 담당 (mysql_one, mysql_two, 포트 3306, 3307)
  • 읽기 작업은 두 Slave DB 간에 랜덤하게 분산

 

Docker를 이용한 MySQL 설정

먼저 아래와 같은 디렉토리 구조를 만듭니다:

your-project/
├── docker-compose.yml
├── conf.d_one/
│   └── my.cnf
├── conf.d_two/
│   └── my.cnf
├── conf.d_three/
│   └── my.cnf
├── data_one/
├── data_two/
└── data_three/

1. docker-compose.yml 작성

version: "3"
services:
  mysql_one:
    image: mysql:8.0.35-debian
    container_name: mysql_1
    ports:
      - 3306:3306
    environment:
      TZ: Asia/Seoul
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: repl
    volumes:
      - ./data_one:/var/lib/mysql
      - ./conf.d_one:/etc/mysql/conf.d
  mysql_two:
    image: mysql:8.0.35-debian
    container_name: mysql_2
    ports:
      - 3307:3306
    environment:
      TZ: Asia/Seoul
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: repl
    volumes:
      - ./data_two:/var/lib/mysql
      - ./conf.d_two:/etc/mysql/conf.d
  mysql_three:
    image: mysql:8.0.35-debian
    container_name: mysql_3
    ports:
      - 3308:3306
    environment:
      TZ: Asia/Seoul
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: repl
    volumes:
      - ./data_three:/var/lib/mysql
      - ./conf.d_three:/etc/mysql/conf.d

2. MySQL 설정 파일 작성

각 인스턴스의 설정 파일을 아래와 같이 작성합니다.

conf.d_one/my.cnf (Slave 1)

[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
gtid_mode                      = ON
enforce-gtid-consistency       = ON
server_id                      = 1

[mysqldump]
default-character-set = utf8mb4

conf.d_two/my.cnf (Slave 2)

[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
gtid_mode                      = ON
enforce-gtid-consistency       = ON
server_id                      = 2

[mysqldump]
default-character-set = utf8mb4

conf.d_three/my.cnf (Master)

[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server           = utf8mb4
collation-server               = utf8mb4_unicode_ci
gtid_mode                      = ON
enforce-gtid-consistency       = ON
server_id                      = 3

[mysqldump]
default-character-set = utf8mb4

3. Docker Compose 실행

docker compose up -d

4. Master DB(mysql_three) 설정

Master DB에 접속하여 Replication 사용자를 생성하고 권한을 부여합니다:

docker exec -it mysql_3 mysql -uroot -proot
CREATE USER 'young'@'%' IDENTIFIED WITH mysql_native_password BY 'young';
GRANT REPLICATION SLAVE ON *.* TO 'young'@'%';

5. Slave DB(mysql_one, mysql_two) 설정

각 Slave DB에 접속하여 Replication을 설정합니다:

mysql_one 설정

docker exec -it mysql_1 mysql -uroot -proot
CHANGE REPLICATION SOURCE TO
    SOURCE_HOST = 'mysql_3',
    SOURCE_PORT = 3306,
    SOURCE_USER = 'young',
    SOURCE_PASSWORD = 'young',
    SOURCE_AUTO_POSITION = 1;

START REPLICA;

SHOW REPLICA STATUS\G

mysql_two 설정

docker exec -it mysql_2 mysql -uroot -proot
CHANGE REPLICATION SOURCE TO
    SOURCE_HOST = 'mysql_3',
    SOURCE_PORT = 3306,
    SOURCE_USER = 'young',
    SOURCE_PASSWORD = 'young',
    SOURCE_AUTO_POSITION = 1;

START REPLICA;

SHOW REPLICA STATUS\G

참고: SHOW REPLICA STATUS\G 명령을 실행하여 Slave_IO_RunningSlave_SQL_Running이 모두 "Yes"로 표시되는지 확인합니다. 이는 Replication이 정상적으로 작동하고 있음을 나타냅니다.

Spring Boot 설정

1. 의존성 추가

build.gradle 또는 pom.xml에 필요한 의존성을 추가합니다:

Gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'mysql:mysql-connector-java'
    // 기타 필요한 의존성
}

2. application.yml 구성

app:
  datasource:
    one:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/repl
      username: root
      password: root
    two:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3307/repl
      username: root
      password: root
    three:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3308/repl
      username: root
      password: root

spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

3. 데이터 소스 라우팅 설정

DoubleRouting.java

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Random;

public class DoubleRouting extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 읽기 전용 트랜잭션인 경우 Slave DB(0 또는 1) 사용
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            Random random = new Random();
            // 0: mysql_one, 1: mysql_two 중 랜덤하게 선택
            return random.nextInt(2);
        }
        // 그 외의 경우 Master DB(2: mysql_three) 사용
        return 2;
    }
}

4. DataSource 설정 클래스 작성

MyDataSourceConfiguration.java

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.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class MyDataSourceConfiguration {

    @Primary
    @Bean
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(routingDataSource());
    }

    @Bean
    public DataSource routingDataSource() {
        DoubleRouting routing = new DoubleRouting();
        routing.setTargetDataSources(targetDataSources());
        return routing;
    }

    private Map<Object, Object> targetDataSources() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(0, dataSourceOne());   // Slave 1
        targetDataSources.put(1, dataSourceTwo());   // Slave 2
        targetDataSources.put(2, dataSourceThree()); // Master
        return targetDataSources;
    }

    @Bean
    @ConfigurationProperties("app.datasource.one")
    public DataSource dataSourceOne() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "ds_2")
    @ConfigurationProperties("app.datasource.two")
    public DataSource dataSourceTwo() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "ds_3")
    @ConfigurationProperties("app.datasource.three")
    public DataSource dataSourceThree() {
        return DataSourceBuilder.create().build();
    }
}

서비스 레이어 예시

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 읽기 작업: Slave DB 사용
    @Transactional(readOnly = true)
    public User findById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }

    // 쓰기 작업: Master DB 사용
    @Transactional
    public User save(User user) {
        return userRepository.save(user);
    }
}

테스트

Replication이 제대로 설정되었는지 확인하기 위해 간단한 테스트를 수행할 수 있습니다:

  1. Master DB에 테이블을 생성하고 데이터를 삽입합니다:
  2. CREATE TABLE test (id INT PRIMARY KEY, name VARCHAR(50)); INSERT INTO test VALUES (1, 'Test Data');
  3. Slave DB에서 데이터를 확인합니다:
  4. SELECT * FROM test;
  5. Spring Boot 애플리케이션에서 @Transactional(readOnly = true)를 사용하는 메서드를 호출하여 로그를 확인합니다. Slave DB 중 하나에 연결되어야 합니다.

결론

이제 MySQL Replication과 Spring Boot를 연동하여 읽기/쓰기 작업을 적절히 분리하는 환경이 구축되었습니다. 이 구성은 읽기 작업을 Slave DB로 분산시켜 전체 시스템의 성능을 향상시키고, 부하를 분산시킵니다.

더 복잡한 환경에서는 추가적인 설정이 필요할 수 있으며, 운영 환경에서는 모니터링과 장애 복구 전략도 고려해야 합니다.

Reference

https://hoing.io/archives/18445
https://velog.io/@max9106/DB-Spring-Replication