Spring Boot 에서AbstractRoutingDatasource 사용한 Read, Write 분리

Ilyoung Hwang·2022년 5월 29일

1. 개요

보통 서비스의 규모가 작은 경우에 한개의 DataBase 에서 ReadWrite 작업을 모두 수행 하도록 구현된다. 하지만, 점차 서비스 규모가 커짐에 따라 한 곳에서 모든 작업을 처리하기엔 병목 현상이 발생할 위험이 높아진다.

이를 예방하기 위해 Replication 방법을 사용하게 된다. 이 블로그에서는 DataBase Replication 설정은 건너 뛴다.

여기서 Replication 란 하나 이상의 다른 데이터 베이스(복제본)로 데이터를 복사하는 방법이다. master / slave 나눠 양쪽에 동일한 데이터를 저장 후, master 엔 Write 작업을 slave 엔 Only Read 작업만 처리 되도록 Spring Boot 에서AbstractRoutingDataSource 라는 기능을 제공한다.

AbstractRoutingDataSource 는 조회 키를 기반으로 getConnection() 호출을 다양한 DataSource 중 하나로 라우팅하는 추상 클래스 이다. 이 동적 데이터 소스 라우딩을 구하는데 필요한 4가지가 있다.

  1. determineCurrentLookupKey() 는 AbstractRoutingDataSource 의 추상 메소드로 현재 대상 DataSource를 검색한다. 현재 조회 키를 결정하고, targetDataSources 맵에서 조회를 수행하고, 필요한 경우 지정된 기본 대상 DataSource로 대체한다.

  2. Threadlocal 는 컨텍스트가 현재 실행 중인 스레드에 바인딩되도록 Threadlocal을 사용하여 스레드 바인딩되는 컨텍스트를 결정하는 Context Holder 구성 요소이다.

  3. ReplicationType 는 현재 대상 DataSource 를 결정하기 위한 조회 키로 사용된다.

  4. DataSource Bean, Entity 클래스

2. 구현

2.1 application.yml

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

2.2 Department Entity

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;

}

2.3 DepartmentService

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);
    }


}

2.4 DepartmentRepository

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();
}

2.5 MasterDetail, SlaveDetail Configuration

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;

}

2.6 DataSource Bean 등록

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;
    }

}

2.7 Replication Type

데이터 소스에 대한 조회 키 역할을 하는 Enum 이다.

package com.spring.oauth2.config.datasource;

public enum ReplicationType {
    READ, WRITE
}

2.8 ReplicationDataBaseContextHolder

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();
    }
}

3. 결론

해당 코드 깃헙에서 확인할 수 있다. GitHub

0개의 댓글