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

Ilyoung Hwang·2022년 5월 29일
0

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개의 댓글