업데이트:


우테코 팀 프로젝트의 백엔드 성능 개선을 하고 있다.

VUser = 500일 때 WAS를 5대까지 Scale-out을 하면서 59%의 요청 처리가 실패되던 것을 100%의 요청 처리 성공으로 성능 개선을 했다.

하지만, 95% 요청의 응답시간이 17.24s -> 29s로 속도가 느려졌다.

‘WAS가 1개일 때는 애플리케이션 단에서 트랜잭션 관리를 어느 정도 해 주는데, Scale-out을 하니 별도의 WAS 5개가 하나의 DB를 동시에 찔러서 과부하가 걸리나?’라는 생각이 들었다.

그래서 우테코 크루 현구막블로그 포스팅을 보며 DB Replication을 적용해보려 한다.

여기에서 DB Replication의 목적은 Scale-out을 통한 성능(응답 속도) 향상이다.

Master DB 서버 생성 및 설정

AWS에서 Master DB를 위한 EC2를 새로 생성하겠다. (과정 설명은 생략한다.)

EC2 이름 출력 설정

AWS에서 처음 EC2를 생성해 터미널로 접속하면

위와 같이 나온다.

내가 지금 어느 EC2 인스턴스에 접속해있는지 쉽게 구분하기 위해, IP 부분을 이름으로 바꿔보자.

$ sudo vim ~/.bashrc

위의 명령어로 파일을 연다.

USER=${설정할 이름}
PS1='[\e[1;31m$USER\e[0m][\e[1;32m\t\e[0m][\e[1;33m\u\e[0m@\e[1;36m\h\e[0m \w] \n\$ \[\033[00m\]'

위의 내용을 파일의 맨 아래에 붙여넣고 저장한 뒤, 홈 화면으로 나온다.

지금은 Mater DB 서버에 접속해 있기 때문에, ${설정할 이름}DB-MASTER이라고 입력하겠다.

$ source ~/.bashrc

위의 명령어를 입력해, 변경된 설정을 반영한다.

그러면 위와 같이, DB-MASTER이라는 이름이 앞에 출력된다.

MySQL server 설치

현재 EC2의 OS는 Ubuntu이다.

Ubuntu에서 mysql-server를 설치하는 법을 소개한다.

$ sudo apt update

먼저, 위의 명령어를 입력해 업데이트한다.

$ sudo apt install mysql-server

그다음, 위의 명령어를 입력해 mysql-server를 설치한다.

root 계정 비밀번호 변경

$ sudo mysql -u root -p

설치가 완료되었으면, 위의 명령어를 입력해 root 계정으로 접속한다.

그러면 비밀번호를 입력하라고 Enter password: 가 나올텐데,

맨 처음에는 바로 엔터를 치면 접속된다.

이제 root 계정의 비밀번호를 변경하자.

mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${새로운 비밀번호}';

위의 명령어를 입력해 root 계정의 비밀번호를 변경한다.

문자 인코딩 utf8mb4 설정 & 테이블 명 대소문자 구분 X 설정

Ubuntu home으로 나온다.

$ sudo vim /etc/mysql/my.cnf

위의 명령어를 입력해 MySQL 설정 파일을 연다.

