Skip to content

DB Replication Spring Boot 적용

Jinho Huh edited this page Aug 20, 2021 · 26 revisions

내가 적용해보고 싶은 설정

  • @Transactional(readOnly=true)로 지정된 메소드에서 나가는 쿼리는 Slave DB로만 쿼리가 나가게 설정
  • 그 외에 readOnly=true로 지정하지 않은 메소드에서 나가는 쿼리는 Master DB로만 쿼리가 나가게 설정
  • 위와 같이 설정하더라도 Transactional의 기존 기능이 정상적으로 동작
  • 위와 같이 설정하더라도 JPA의 기본 동작에 영향이 없도록 설정
  • 추후 고민...

변경사항-1 application-prod.properties와 application-dev.properties로 분리

  • DB Replication 인프라 구성은 개발 서버의 DB에는 적용하지 않을 예정(Prod, 운영 서버에만 적용)
  • 기존 개발서버, 운영서버는 같은 prod properties로 실행하고 있었기 때문에 분리할 필요가 있음

Master/Slave URL과 Name에 대한 설정 Properties에 추가

### application-prod.properties

### master db
datasource.url=${DATASOURCE_URL}
datasource.username=${DATASOURCE_USERNAME}
datasource.password=${DATASOURCE_PASSWORD}

### slave-1 db
datasource.slave.slave1.name=slave-1
datasource.slave.slave1.url=${SLAVE1_URL}

### slave-2 db
datasource.slave.slave2.name=slave-2
datasource.slave.slave2.url=${SLAVE2_URL}

### sql 로깅 설정
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type=trace
logging.level.org.hibernate.type.descriptor.sql=trace
### application-dev.properties

datasource.url=${DATASOURCE_URL}
datasource.username=${DATASOURCE_USERNAME}
datasource.password=${DATASOURCE_PASSWORD}

변경사항-2 여러 개의 Datasource를 사용하도록 설정

  • 위에서 설정한 Master DB와 Slave DB들의 url에 대한 properties에 대한 설정을 객체로 가져오는 설정
  • Oauth Login에서 사용한 oauth-properties를 가져오는 걸 생각하면 이해하기 쉬움

prod-properties의 datasource 설정을 직접 가져옴

@Getter
@Setter
@ConfigurationProperties(prefix = "datasource")
public class CustomDataSourceProperties {
    private String url;
    private String username;
    private String password;
    private final Map<String, Slave> slave = new HashMap<>();

    @Getter
    @Setter
    public static class Slave {
        private String name;
        private String url;
    }
}

Slave Datasource 중 하나를 사용하기 위한 CircularList 구현

  • Slave List 중에서 Slave의 부하를 분산하기 위해 한 번씩 번갈아가면서 사용하기 위한 CircularList를 생성
public class CircularList<T> {
    private final List<T> list;
    private Integer counter = 0;

    public CircularList(List<T> list) {
        this.list = list;
    }

    public T getOne() {
        if (counter + 1 >= list.size()) {
            counter = -1;
        }
        return list.get(++counter);
    }
}

AbstractRoutingDataSource를 상속하여 동적으로 Datasource 동적으로 사용할 수 있게 함

  • readOnly가 true로 설정되어 있는 Transaction은 slave 중 하나의 datatsource를 사용하도록 지정
  • 나머지는 master datasource를 사용
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    private CircularList<String> dataSourceNameList;

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

        dataSourceNameList = new CircularList<>(
                targetDataSources.keySet()
                                 .stream()
                                 .filter(key -> key.toString().contains("slave"))
                                 .map(key -> key.toString())
                                 .collect(Collectors.toList())
        );
    }
    @Override
    protected Object determineCurrentLookupKey() {
        // readOnly가 true인 경우 
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        if(isReadOnly) {
            // slave를 사용하는 경우 확인하기 위한 로깅 설정
            logger.info("Connection Slave");
            // 위에서 CircularList에서 Slave 중에 하나를 사용
            return dataSourceNameList.getOne();
        } else {
            // master를 사용하는 경우 확인하기 위한 로깅 설정
            logger.info("Connection Master");
            // master 사용
            return "master";
        }
    }
}

변경사항-3 DataSourceAutoConfiguration 비활성화 및 Hibernate 설정 직접 주입

  • Spring Boot의 DataSourceAutoConfiguration을 사용하면 여러 개의 DataSource를 등록할 수 없다.
  • DataSourceAutoConfiguration을 비활성화하는 @Configuration을 추가해야 한다.
  • 그런데 이 DataSourceAutoConfiguration을 포기하면 기존의 application.properties를 사용하여 설정하던 Hibernate 설정들이 자동으로 등록되지 않는다.
  • 결국 직접 DataSourceConfiguration을 직접 작성하여 DataSourceAutoConfiguration 비활성화하고 Hibernate 설정을 직접 주입해줘야 한다.
  • 특히 Table, Column에 대한 네이밍 전략까지 직접 지정해줘야 한다.

