[Spring] Master-Slave 구축 (Spring과 JPA 설정 )

오형상·2023년 9월 2일
0

Spring

목록 보기
7/9
post-thumbnail

MySQL 데이터 분산 처리를 위한 Master-Slave 이중화 구축

저번에 작성한 MySQL 데이터 분산 처리를 위한 Master-Slave 이중화 구축에 이어서 Spring Boot와 JPA 설정을 하여 최종적으로 작동하도록 하겠습니다.

Spring Boot 설정

1. application.yml 작성

먼저, application.yml 파일에 데이터베이스 연결 정보를 설정합니다. Master와 Slave 데이터베이스에 대한 정보를 포함하고 있습니다.

spring:
  datasource:
    master:
      hikari:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ec2-52-79-202-191.ap-northeast-2.compute.amazonaws.com:3306/shoppingmall
        username: user
        password: 1234
    slave:
      hikari:
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://ec2-52-79-202-191.ap-northeast-2.compute.amazonaws.com:3307/shoppingmall
        username: user
        password: 1234

2. DataSourceConfiguration 클래스 작성

이제 Spring Boot 애플리케이션에서 데이터베이스 연결을 구성할 클래스를 작성합니다.

@Configuration
public class DataSourceConfiguration {

    public static final String MASTER_DATASOURCE = "masterDataSource";
    public static final String SLAVE_DATASOURCE = "slaveDataSource";

    @Value("${spring.datasource.master.hikari.driver-class-name}")
    private String masterDriverClassName;

    @Value("${spring.datasource.master.hikari.jdbc-url}")
    private String masterJdbcUrl;

    @Value("${spring.datasource.master.hikari.username}")
    private String masterUsername;

    @Value("${spring.datasource.master.hikari.password}")
    private String masterPassword;

    @Value("${spring.datasource.slave.hikari.driver-class-name}")
    private String slaveDriverClassName;

    @Value("${spring.datasource.slave.hikari.jdbc-url}")
    private String slaveJdbcUrl;

    @Value("${spring.datasource.slave.hikari.username}")
    private String slaveUsername;

    @Value("${spring.datasource.slave.hikari.password}")
    private String slavePassword;

    // Master 데이터베이스의 DataSource를 생성하는 빈 설정
    @Bean(MASTER_DATASOURCE)
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .driverClassName(masterDriverClassName)
                .url(masterJdbcUrl)
                .username(masterUsername)
                .password(masterPassword)
                .build();
    }

    // Slave 데이터베이스의 DataSource를 생성하는 빈 설정
    @Bean(SLAVE_DATASOURCE)
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .driverClassName(slaveDriverClassName)
                .url(slaveJdbcUrl)
                .username(slaveUsername)
                .password(slavePassword)
                .build();
    }

    // 라우팅 데이터베이스를 생성하는 빈 설정
    @Bean
    public DataSource routingDataSource(@Qualifier(MASTER_DATASOURCE) DataSource masterDataSource,
                                        @Qualifier(SLAVE_DATASOURCE) DataSource slaveDataSource) {

        RoutingDataSource routingDataSource = new RoutingDataSource();

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave", slaveDataSource);

        Map<Object, Object> immutableDataSourceMap = Collections.unmodifiableMap(dataSourceMap);

        routingDataSource.setTargetDataSources(immutableDataSourceMap);
        routingDataSource.setDefaultTargetDataSource(masterDataSource);

        return routingDataSource;
    }

    // 라우팅 데이터베이스를 기본 DataSource로 설정하는 빈 설정
    @Primary
    @Bean
    public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

}

이 코드는 Master와 Slave 데이터베이스에 대한 데이터 소스를 설정하고, 라우팅 데이터베이스를 생성하여 데이터베이스 연결을 동적으로 선택할 수 있게 합니다.

JPA 설정

Spring Boot 애플리케이션에서 JPA 설정을 구현합니다.