[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

lower_case_table_names=1

파일의 맨 마지막에 위의 내용들을 붙여넣고, 저장한 후에 나온다.

$ sudo service mysql restart

변경된 설정을 반영하기 위해, 위의 명령어를 입력해 MySQL을 재시작한다.

$ sudo mysql -u root -p

잘 반영되었는지 확인하기 위해, 위의 명령어로 MySQL에 로그인한다.

비밀번호는 앞에서 변경한 비밀번호로 입력해야 한다.

mysql> SHOW VARIABLES LIKE 'lower_case_table_names';

위의 명령어를 입력한다.

위와 같이 Value 값이 1이 나오면 테이블 명의 대소문자 구분을 하지 않는 설정이 잘 적용된 것이다.

mysql> SHOW VARIABLES LIKE 'c%';

위의 명령어를 입력한다.

위처럼 utf8mb4, utf8, utf8mb4_unicode_ci가 나오면, 이모지도 인식할 수 있는 문자 설정이 잘 된 것이다.

Ubuntu Port Redirect 설정

우테코 AWS 보안그룹에 3306은 Private IP 사이에서만 열려있다.

Public으로 열려있는 것은 9000번 포트이다.

하지만, mysql-server는 3306으로 실행 중이다.

따라서 외부에서 9000번 포트로 접속 -> EC2에서 3306포트로 REDIRECT -> mysql-server 에 3306포트로 접속

위의 과정으로 mysql-server에 접속하려 한다.

이 설정은 EC2 Ubuntu에서 아래의 명령어를 입력하면 된다.

sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport ${외부에서 들어오는 포트 번호} -j REDIRECT --to-port ${REDIRECT 되어 바뀐 결과의 포트 번호}

즉, 실제 명령어는 아래와 같다.

sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 9000 -j REDIRECT --to-port 3306

Database와 USER

:bangbang: 이 글은 아래의 내용들을 전제로 하고있다.

  • 기존에 사용중인 Database가 존재한다.
  • DatabaseSELECT, INSERT, UPDATE, DELETE 등 적절한 권한이 부여되어있는 USER 계정이 존재한다.

MySQL Replication용 USER 생성

mysql> CREATE USER '${생성할 Replication용 USER 이름}'@'%' IDENTIFIED BY '${설정할 비밀번호}';

위의 명령어를 입력해, 새로운 Replication용 USER를 생성한다.

mysql> GRANT REPLICATION SLAVE ON *.* TO '${새로 생성한 Replication용 USER 이름}'@'%' IDENTIFIED BY '${비밀번호}';

위의 명령어를 입력해, 새로 생성한 USER에게 모든 DB에 대한 Replication 권한을 부여한다.

mysql> exit;

위의 명령어를 입력해, Ubuntu home으로 나온다.

$ sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

위의 명령어를 입력해 MySQL 설정 파일을 연다.

위와 같이 bind-address 부분을 127.0.0.1에서 0.0.0.0으로 변경해, Slave DB 서버WAS 서버로부터의 접근을 허용한다.

설정 파일의 아랫부분을 보면, 위 빨간 네모 부분이 주석처리 되어있을 것이다. 이 주석들을 해제한다.

파일의 변경사항을 저장하면서 밖으로 나온다.

$ sudo service mysql restart

위의 명령어를 입력해, MySQL을 재시작한다.

MySQL에 재접속한 뒤,

mysql> SHOW MASTER STATUS\G;

위의 명령어를 입력한다.

위와 같이 출력되면, 성공한 것이다.

Slave DB 서버 생성 및 설정

아래의 과정들은 중복되므로 생략한다.

  • EC2 생성
  • EC2 이름 표시 설정
  • mysql-server 설정
  • root 계정 비밀번호 변경과정
  • 문자 인코딩 utf8mb4 설정 & 테이블 명 대소문자 구분 X 설정
mysql> CREATE USER '${생성할 USER 이름}'@'%' IDENTIFIED BY '${설정할 비밀번호}';

위의 명령어를 입력해, 새로운 USER를 생성한다.

mysql> CREATE DATABASE ${생성할 DB 이름};

그리고, 위의 명령어를 입력해 새로운 DB를 생성한다.

:bangbang: ${생성할 DB 이름}Master DB와 동일하게 생성한다.

mysql> GRANT ALL PRIVILEGES ON ${새로 생성한 DB 이름}.* TO '${새로 생성한 USER 이름}'@'%';

위의 명령어를 입력해, 새로 생성한 USER에게 새로 생성한 DB에 대한 모든 권한을 부여한다.

mysql> exit;

위의 명령어를 입력해, Ubuntu home으로 나온다.

$ sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

위의 명령어를 입력해 MySQL 설정 파일을 연다.

위와 같이 bind-address 부분을 127.0.0.1에서 0.0.0.0으로 변경해, Slave DB 서버WAS 서버로부터의 접근을 허용한다.

설정 파일의 아랫부분을 보면, 위 빨간 네모 부분이 주석처리 되어있을 것이다. 이 주석들을 해제한다.

:bangbang: 위의 이미지에서 server-id 값을 Master DB에서 1로 설정한 것과 다르게 2로 설정했다.

server_id 값은 복제 구성에 포함된 MySQL 서버들이 각자 고유하게 갖고있는 식별 값이다.

이 값은 바이너리 로그에 쌓이는 이벤트들의 이벤트가 최초로 발생한 MySQL이 어디인지 식별하기 위해 사용되고, 기본값은 모두 1이다.

만약 Master DB와 Slave DB의 server-id 값이 동일한 경우, Master DB에서 발생한 이벤트라도 해당 바이너리 로그에 적혀있는 server-id가 Slave DB와 같으므로, 해당 Slave DB는 자기 자신이 발생시킨 이벤트로 보고 동기화를 진행하지 않는다.

따라서, 복제 구성에 포함된 MySQL 서버들은 각자 고유한 server-id를 갖도록 설정해야 한다.

파일의 변경사항을 저장하면서 밖으로 나온다.

$ sudo service mysql restart

위의 명령어를 입력해, MySQL을 재시작한다.

Master & Slave 스키마 동기화

Replication 연결을 하려면, Master DBSlave DB의 스키마 및 내부 데이터가 모두 동일해야 한다.

이를 위해 Slave DB를 덤핑하자.

:bangbang: 기존 운영 DB는 Slave DB였다.

즉, 기존의 DB 스키마와 데이터가 존재하는 DB는 Slave DB이다.

Slave DB 서버에 접속하고, 다음 명령어를 입력한다.

$ sudo mysqldump -u ${계정 이름} -p ${덤프할 DB 스키마 이름} > ${내보낼 덤프 파일명}.sql

ls 명령어를 입력해 잘 덤핑이 됐는지 확인하자.

scp 명령어를 통해, 덤프 파일을 Slave DB에서 Master DB 서버로 보내자.

Slave DB 서버에서 Master DB 서버로 덤프 파일을 보내려면

Slave DB 서버 -> 로컬 PC -> Master DB 서버

의 경로로 덤프 파일을 전송해야 한다.

Public Key를 사용해 Slave DB 서버 EC2 -> Master DB 서버 EC2로 직접 보낼수도 있지만, 현재 우테코 AWS EC2 보안그룹에 22번 포트가 일부 막혀있어서, 로컬 PC를 통한 방법으로 진행하겠다.

scp 명령어를 사용한다.

로컬 PC에서 터미널을 켜고, 아래의 명령어를 입력한다.

$ scp -i ${AWS EC2 접속용 private 키페어 경로} ubuntu@${Slave DB 서버 퍼블릭 IP}:${Slave DB 상에서 덤프 파일 위치} ${로컬 PC에서 덤프 파일을 받을 위치}

덤프 파일을 로컬 PC에 받았으면, 아래의 명령어를 입력해 다시 Master DB로 전송한다.

$ scp -i ${AWS EC2 접속용 private 키페어 경로} ${전송할 덤프 파일 경로} ubuntu@${Master DB 서버 퍼블릭 IP}:${Master DB 상에서 덤프 파일을 받을 위치}

Slave DB 서버에 접속한다.

$ sudo mysql -u root -p ${덤프 파일을 적용할 DB 이름} < ${덤프 파일명}.sql

위의 명령어를 입력해 덤프 파일에 있는 데이터를 Master DB에 주입한다.

주입이 완료되었으면, DB 안에 접속해서 주입이 잘 됐는지 확인한다.

주입이 잘 됐으면, Slave DB에 접속한다.

mysql> USE mysql;

위의 명령어를 입력해, mysql DB를 선택한다.

mysql> CHANGE MASTER TO MASTER_HOST='${Master DB IP}', MASTER_PORT=${Master DB 포트번호}, MASTER_USER='${Master DB USER 이름}', MASTER_PASSWORD='${Master DB USER의 비밀번호}', MASTER_LOG_FILE='${Master DB의 바이너리 로그 파일명}', MASTER_LOG_POS=${POS };
  • MASTER_USERMASTER_PASSWORD는 앞서 Master DB에서 생성해둔 Replication 전용 계정, 비밀번호를 입력한다.
  • MASTER_LOG_FILE은 앞서 MASTER DB에서 SHOW MASTER STATUS 명령어를 입력해 확인한 바이너리 로그 파일의 이름을 명시한다. (ex. mysql-bin.0.000001)
  • MASTER_LOG_FILE은 앞서 MASTER DB에서 SHOW MASTER STATUS 명령어를 입력해 확인한 바이너리 로그 파일 위치를 명시한다. (ex. 154)

위의 설명에 따라 명령어를 입력한다.

mysql> START SLAVE;

위의 명령어를 입력해 SLAVE를 시작시키고,

mysql> SHOW SLAVE STATUS\G;

위의 명령어를 입력해 상태를 확인한다.

위와 같이 출력되어야 성공한 것이다.

임의의 값을 Master DB에 INSERT 해서, 정말 SLAVE DB들에도 INSERT 되는지 테스트해 보자.

Master-Slave 연결 실패

만약 Slave_IO_State, Slave_IO_Running, Slave_SQL_Running 등이 다르게 출력될 경우 연결에 실패한 것이다.

Master 서버의 IP, 포트 번호, replication 용 계정, 비밀번호를 잘못 입력했을 가능성이 있다.

쉽고 빠르게 확인해볼 방법으로는 Slave 서버 터미널에서 아래 명령을 통해 replication 용 계정으로 직접 Master DB에 접속해보는 것이다.

$ mysql -h ${Master DB 서버 IP} -u ${Replication 권한이 부여된 Master DB의 계정} -p
Enter password: ${비밀번호}

접속이 성공하는 경우, Slave DB와 Master DB의 스키마 구조가 다른 것이다.

dump 적용이 잘 되었는지 다시 확인한다.

원인을 해결했다면, 아래의 과정을 따라 Slave DB에서 Master DB로 연결을 재시도한다.

mysql> RESET SLAVE ALL;

위의 명령어를 입력해 모든 SLAVE를 리셋한다.

mysql> SHOW SLAVE STATUS\G;

모든 SLAVE들이 제대로 RESET 됐다면, 위의 명령어를 입력했을 때 Empty set (0.00 sec)가 출력되어야 한다.

mysql> CHANGE MASTER TO MASTER_HOST='${Master DB IP}', MASTER_PORT=${Master DB 포트번호}, MASTER_USER='${Master DB USER 이름}', MASTER_PASSWORD='${Master DB USER의 비밀번호}', MASTER_LOG_FILE='${Master DB의 바이너리 로그 파일명}', MASTER_LOG_POS=${POS };

Master DB 정보를 다시 설정하고,

mysql> START SLAVE;

SLAVE를 시작한다.

Spring Boot Datasource 설정

기존 Spring Boot properties 파일에서 spring.datasource 에 관련 정보를 기입하면 자동으로 datasource가 등록되었다. 하지만 Replication을 이용하려면 여러 개의 datasource를 동시에 등록해야 하므로, 자동 등록 기능을 이용할 수 없다. 직접 각각의 datasource들을 적용해주어야 한다.

즉, properties 파일에 기재한 datasource 내용을 코드로 직접 읽어서 등록해야 한다.

spring:
  # 새로운 커스텀
  # 어떤 양식을 이용하든지 본인 자유
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${Master DB 서버 IP}:${포트 번호}/${DB 이름}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: ${Master DB 계정 이름}
    password: ${Master DB 계정 비밀번호}

    slaves:
      slave1:
        name: slave1
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://${Slave DB 서버 IP}:${포트 번호}/${DB 이름}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
        username: ${Slave DB 계정 이름}
        password: ${Slave DB 계정 비밀번호}

# 여기부턴 JPA가 읽고 해석하므로 자유 아님
  jpa:
    hibernate:
      ddl-auto: validate
      generate-ddl: false
    properties:
      hibernate:
        show_sql: false
        format_sql: false
        physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
        jdbc:
          lob:
            non_contextual_creation: true

physical_naming_strategy의 값 SpringPhysicalNamingStrategy는 네이밍 전략 설정이다.

설정을 생략할 경우 테이블/칼럼 명이 자바에서 사용하는 camel case 그대로 사용된다. (datasource 자동 등록 시 얼마나 많은 기본설정들이 포함되어 있는지 알 수 있는 대목)

non_contextual_creation 설정은 createClob() 메서드를 구현하지 않았다는 Hibernate의 에러 로그를 보여주지 않는 설정이다.

Spring Boot properties 설정 작성이 완료되었다면, 임의로 작성한 DataSource를 @ConfigurationProperties 애노테이션을 통해 객체로 매핑해주자.

Properties 클래스

@Setter
@Getter
@ConfigurationProperties(prefix = "spring.datasource")
public class ReplicationDataSourceProperties {

    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private final Map<String, Slave> slaves = new HashMap<>();

    @Setter
    @Getter
    public static class Slave {

        private String name;
        private String driverClassName;
        private String url;
        private String username;
        private String password;
    }
}

필드 변수명 하나하나가 yml 파일에서 설정한 이름과 매칭되어야 함을 주의하자.

@ConfigurationProperties 애노테이션을 처음 사용하면 에러를 마주할 수 있는데,

공식 문서를 확인해보면 의존성 추가를 통해 해결할 수 있음을 알려준다.

dependencies {
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}

다음은 @Transactional(readOnly=true) 분기 처리를 위한 로직을 작성해야 한다. 서비스 로직을 수행할 때 메서드에 붙은 애노테이션이 @Transactional(readOnly=true)인 경우 Slave Datasource로, 나머지는 Master Datasource로 분기 처리를 하기위한 RoutingDataSource 클래스를 생성한다.

RoutingDataSource 클래스

@Slf4j
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    private static final String DATASOURCE_KEY_MASTER = "master";
    private static final String DATASOURCE_KEY_SLAVE = "slave";

    private DataSourceNames<String> slaveNames;

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);

        List<String> replicas = targetDataSources.keySet()
            .stream()
            .map(Object::toString)
            .filter(string -> string.contains(DATASOURCE_KEY_SLAVE))
            .collect(toList());

        this.slaveNames = new DataSourceNames<>(replicas);
    }

    // 요청에서 사용할 DataSource Key 값 반환
    @Override
    protected Object determineCurrentLookupKey() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();

        if (isReadOnly) {
            final String nextSlaveName = slaveNames.getNext();
            log.info("Connection Slave : {}", nextSlaveName);
            return nextSlaveName;
        }

        log.info("Connection Master");
        return DATASOURCE_KEY_MASTER;
    }

    public static class DataSourceNames<T> {

        private final List<T> values;
        private int index = 0;

        public DataSourceNames(List<T> values) {
            this.values = values;
        }

        public T getNext() {
            if (index + 1 >= values.size()) {
                index = -1;
            }
            return values.get(++index);
        }
    }
}

