Architecture

Sharding Storage + Spring + JPA (분산저장). 1부

백엔드 유성 2023. 3. 25. 22:02

소스코드 (해당 블로그 내용은 2번째 커밋에 있습니다: 5ae762acce83f126a8e2c153eeceeeddf9e4275a)

github: https://github.com/youseonghyeon/multi-datasource-jpa

 

GitHub - youseonghyeon/multi-datasource-jpa: 샤딩 스토리지 구성을 위한 JPA 설정

샤딩 스토리지 구성을 위한 JPA 설정. Contribute to youseonghyeon/multi-datasource-jpa development by creating an account on GitHub.

github.com

 

우선 분산 저장의 장점은 DB 과부화를 방지할 수 있다는 장점이 있습니다.

트래픽이 많고 DB connection이 많거나, 대용량의 데이터를 DB에 저장 및 호출하는 경우 분산 저장을 고려해볼 필요가 있죠.

 

작업 순서를 먼저 보자면

1. MySQL 2개 실행

2. JPA는 datasource가 2개 이상이면 자동 구성을 못해주므로, 수동 빈 등록

3. 데이터를 insert, select할 때 알맞는 DB를 선택하는 알고리즘을 제작

4. DB 리소스를 제한하여 트래픽 테스트를 진행

 

저는 docker Mysql 컨테이너 2개를 준비하였습니다.

 

DB 2대가 준비 되었으면

이제 영속성 컨텍스트를 관리하는 EntityManager와 DB를 연결하는 Datasource를 등록해봅시다.

영속성 컨텍스트에 관하여 궁금하신분은 참고해주세요. ->

 

@@ 빈 설정하기 전 주의할 것이 있습니다. 처음에 여기서 많이 헷갈렸는데요.

첫번째 DB를 master라 하고 두번째 DB를 slave라고 했을 때

masterRepository와 slaveRepository를 같은 패키지안에 생성하면 절대 안됩니다!

 

우선 데이터베이스와 jpa(hibernate)에서 설정으로 사용할 property들을 application.yml에 등록해줍니다.

spring.datasource:
  master:
    jdbcUrl: jdbc:mysql://localhost:3307/sharding # master DB url
    username: myName
    password: myPw
  slave:
    jdbcUrl: jdbc:mysql://localhost:3308/sharding # slave DB url
    username: myName
    password: myPw

hibernate:
  dialect: org.hibernate.dialect.MySQLDialect
  show_sql: true
  hbm2ddl:
    auto: create
  • 데이터베이스는 총 2대로 master와 slave로 구분지었습니다.
  • dialect는 MySQL로 지정해주었고, sql은 콘솔로 출력되며, ddl은 create로 설정하였습니다.
    (단! 해당 내용은 Config에서 수동 등록해야합니다. 아래 설명이 있습니다.)

property를 설정해주었으면 property를 사용할 config 를 만들어줍니다.

우선 코드부터 보시죠

 

Domain(Entity)

package study.multidatasourcejpa.domain;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

 

Master JPA Config

package study.multidatasourcejpa.config;

@Slf4j
@Configuration
@PropertySource("classpath:application.yml")
@RequiredArgsConstructor
@EnableJpaRepositories(
        // master repository가 존재하는 패키지
        // ##주의## slave repository와 패키지가 동일하면 안됨
        basePackages = "study.multidatasourcejpa.repository.master",
        // EntityManager 빈 이름
        entityManagerFactoryRef = "masterEntityManager",
        // transactionManager 빈 이름
        transactionManagerRef = "masterTransactionManager"
)
public class MasterDatabaseConfig {
    private final Environment env;

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean masterEntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        // master datasource 설정
        em.setDataSource(masterDataSource());

        // 도메인 경로 설정 (도메인은 Master와 Slave 둘다 같아도 됨)
        String[] domainPath = new String[]{"study.multidatasourcejpa.domain"};
        em.setPackagesToScan(domainPath);
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        // Jpa 환경 설정
        HashMap<String, Object> propertyMap = new HashMap<>();
        propertyMap.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
        propertyMap.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
        propertyMap.put("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
        em.setJpaPropertyMap(propertyMap);

        return em;
    }

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager masterTransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(masterEntityManager().getObject());

