SpringBoot Master/Slave DB 세팅

2024. 2. 25. 12:42·Spring

SpringBoot Master/Slave DB 세팅

 

기존에 DB 를 하나만 사용하던 스프링 부트(버전 2.7.12) 프로젝트가 있다. 이 어플리케이션의 DB 세팅은 아래와 같이 application.properties 에 하나의 DB만 세팅 되어 있다.

spring.datasource.url=IP
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.username=계정
spring.datasource.password=비밀번호
spring.datasource.hikari.pool-name=풀이름
기타등등 hikari connection pool 세팅

 

이렇게 세팅을 해주면 스프링에서 알아서 데이터 소스를 만들어주고 사용자는 비즈니스 로직만 작성하면 된다. 하지만 두개의 DB 를 사용해야 한다면 개발자가 직접 DataSource 를 생성하는 코드를 작성해야 한다. 또한 기존의 코드도 수정해야 할 일이 생길 수 있다. 두개의 DataSource 중에 어떤 DataSource 를 선택해야 하는지를 결정해줘야 하기 때문이다.

 

지금 프로젝트는 아래와 같은 규칙을 가지고 있다.

- DB 1개 사용

- READ 기능 : @Transactional(readOnly=true) 사용

- CREATE, UPDATE, DELETE 기능 :  @Transactional 사용

 

이 프로젝트의 DB 세팅을 두개로 변경해야 한다. CUD 를 수행하는 Master db와 READ 만 수행하는 Slave db 로 구성하기로 했고,  기존에 존재하던 기능의 코드 수정은 없이 변경된 DB 세팅만 적용하고 싶다. Transactional 의 readOnly 값이 true 이면 Slave DB 를 사용하고 그 외의 @Transactional 에서는 Master DB 를 사용하고, 이러한 일을 하기 위해 RoutingDataSource 를 사용한다.

 

application.properties 수정

 

일단 아래와 같이 application.properties 를 수정했다.

db.master.datasource.url=MASTER DB IP:PORT
db.master.datasource.driver-class-name=org.mariadb.jdbc.Driver
db.master.datasource.username=계정
db.master.datasource.password=비밀번호
db.master.datasource.hikari.pool-name=master-db-pool
기타등등 hikari connection pool 세팅

db.slave.datasource.url=SLAVE DB IP:PORT
db.slave.datasource.driver-class-name=org.mariadb.jdbc.Driver
db.slave.datasource.username=계정
db.slave.datasource.password=비밀번호
db.slave.datasource.hikari.pool-name=slave-db-pool
기타등등 hikari connection pool 세팅

 

사용할 DB 정보를 입력한다. 

 

DataSource 생성

    @Bean
    @ConfigurationProperties(prefix = "db.master.datasource")
    public DataSourceProperties dbMasterDataSourceProp() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties(prefix = "db.master.datasource.hikari")
    public DataSource dbMasterDataSource() {
        return dbMasterDataSourceProp().initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = "db.slave.datasource")
    public DataSourceProperties dbSlaveDataSourceProp() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties(prefix = "db.slave.datasource.hikari")
    public DataSource dbSlaveDataSource() {
        return dbSlaveDataSourceProp().initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

 

RoutingDataSource 를 만들기 전에 application.properties 의 DB 정보를 읽어 DataSource 를 만들어준다.

 

DataSourceRouter 구현

 

RoutingDataSource 스프링 빈을 만들기 전에 AbstractRoutingDataSource 를 상속받은 구현체가 필요하다. 

public class RoutingDataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            System.out.println("================== determineCurrentLookupKey: " + "slave");
            return "slave";
        } else {
            System.out.println("================== determineCurrentLookupKey: " + "master");
            return "master";
        }
        // 아래와 같은 코드로 충분하나 확인을 위해 위와 같이 코딩을 했다.
        //return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master";
    }
}

 

AbstractRoutingDataSource 를 상속받고 determineCurrentLookupKey 를 오버라이딩 해야 한다. 이 메서드는 DataSource 를 고르기 위한 Key 를 제공하는 역할을 한다. 위 예시 코드에서는 트랜잭션에 readOnly 가 있으면 slave, 아니면 master 라는 문자열을 반환한다.

 

RoutingDataSource 스프링 빈 생성

    @Bean
    @Primary // 위에서 생성한 DataSource 빈이 있기 때문에 primary 를 붙여줘야 한다.
    public DataSource routingMasterSlaveDataSource() {
        DataSource masterDataSource = dbMasterDataSource(); // 위에서 만든 Master DataSource
        DataSource slaveDataSource = dbSlaveDataSource(); // 위에서 만든 Slave DataSource

        RoutingDataSourceRouter routingDataSourceRouter = new RoutingDataSourceRouter();
        Map<Object, Object> datasourceMap = new HashMap<>() {
            {
                put("master", masterDataSource);
                put("slave", slaveDataSource);
            }
        };
        routingDataSourceRouter.setTargetDataSources(datasourceMap);
        routingDataSourceRouter.setDefaultTargetDataSource(masterDataSource);
        return routingDataSourceRouter;
    }

 