ReplicationRoutingDataSource 클래스는 AbstractRoutingDataSource를 상속하여 구현해야 한다. AbstractRoutingDataSourcespring-jdbc 모듈에 포함되어 있는 클래스로, 복수의 datasource를 등록하고 상황에 맞게 원하는 datasource를 사용할 수 있도록 추상화한 클래스이다.

determineCurrentLookupKey() 메서드 오버라이딩을 통해 현재 요청에서 필요한 Master/Slave 분기를 진행하고 사용할 datasource의 key 값을 반환해준다.

여러 개의 Slave 서버를 골고루 사용해서 부하를 분산시킬 수 있도록 원형 연결리스트 형태의 클래스도 사용한다.

이를 통해 요청마다 인덱스가 증가/감소하면서 모든 Slave 서버를 순회할 수 있게 된다.

다음은 실제 datasource를 스프링부트에 등록하기 위한 config 클래스를 생성한다.

DataSourceConfig 클래스

@Profile({"local", "prod"})
// @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(ReplicationDataSourceProperties.class)
@Configuration
public class ReplicationDataSourceConfig {

    private final ReplicationDataSourceProperties dataSourceProperties;
    private final JpaProperties jpaProperties;

    public ReplicationDataSourceConfig(ReplicationDataSourceProperties dataSourceProperties,
                                       JpaProperties jpaProperties) {
        this.dataSourceProperties = dataSourceProperties;
        this.jpaProperties = jpaProperties;
    }