@Configuration
@EnableJpaRepositories(basePackages = {
        "store.myproject.onlineshop.domain.customer.repository",
        "store.myproject.onlineshop.domain.brand.repository",
        "store.myproject.onlineshop.domain.cart.repository",
        "store.myproject.onlineshop.domain.cartitem.repository",
        "store.myproject.onlineshop.domain.delivery.repository",
        "store.myproject.onlineshop.domain.item.repository",
        "store.myproject.onlineshop.domain.membership.repository",
        "store.myproject.onlineshop.domain.order.repository",
        "store.myproject.onlineshop.domain.orderitem.repository",
        "store.myproject.onlineshop.domain.like.repository",
        "store.myproject.onlineshop.domain.recipe.repository",
        "store.myproject.onlineshop.domain.recipeitem.repository",
        "store.myproject.onlineshop.domain.review.repository",
})
@EnableTransactionManagement // 트랜잭션 관리 기능을 활성화하는 애너테이션
public class JpaConfiguration {


    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            // 이름이 dataSource인 Bean을 주입 받는다.
            @Qualifier("dataSource") DataSource dataSource) {

        LocalContainerEntityManagerFactoryBean entityManagerFactory
                = new LocalContainerEntityManagerFactoryBean();

        // DataSource를 주입받은 dataSource로 설정한다.
        entityManagerFactory.setDataSource(dataSource);
        // JPA 엔티티 클래스가 포함된 패키지를 설정한다.
        entityManagerFactory.setPackagesToScan("store.myproject.onlineshop");
        // JPA 벤더 어뎁터를 설정한다.
        entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter());
        // 영속성 유닛의 이름을 entityManager로 설정한다.
        entityManagerFactory.setPersistenceUnitName("entityManager");
        // Hibernate 속성을 설정하기 위해 별도의 Properties 객체를 사용합니다.
        entityManagerFactory.setJpaProperties(hibernateProperties());

        return entityManagerFactory;

    }

    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        // DDL 생성 기능을 활성화
//        hibernateJpaVendorAdapter.setGenerateDdl(false);
        // SQL 쿼리를 로깅하지 않도록 설정
        hibernateJpaVendorAdapter.setShowSql(false);

        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);

        hibernateJpaVendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQLDialect");
        // SQL 방언을 MySQL8Dialect 방언으로 설정
        return hibernateJpaVendorAdapter;
    }

    private Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.hbm2ddl.auto", "create");
        properties.setProperty("hibernate.format_sql", "true");
        return properties;
    }

    @Bean
    public PlatformTransactionManager transactionManager(
            // 이름이 entityManager인 Bean을 주입받는다.
            @Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
        JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
        // 주입받은 entityManagerFactory의 객체를 설정한다 -> 트랜잭션 매니저가 올바른 엔티티 매니저 팩토리를 사용하여 트랜잭션을 관리할 수 있다.
        jpaTransactionManager.setEntityManagerFactory(entityManagerFactory.getObject());
        return jpaTransactionManager;
    }
}

이 부분에서는 JPA와 관련된 설정을 구현하고 있습니다. entityManagerFactory를 설정하여 엔티티 관리자 팩토리를 생성하고, 트랜잭션 관리를 위한 transactionManager 빈을 설정합니다.

RoutingDataSource 작성

마지막으로, 데이터베이스 연결을 동적으로 선택하기 위한 RoutingDataSource 클래스를 구현합니다.

AbstractRoutingDataSource란?

AbstractRoutingDataSource는 스프링 프레임워크에서 제공하는 추상 클래스로, 다수의 데이터 소스 중에서 어떤 데이터 소스를 선택할지를 동적으로 결정할 수 있도록 도와줍니다. 주로 데이터베이스 이중화 환경에서 읽기와 쓰기 작업을 분산하기 위해 활용됩니다.

@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    // 현재 데이터베이스 연결을 결정하기 위해 호출하는 메서드
    protected Object determineCurrentLookupKey() {
        // 현재 트랜잭션이 읽기 전용인지 여부를 판단합니다.
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        
        // 현재 트랜잭션이 읽기 전용인 경우는 "slave", 아닐 경우 "master"를 반환하여 데이터베이스 연결을 결정
        return isReadOnly ? "slave" : "master";
    }
}

이 클래스는 스프링 프레임워크에서 제공하는 AbstractRoutingDataSource를 상속하여 현재 트랜잭션의 속성에 따라 Master 또는 Slave 데이터베이스로 연결을 라우팅합니다.


이제 Spring Boot 애플리케이션에서 Master-Slave 데이터베이스 아키텍처를 구현하는 데 필요한 설정을 완료했습니다. 데이터의 쓰기와 읽기 작업을 분리하여 데이터베이스 성능을 최적화할 수 있게 되었습니다.

이 설정을 통해 애플리케이션은 읽기 작업을 슬레이브 데이터베이스로 보내고 쓰기 작업을 마스터 데이터베이스로 보내는 등의 데이터베이스 분산 처리를 구현할 수 있습니다.

0개의 댓글