위에서 determineCurrentLookupKey 메서드를 통해 Key 를 얻을 수 있다고 했다. 이 Key 가 datasourceMap 을 조회하기 위해서 사용되는 것이다. 

 

 

LazyConnectionDataSourceProxy 생성

RoutingDataSource 를 바로 사용하면 master, slave 라우팅이 안된다. 왜냐하면 스프링에서 트랜잭션을 시작할 때 트랜잭션 동기화 이전에 RoutingDataSource 빈에 디폴트로 지정해둔 master DataSource 를 통해 Connection 을 얻기 때문이다. 트랜잭션 동기화 이후에 DataSource 를 정하고 Connection 을 가져오게 하기 위해서 LazyConnectionDataSourceProxy 로 RoutingDataSource 를 감싸줘야 한다. 그러면 트랜잭션 동기화 이전에 Connection Proxy를 획득하고 이후에 readOnly 상태에 따라 DataSource 를 정하게 만들 수 있다.

    @DependsOn("routingMasterSlaveDataSource") // routingDataSource 가 먼저 있어야 함
    @Bean
    public LazyConnectionDataSourceProxy lazyConnectionDataSource(
            @Qualifier("routingMasterSlaveDataSource") DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

 

lazyConnectionDataSource 빈을 만들기 위해선 routingMasterSlaveDataSource 가 만들어져 있어야 하기 때문에 @DependsOn 어노테이션을 사용했다.

 

EntityManagerFactory 생성

    @Bean(name = "dbEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean dbEntityManagerFactory(@Qualifier("lazyConnectionDataSource") DataSource dataSource) {
        HibernateJpaVendorAdapter vendor = new HibernateJpaVendorAdapter();
        vendor.setGenerateDdl(false);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendor);
        factory.setDataSource(dataSource);
        factory.setJpaDialect(new HibernateJpaDialect());
        factory.setPersistenceUnitName("db-em");

        Map<String, String> map = new HashMap<>();
        map.put("hibernate.show_sql ", env.getProperty("db.jpa.show-sql"));
        factory.setJpaPropertyMap(map);
        return factory;
    }

 

LazyConnectionDataSource 를 사용하는 EntityManagerFactory 를 만든다. 필요한 하이버네이트 세팅도 여기서 추가해주면 된다. 

 

TransactionManager 생성

    @Bean(name = "dbTransactionManager")
    public PlatformTransactionManager dbTransactionManager(@Qualifier("dbEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
        jpaTransactionManager.setEntityManagerFactory(entityManagerFactory);
        jpaTransactionManager.setPersistenceUnitName("dbTransactionManager");
        return jpaTransactionManager;
    }

 

위에서 만든 EntityManagerFactory 를 통해 트랜잭션 매니저를 만들게 설정을 한다.

 

이제 MasterDB/SlaveDB 를 용도에 맞게 사용할 수 있는 설정이 마무리되었다. 이제 @Transactional(readOnly=true) 를 호출하면 determineCurrentLookupKey() 메서드에 있는 slave 로그가 찍히고, readOnly가 false 가 아닌 @Transaction 을 호출하면 master 로그가 찍히는 것을 확인할 수 있다.

 

세팅이 실제로 어떻게 동작하는지도 알아보겠습니다.

링크 : 글 작성중

반응형
'Spring' 카테고리의 다른 글
  • SpringBoot Master/Slave DB 세팅 2
  • OAS 사용해 API 문서 작성하기
  • Resilience4jFeign Java Record 문제
  • Tomcat 실행시 스프링 내부 동작과정
Jadie Blog
Jadie Blog
  • Jadie Blog
    Jadie
    Jadie Blog
  • 전체
    오늘
    어제
    • 분류 전체보기 (44)
      • OOP (7)
      • DDD (1)
      • JAVA (8)
      • Spring (12)
      • Kafka (1)
      • TDD,Test (4)
      • Basic (1)
      • ETC (1)
      • MySQL (0)
      • Javascript (0)
      • Spark (3)
      • Infra (2)
      • Algorithm (0)
      • Network (1)
      • Jobs (0)
      • 일상 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

    • 휴튼
  • 공지사항

  • 인기 글

  • 태그

    메시징시스템
    MASTER
    Spring #ApplicationContext
    Spring
    localdatetime
    springboot
    OOP
    MSA
    jpa
    테스트
    Test
    routingdatasource
    객체지향사실과오해
    의존역전원칙
    우아한스터디
    Kafka
    java
    글또
    캡슐화
    Transactional Outbox
    Resilience4jFeign
    추상클래스 #인터페이스
    JPQL
    OAuth2 #Spring
    HTTP #HTTPS
    객체지향
    entitymanager
    JAVA #IO
    API문서
    slave
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
Jadie Blog
SpringBoot Master/Slave DB 세팅
상단으로

티스토리툴바