    // DataSourceProperties 클래스를 통해 yml 파일에서 읽어 들인 DataSource 설정들을 실제 DataSource 객체로 생성 후 ReplicationRoutingDataSource에 등록
    @Bean
    public DataSource routingDataSource() {
        DataSource masterDataSource = createDataSource(
            dataSourceProperties.getDriverClassName(),
            dataSourceProperties.getUrl(),
            dataSourceProperties.getUsername(),
            dataSourceProperties.getPassword()
        );

        Map<Object, Object> dataSources = new LinkedHashMap<>();
        dataSources.put("master", masterDataSource);

        for (Slave slave : dataSourceProperties.getSlaves().values()) {
            DataSource slaveDatSource = createDataSource(
                slave.getDriverClassName(),
                slave.getUrl(),
                slave.getUsername(),
                slave.getPassword()
            );

            dataSources.put(slave.getName(), slaveDatSource);
        }

        ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();
        replicationRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
        replicationRoutingDataSource.setTargetDataSources(dataSources);

        return replicationRoutingDataSource;
    }

    // DataSource 생성
    public DataSource createDataSource(String driverClassName, String url, String username, String password) {
        return DataSourceBuilder.create()
            .type(HikariDataSource.class)
            .url(url)
            .driverClassName(driverClassName)
            .username(username)
            .password(password)
            .build();
    }