        return tm;
    }
}

Slave JPA Config

package study.multidatasourcejpa.config;

@Configuration
@PropertySource("classpath:application.yml")
@RequiredArgsConstructor
@EnableJpaRepositories(
        // slave repository가 존재하는 패키지
        // ##주의## master repository와 패키지가 동일하면 안됨
        basePackages = "study.multidatasourcejpa.repository.slave",
        // EntityManager 빈 이름
        entityManagerFactoryRef = "slaveEntityManager",
        // transactionManager 빈 이름
        transactionManagerRef = "slaveTransactionManager"
)
public class SlaveDatabaseConfig {
    private final Environment env;

    @Bean
    public LocalContainerEntityManagerFactoryBean slaveEntityManager() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        // slave datasource 설정
        em.setDataSource(slaveDatasource());
        // 도메인 경로 설정
        // 도메인은 Master와 Slave 둘다 같아도 됨
        String[] domainPath = new String[]{"study.multidatasourcejpa.domain"};
        em.setPackagesToScan(domainPath);
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        // Jpa 환경 설정
        HashMap<String, Object> propertyMap = new HashMap<>();
        propertyMap.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
        propertyMap.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
        propertyMap.put("hibernate.show_sql", env.getProperty("hibernate.show_sql"));
        em.setJpaPropertyMap(propertyMap);

        return em;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDatasource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public PlatformTransactionManager slaveTransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(slaveEntityManager().getObject());
        return tm;
    }
}

먼저 주의할점을 말씀드리면

  1. masterRepsitory와 slaveRepository의 패키지가 동일하면 안됩니다.
    -> 저는 Master : ~/repository/master/UserMasterRepository.java
                 Slave : ~/repository/slave/UserSlaveRepository.java
         로 분리해두었습니다.
  2. returnType이 동일한 빈 2개를 등록할때에는 둘중 하나에 @Primry를 지정해야 합니다.

 

@EnableJpaRepositories

 ++++ 추가 예정

 

여기서 등록할 것은

1. DataSource : 어떤 DB를 사용할지

2. EntityManager : JPA로 관리할 datasource, domain 및 추가 옵션 설정

3. TransactionManager : 트랜잭션

입니다.

 

DataSource는 application.yml에 미리 작성해두었던 property를 이용해 생성해줍니다.

 

EntityManager에서는 DataSource를 연결해주고, EntityManger에서 관리해줄 Domain(Entity)를 선택합니다.

또한 ddl-auto, dialect, show_sql같은 부가옵션을 추가할 수 있습니다.

 

TransactionManager에서 어떤 EntityManager를 사용할지 선택해주면 JPA 설정 부분은 끝이납니다.

 

@EnableJpaRepositories(basePackages = study.multidatasourcejpa.repository.master)

 

basePackages에 적어놓은 경로대로 Repository를 생성해줍니다.

사진에 있는 UserRepository 클래스와 UserRepositoryHelper 인터페이스는 무시해주세요.

public interface UserMasterRepository extends JpaRepository<User, Long>, UserRepositoryHelper {}
public interface UserSlaveRepository extends JpaRepository<User, Long>, UserRepositoryHelper {}

이제 스프링을 실행하여 로그를 확인하면 

다음과 같이 HikariPool이 MasterDB와 SlaveDB에 각각 connection을 맺는 것을 확인할 수 있습니다.

 

이제 UserMasterRepository와 UserSlaveRepository를 따로 주입받아서 원하는 DB를 사용하시면 됩니다.

 

- 분산 저장의 문제점 - 

1. MasterDB와 SlaveDB에 동일한 PK값이 들어갈 수 있음

2. 개발자가 로직을 작성할 때 DB를 직접 선택해야 함

 

다음편은 UserMasterRepository와 UserSlaveRepository를 추상화하여 하나의 UserRepository로 만들고

사용자가 UserRepository만 호출하면 적절한 알고리즘을 통해 자동으로 분산 저장을 해주는 로직을 만들어보겠습니다.