Hibernate 설정 properties에 추가

### application-prod.properties
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.generate-ddl=false
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

# 참고 - https://velog.io/@mumuni/Hibernate5-Naming-Strategy-%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC 참고
# 이 설정은 Column과 Table의 명칭을 정하는 기준이 되는 전략에 대한 설정
# SpringPhysicalNamingStrategy - camel case를 underscore 형태로 변경
# PhysicalNamingStrategyStandardImpl - 기본 변수명을 그대로 사용

spring.jpa.properties.hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

... 

### master db
datasource.url=${DATASOURCE_URL}
datasource.username=${DATASOURCE_USERNAME}
datasource.password=${DATASOURCE_PASSWORD}

...

Hibernate properties 설정 JpaProperties로 가져오고 entityManagerFactory에 직접 주입

LazyConnectionDataSourceProxy을 사용하는 이유

  1. Spring은 기본적으로 트랜잭션을 시작할 때 쿼리가 실행되기도 전에 DataSource를 정해놓는다. (TransactionManager 식별 -> DataSource에서 Connection 가져오고 -> Transaction 동기화(Synchronization)
  2. Transaction이 시작되면 같은 DataSource만을 이용한다.
  • 우리가 원하는 방식은 쿼리를 실행할 때 DataSource를 정할 수 있도록 미리 DataSource 정하지 않고 늦춰주도록 구현

  • https://tjdrnr05571.tistory.com/15

@Configuration
@Profile("prod")
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableConfigurationProperties(CustomDataSourceProperties.class)
public class CustomDataSourceConfig {

    private final CustomDataSourceProperties databaseProperty;
    private final JpaProperties jpaProperties;

    public CustomDataSourceConfig(CustomDataSourceProperties databaseProperty, JpaProperties jpaProperties) {
        this.databaseProperty = databaseProperty;
        this.jpaProperties = jpaProperties;
    }

    public DataSource createDataSource(String url) {
        return DataSourceBuilder.create()
                                .type(HikariDataSource.class)
                                .url(url)
                                .driverClassName("com.mysql.cj.jdbc.Driver")
                                .username(databaseProperty.getUsername())
                                .password(databaseProperty.getPassword())
                                .build();
    }

    @Bean
    public DataSource routingDataSource() {
        DataSource master = createDataSource(databaseProperty.getUrl());

        Map<Object, Object> dataSourceMap = new LinkedHashMap<>();
        dataSourceMap.put("master", master);
        databaseProperty.getSlave()
                        .forEach((key, value) -> dataSourceMap.put(value.getName(), createDataSource(value.getUrl())));

        ReplicationRoutingDataSource replicationRoutingDataSource = new ReplicationRoutingDataSource();
        replicationRoutingDataSource.setDefaultTargetDataSource(master);
        replicationRoutingDataSource.setTargetDataSources(dataSourceMap);
        return replicationRoutingDataSource;
    }

    @Bean
    public DataSource dataSource() {
        return new LazyConnectionDataSourceProxy(routingDataSource());
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        EntityManagerFactoryBuilder entityManagerFactoryBuilder = createEntityManagerFactoryBuilder(jpaProperties);
        return entityManagerFactoryBuilder.dataSource(dataSource()).packages("com.wootech.dropthecode").build();
    }

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

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        return tm;
    }
}

테스트

GET /teachers?language=java

Transaction 의존 구성

  • TeacherService findAll(readOnly)
    • LanguageService - findAllLanguages(readOnly)
    • SkillService - findAllSkills(readOnly)
    • TeacherRepository - findAllTeacher

눈으로 확인

Slave를 사용한 부모 트랜잭션 생성 및 자식 트랜잭션이 부모 트랜잭션에 포함됨

스크린샷 2021-08-10 오전 8 29 29

부모 트랜잭션 실행

스크린샷 2021-08-10 오전 8 38 50

트랜잭션 커밋 및 종료

스크린샷 2021-08-09 오후 9 22 26

POST /teachers

Transaction 의존 구성

  • MemberService findMemberByToken(readOnly) -> Transaction 1

    • MemberService - findById(readOnly)
  • TeacherService registerTeacher -> Transaction 2

    • MemberService - findById(readOnly)
    • LanguageService - findAllLanguages(readOnly)
    • SkillService - findAllSkills(readOnly)
    • TeacherService - save
    • TeacherLanguageService - saveAll
    • TeacherSkillService - saveAll
    • MemberService - save

눈으로 확인

Slave를 사용한 부모 트랜잭션 생성 및 자식 트랜잭션이 부모 트랜잭션에 포함됨

스크린샷 2021-08-10 오전 9 02 57

트랜잭션 커밋 및 종료

스크린샷 2021-08-10 오전 9 14 43

Master를 사용한 부모 트랜잭션 생성 및 자식 트랜잭션이 부모 트랜잭션에 포함됨

스크린샷 2021-08-10 오전 9 15 40

백엔드 학습 공유

Application

Infra

Web Socket

Clone this wiki locally