    // 매 쿼리 수행마다 Connection 연결
    @Bean
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(routingDataSource());
    }

    // JPA에서 사용하는 EntityManagerFactory 설정. hibernate 설정을 직접 주입한다.
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        EntityManagerFactoryBuilder entityManagerFactoryBuilder = createEntityManagerFactoryBuilder(jpaProperties);
        return entityManagerFactoryBuilder.dataSource(dataSource())
            .packages("{프로젝트 패키지 경로 ex) gg.babble.babble}")
            .build();
    }

    private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) {
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        return new EntityManagerFactoryBuilder(vendorAdapter, jpaProperties.getProperties(), null);
    }

    // JPA에서 사용할 TransactionManager 설정
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        return tm;
    }
}

가장 첫 줄을 보면 주석 처리가 되어있다. exclude 옵션을 이용해 DataSource 자동설정(DataSourceAutoConfiguration)을 제외시킬 수 있으나, Spring 공식문서에서 DataSourceAutoConfiguration에 대해 침투적이지 않다(Auto-configuration is non-invasive)고 평가한다. 때문에 굳이 제외시킬 필요 없다고 생각해서 포함하지 않았다.

위 코드에서 눈에 띄는 부분은 LazyConnectionDataSourceProxy()일 것이다. 우선 Spring은 트랜잭션에 진입하는 순간 이미 설정된 DataSource의 커넥션을 가져온다. TransactionManager가 트랜잭션을 식별하면 DataSource의 커넥션을 가져오고, 트랜잭션의 동기화가 시작되어버린다. 이럴 경우 다중 DataSource 환경에서는 DataSource를 선택하는 분기가 불가능하다.

