Backend/Relational Databases

[Relational Databases] Datasource Routing

lakelight 2022. 10. 25. 19:27
728x90
반응형
사용자에 따라서 데이터베이스를 다르게 지정하고 싶다는 생각을 했습니다.
그래서 사용자에 따른 다른 데이터베이스를 적용하는 법을 알아보았습니다.

 

개요

User를 간단하게 만들고 Repository와 Controller를 만들어서 API를 구축하였습니다.
그리고 회사에 따라 DB를 변경하여 저장하고 조회하는 기능을 구현해보겠습니다.

 

구조

구조는 다음과 같습니다.

 

User API 부분은 기본적인 코드라서 Database Routing 부분만 설명드리겠습니다. 전체코드는 아래에 깃허브 링크를 참고해주세요.

 

1. application.properties 설정

#company01과 company02는 제가 설정한 이름입니다. 임의로 이름을 지정하시면 됩니다.
#저는 두 회사의 하나의 서비스를 공급할 때, 데이터베이스를 회사마다 개별적으로 사용할 수 있도록 하였습니다.
spring.datasource.company01.url=jdbc:mysql://localhost:3306/company01?useSSL=false
spring.datasource.company01.username=root
spring.datasource.company01.password=password

spring.datasource.company02.url=jdbc:mysql://localhost:3306/company02?useSSL=false
spring.datasource.company02.username=root
spring.datasource.company02.password=password

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true

 

2. DatabaseEnum.java

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum DatabaseEnum {
    //제가 사용할 데이터베이스의 이름을 설정하는 Enum 파일입니다.
    COMPANY01("company01"), COMPANY02("company02");

    private final String databaseSource;
}

 

3. DatabaseContextHolder.java

import org.springframework.util.Assert;

public class DatabaseContextHolder {

    private static ThreadLocal<DatabaseEnum> CONTEXT = new ThreadLocal<>();

    public static void setDatabaseContext(DatabaseEnum databaseSource){
        Assert.notNull(databaseSource, "Routing database cannot be null");
        CONTEXT.set(databaseSource);
    }

    public static DatabaseEnum getDatabaseContext(){
        return CONTEXT.get();
    }

    public static void clearDatabaseContext(){
        CONTEXT.remove();
    }
}
AbstractRoutingDataSource에서 사용할 DataSource의 lookup key를 저장하는 용도로 사용됩니다.  현재 컨텍스트에서 사용될 DataSource의 key를 ThreadLocal에 저장하고 ThreadLocal의 저장된 값을 통해 DataSource 값을 결정합니다.

 

4. DatabaseSourceRouting.java

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DatabaseSourceRouting extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getDatabaseContext();
    }
}
현재 Context에서 사용될 DataSource를 가져옵니다.

 

5. DatabaseSourceConfig.java

import com.zaxxer.hikari.HikariDataSource;
import hooyn.routing_datasource.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "hooyn.routing_datasource", // 현재 파일이 있는 패키지
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager"
)
public class DatabaseSourceConfig {

    @Bean
    @Primary
    public DataSource dataSource(){
        // DatabaseSourceRouting 생성
        DatabaseSourceRouting databaseSourceRouting = new DatabaseSourceRouting();
        
        // DatabaseSource를 저장합니다.
        databaseSourceRouting.setTargetDataSources(targetDataSources());
        
        // DatabaseSource가 없을 때 기본 DatabaseSource를 저장합니다.
        databaseSourceRouting.setDefaultTargetDataSource(company01DataSource());
        return databaseSourceRouting;
    }

    private Map<Object, Object> targetDataSources(){
        Map<Object, Object> targetDataSources = new HashMap<>();
        
        // 제가 지정한 회사 01, 02를 Map에 저장시켜줍니다.
        targetDataSources.put(DatabaseEnum.COMPANY01, company01DataSource());
        targetDataSources.put(DatabaseEnum.COMPANY02, company02DataSource());
        return targetDataSources;
    }

    @Bean
    @ConfigurationProperties("spring.datasource.company01")
    public DataSourceProperties company01DatabaseSourceProperties(){
    
        // DatabaseSourceProperties를 설정합니다.
        return new DataSourceProperties();
    }

    @Bean
    public DataSource company01DataSource(){
        // DataSource를 빌드합니다.
        return company01DatabaseSourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.company02")
    public DataSourceProperties company02DataSourceProperties(){
    
        // DatabaseSourceProperties를 설정합니다.
        return new DataSourceProperties();
    }

    @Bean
    public DataSource company02DataSource(){
        // DataSource를 빌드합니다.
        return company02DataSourceProperties()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
            EntityManagerFactoryBuilder builder) {
        return builder.dataSource(dataSource()).packages(User.class) // DataSource를 적용할 클래스
                .build();
    }

    @Bean(name = "transactionManager")
    public JpaTransactionManager transactionManager(
            @Autowired @Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        return new JpaTransactionManager(entityManagerFactoryBean.getObject());
    }
}

 

6. DataSourceInterceptor.java

import hooyn.routing_datasource.routingconfig.DatabaseContextHolder;
import hooyn.routing_datasource.routingconfig.DatabaseEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class DataSourceInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // Header의 'company'를 읽습니다.
        String company = request.getHeader("company");
        
        // company01이라면 DatabaseContext를 company01에 해당하는 source로 설정
        if (DatabaseEnum.COMPANY01.toString().equalsIgnoreCase(company)) {
            DatabaseContextHolder.setDatabaseContext(DatabaseEnum.COMPANY01);
        } else {
        // company02라면 DatabaseContext를 company02에 해당하는 source로 설정
            DatabaseContextHolder.setDatabaseContext(DatabaseEnum.COMPANY02);
        }
        return true;
    }
}
이 부분은 추후에 JWT token을 이용할 수 있도록 변경될 예정입니다.

 

7. WebConfig.java

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final DataSourceInterceptor dataSourceInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        // Interceptor를 추가해서 모든 url에서 header를 검증하여 DataSource를 결정합니다.
        registry.addInterceptor(dataSourceInterceptor).addPathPatterns("/**");
    }
}

 

마무리

늘 생각만 했던 사용자별로 데이터베이스를 설정하는 작업을 진행해보았습니다.
많은 회사들에게 서비스를 제공할 때 이 방법을 사용하면 좋을 것입니다.

포스팅 읽어주셔서 감사합니다.

 

 

전체 코드

 

GitHub - hooyn/DatasourceRouting

Contribute to hooyn/DatasourceRouting development by creating an account on GitHub.

github.com

 

728x90
반응형