보통 서비스의 규모가 작은 경우에 한개의 DataBase 에서 Read
와 Write
작업을 모두 수행 하도록 구현된다. 하지만, 점차 서비스 규모가 커짐에 따라 한 곳에서 모든 작업을 처리하기엔 병목 현상이 발생할 위험이 높아진다.
이를 예방하기 위해 Replication 방법을 사용하게 된다. 이 블로그에서는 DataBase Replication 설정은 건너 뛴다.
여기서 Replication
란 하나 이상의 다른 데이터 베이스(복제본)로 데이터를 복사하는 방법이다. master / slave 나눠 양쪽에 동일한 데이터를 저장 후, master
엔 Write 작업을 slave
엔 Only Read 작업만 처리 되도록 Spring Boot 에서AbstractRoutingDataSource
라는 기능을 제공한다.
AbstractRoutingDataSource
는 조회 키를 기반으로 getConnection() 호출을 다양한 DataSource 중 하나로 라우팅하는 추상 클래스 이다. 이 동적 데이터 소스 라우딩을 구하는데 필요한 4가지가 있다.
determineCurrentLookupKey() 는 AbstractRoutingDataSource 의 추상 메소드로 현재 대상 DataSource를 검색한다. 현재 조회 키를 결정하고, targetDataSources 맵에서 조회를 수행하고, 필요한 경우 지정된 기본 대상 DataSource로 대체한다.
Threadlocal 는 컨텍스트가 현재 실행 중인 스레드에 바인딩되도록 Threadlocal을 사용하여 스레드 바인딩되는 컨텍스트를 결정하는 Context Holder 구성 요소이다.
ReplicationType 는 현재 대상 DataSource 를 결정하기 위한 조회 키로 사용된다.
DataSource Bean, Entity 클래스
master, slave 데이터 베이스 정보를 입력한다.
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/employees?
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3307/comployees?
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
package com.spring.oauth2.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Department {
private String deptNo;
private String deptName;
}
package com.spring.oauth2.service;
import com.spring.oauth2.domain.Department;
import com.spring.oauth2.repository.DepartmentsRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
public class DepartmentsService {
@Autowired
DepartmentsRepository departmentsRepository;
public List<Department> getDepartments() {
List<Department> deptManagers = departmentsRepository.getDepartments();
return deptManagers;
}
@Transactional
public void updateDeptNameByDeptNo() {
int updateCount = departmentsRepository.updateDeptNameByDeptNo();
System.out.println(updateCount);
}
}
package com.spring.oauth2.repository;
import com.spring.oauth2.domain.Department;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface DepartmentsRepository {
List<Department> getDepartments();
int updateDeptNameByDeptNo();
}
package com.spring.oauth2.config.datasource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "spring.datasource.master")
public class MasterDetails {
private String url;
private String username;
private String password;
private String driverClassName;
}
package com.spring.oauth2.config.datasource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "spring.datasource.slave")
public class SlaveDetails {
private String url;
private String username;
private String password;
private String driverClassName;
}
Master 또는 Slave DataSource 로 Routing 할 DataSource 를 구성한다.
package com.spring.oauth2.config.datasource;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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 ReplicationRoutingConfiguration {
@Autowired
MasterDetails masterDetails;
@Autowired
SlaveDetails slaveDetails;
@Primary
@Bean
public DataSource dataSource() {
return new LazyConnectionDataSourceProxy(replicationDataSource());
}
@Bean
public DataSource replicationDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
DataSource masterDataSource = masterDataSource();
DataSource slaveDataSource = slaveDataSource();
targetDataSources.put(ReplicationType.WRITE, masterDataSource);
targetDataSources.put(ReplicationType.READ, slaveDataSource);
ReplicationDataSourceRouter clientRoutingDatasource = new ReplicationDataSourceRouter();
clientRoutingDatasource.setTargetDataSources(targetDataSources);
clientRoutingDatasource.setDefaultTargetDataSource(masterDataSource);
return clientRoutingDatasource;
}
@Bean
public DataSource masterDataSource() {
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setDriverClassName(masterDetails.getDriverClassName());
hikariDataSource.setJdbcUrl(masterDetails.getUrl());
hikariDataSource.setUsername(masterDetails.getUsername());
hikariDataSource.setPassword(masterDetails.getPassword());
hikariDataSource.setMaximumPoolSize(10);
return hikariDataSource;
}
@Bean
public DataSource slaveDataSource() {
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setDriverClassName(slaveDetails.getDriverClassName());
hikariDataSource.setJdbcUrl(slaveDetails.getUrl());
hikariDataSource.setUsername(slaveDetails.getUsername());
hikariDataSource.setPassword(slaveDetails.getPassword());
hikariDataSource.setMaximumPoolSize(10);
return hikariDataSource;
}
}
데이터 소스에 대한 조회 키 역할을 하는 Enum 이다.
package com.spring.oauth2.config.datasource;
public enum ReplicationType {
READ, WRITE
}
ReplicationDataBaseContextHolder는 Thread 바인딩된 컨텍스트의 저장소 역할을 하는 구성 요소이다. 컨텍스트 설정, 검색 및 삭제하는데 사용 된다.
package com.spring.oauth2.config.datasource;
import org.springframework.util.Assert;
public class ReplicationDataBaseContextHolder {
private static ThreadLocal<ReplicationType> CONTEXT = new ThreadLocal<>();
public static void set(ReplicationType dataSourceType) {
Assert.notNull(dataSourceType, "dataSourceType cannot be null");
CONTEXT.set(dataSourceType);
}
public static ReplicationType get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
해당 코드 깃헙에서 확인할 수 있다. GitHub