따라서 미리 DataSource를 정하지 않도록 LazyConnectionDataSourceProxy를 사용하여 실제 쿼리가 실행될 때 Connection을 가져오도록 한 것이다.

설정을 모두 마쳤으면 애플리케이션을 실행해보자. 다른 이슈가 없다면 정상적으로 서비스가 진행되며 로거를 통해 Master/Slave 데이터베이스를 번갈아서 사용하고 있음을 확인할 수 있다.

정리

  1. 스프링 프로필 파일에 작성된 datasource 정보들을 DataSourceProperties 클래스를 통해 수동으로 매핑한다.
  2. isReadOnly 옵션값에 따라 Master/Slave DB 서버를 선택한다.
  3. 리스트를 순환하면서 Slave 서버를 선택하도록 해서, Slave 서버 부하를 분산시킨다.
  4. 매핑된 DataSource 설정들을 실제 DataSource 객체로 생성 후 ReplicationRoutingDataSource에 등록한다.
  5. JPA가 사용할 EntityManagerFactory를 수동으로 설정한다.
  6. JPA가 사용할 TransactionManager를 수동으로 설정한다.
  7. 서비스 로직 메서드마다 datasource가 바뀌어야하므로, LazyConnectionDataSourceProxy를 통해 proxy datasource를 연결하도록 설정한다.

참고자료

댓글남기기