스프링 부트 애플리케이션에서 멀티 테넌시 구현

스프링 부트 애플리케이션과 멀티 테넌시

스프링 부트는 단순한 구성과 높은 생산성으로 인해 개발자들 사이에서 인기가 많은 프레임워크입니다. 이러한 이유로 스프링 부트 애플리케이션의 사용자 수가 늘어나면서, 멀티 테넌시가 필요한 경우가 생기고 있습니다. 멀티 테넌시란 여러 사용자 또는 그룹이 동시에 액세스하는 애플리케이션에서, 각각의 사용자 또는 그룹에게 별도의 데이터베이스를 할당하여 데이터를 분리하는 방법입니다.

멀티 테넌시 구현에 대한 개요

멀티 테넌시를 구현하는 방법에는 다음과 같은 방법들이 있습니다.

  • 다중 데이터베이스를 사용한 멀티 테넌시
  • 스키마를 사용한 멀티 테넌시
  • 테이블을 사용한 멀티 테넌시
  • 컬럼을 사용한 멀티 테넌시

이 중에서도 다중 데이터베이스를 사용한 멀티 테넌시가 가장 일반적인 방법입니다. 다중 데이터베이스를 사용하지 않고도 스키마, 테이블, 컬럼을 사용하여 멀티 테넌시를 구현할 수 있지만, 이러한 방법들은 구현하기 어려울 뿐만 아니라, 유지보수도 어렵습니다. 따라서 다중 데이터베이스를 사용한 멀티 테넌시가 가장 효율적이며, 쉽게 구현할 수 있는 방법입니다.

다중 데이터베이스를 사용한 멀티 테넌시 구현

다중 데이터베이스를 사용하여 멀티 테넌시를 구현하는 방법은 간단합니다. 각각의 테넌트마다 별도의 데이터베이스를 생성하고, 애플리케이션에서는 각각의 데이터베이스에 접근하는 방법으로 구현합니다. 이렇게 구현하면, 각각의 테넌트마다 데이터베이스가 분리되어 있기 때문에, 데이터의 보안성이 높아지고, 데이터의 충돌이 발생하지 않습니다.

스프링 부트 애플리케이션의 멀티 테넌시 구현 방법

스프링 부트 애플리케이션에서 멀티 테넌시를 구현하는 방법은 다음과 같습니다.

  1. 데이터베이스 설정

먼저, 각각의 테넌트마다 별도의 데이터베이스를 생성해야 합니다. 이를 위해서는 데이터베이스 설정에서 각각의 테넌트마다 별도의 데이터베이스를 생성해야 합니다. 예를 들어, MySQL을 사용하는 경우, 다음과 같이 각각의 테넌트마다 별도의 데이터베이스를 생성할 수 있습니다.

CREATE DATABASE tenant1;
CREATE DATABASE tenant2;
  1. 프로퍼티 설정

스프링 부트에서는 application.properties 파일을 사용하여 프로퍼티를 설정할 수 있습니다. 이 파일에서는 데이터베이스 연결 정보를 설정할 수 있습니다. 다중 데이터베이스를 사용하여 멀티 테넌시를 구현하는 경우, 각각의 테넌트마다 별도의 데이터베이스 연결 정보를 설정해야 합니다. 예를 들어, MySQL을 사용하는 경우, 다음과 같이 application.properties 파일에서 데이터베이스 연결 정보를 설정할 수 있습니다.

# tenant1 데이터베이스 연결 정보
spring.datasource.url=jdbc:mysql://localhost:3306/tenant1
spring.datasource.username=root
spring.datasource.password=root

# tenant2 데이터베이스 연결 정보
tenant2.datasource.url=jdbc:mysql://localhost:3306/tenant2
tenant2.datasource.username=root
tenant2.datasource.password=root
  1. 데이터베이스 연결 설정

스프링 부트에서는 다양한 데이터베이스 연결 설정 방법을 지원합니다. 다중 데이터베이스를 사용하여 멀티 테넌시를 구현하는 경우, 각각의 테넌트마다 별도의 데이터베이스 연결 설정을 지정해야 합니다. 이를 위해 스프링에서 제공하는 AbstractRoutingDataSource 클래스를 사용하여 데이터베이스 연결 설정을 지정할 수 있습니다.

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
        return dataSourceProperties().initializeDataSourceBuilder().build();
    }

    @Bean
    @ConfigurationProperties("tenant2.datasource")
    public DataSourceProperties tenant2DataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @ConfigurationProperties("tenant2.datasource")
    public DataSource tenant2DataSource() {
        return tenant2DataSourceProperties().initializeDataSourceBuilder().build();
    }

    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        Map targetDataSources = new HashMap();
        targetDataSources.put("tenant1", dataSource());
        targetDataSources.put("tenant2", tenant2DataSource());

        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(dataSource());
        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }
}
  1. 데이터베이스 라우팅 설정

AbstractRoutingDataSource 클래스를 사용하여 데이터베이스 연결 설정을 지정한 후, 각각의 테넌트에 대한 데이터베이스 라우팅 설정을 지정해야 합니다. 이를 위해 스프링에서 제공하는 AbstractRoutingDataSource 클래스를 상속받아 각각의 테넌트에 대한 데이터베이스 연결 설정을 지정할 수 있습니다.

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getCurrentTenant();
    }
}
  1. 테넌트 컨텍스트 설정

마지막으로, 각각의 테넌트에 대한 컨텍스트를 설정해야 합니다. 이를 위해 스프링에서 제공하는 ThreadLocal을 사용하여 각각의 요청마다 적절한 테넌트 컨텍스트를 설정할 수 있습니다.

public class TenantContext {

    private static final ThreadLocal currentTenant = new ThreadLocal();

    public static void setCurrentTenant(String tenant) {
        currentTenant.set(tenant);
    }

    public static String getCurrentTenant() {
        return currentTenant.get();
    }

    public static void clear() {
        currentTenant.remove();
    }
}

이제 각각의 요청마다 적절한 테넌트 컨텍스트를 설정할 수 있습니다.

@RestController
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/users")
    public List getUsers() {
        TenantContext.setCurrentTenant("tenant1");

        List users = userRepository.findAll();

        TenantContext.clear();

        return users;
    }
}

결론

스프링 부트 애플리케이션에서 멀티 테넌시를 구현하는 방법에 대해 알아보았습니다. 다중 데이터베이스를 사용하여 멀티 테넌시를 구현하는 방법이 가장 효율적이고, 쉽게 구현할 수 있는 방법입니다. 이를 위해 데이터베이스 설정, 프로퍼티 설정, 데이터베이스 연결 설정, 데이터베이스 라우팅 설정, 테넌트 컨텍스트 설정 등 다양한 설정이 필요합니다. 하지만 이러한 설정을 통해 각각의 테넌트마다 별도의 데이터베이스를 할당하고, 데이터를 분리하여 보안성을 높이고, 데이터의 충돌을 방지할 수 있습니다.

Implementing Multi-tenancy in Spring Boot Applications

Implementing Multi-tenancy in Spring Boot Applications

Multi-tenancy in Spring Boot

Multi-tenancy is a software architecture that enables multiple clients or tenants to use the same application or system while maintaining data privacy and security. It is a prevalent architecture in the cloud environment, where multiple users need to share the same resources. In this article, we will discuss how to implement multi-tenancy in Spring Boot applications.

멀티 테넌시 아키텍처 디자인과 선택

Before diving into the implementation details, let’s discuss the different types of multi-tenancy architecture and their use cases.

Shared Database

In this architecture, multiple tenants share the same database, and each tenant has its schema or table within the database. It is a simple and cost-effective architecture, but it has some limitations, such as limited scalability and security concerns.

Separate Database

In this architecture, each tenant has its database instance, which provides better scalability, performance, and security. However, it is more complex and expensive than the shared database architecture.

Separate Instance

In this architecture, each tenant has its application instance, which provides the highest level of isolation, security, and scalability. However, it is the most expensive and complex architecture.

Based on your use case and requirements, you can choose the appropriate multi-tenancy architecture.

스프링 부트 애플리케이션에서 멀티 테넌시 구현하기

Now, let’s dive into the implementation part. We will use the shared database architecture for our Spring Boot application.

Database Design

We need to design our database schema in such a way that each tenant has its data. We can achieve this by adding a tenant ID column in each table and filtering the data based on the tenant ID. For example, consider the following table structure:

CREATE TABLE customer (
  id BIGINT AUTO_INCREMENT,
  name VARCHAR(255),
  email VARCHAR(255),
  tenant_id VARCHAR(50) NOT NULL,
  PRIMARY KEY (id)
);

In this example, we have added a tenant ID column in the customer table. We can filter the data for a specific tenant by adding a WHERE clause in the SQL query, like this:

SELECT * FROM customer WHERE tenant_id = 'TENANT_A';

Tenant Identification

We need to identify the tenant for each request and use the appropriate database schema or table. We can achieve this by using a tenant ID in the request header or URL. For example, consider the following URL:

http://localhost:8080/api/v1/customers?tenant=TENANT_A

In this example, we have added a tenant parameter in the URL. We can extract the tenant ID from the request and use it to filter the data.

Spring Boot Configuration

We need to configure our Spring Boot application to support multi-tenancy. We can achieve this by using the AbstractRoutingDataSource class, which provides a way to switch the data source dynamically based on the current thread’s context.

@Configuration
public class MultiTenantConfig {

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

    @Bean
    public MultiTenantDataSource multiTenantDataSource() {
        Map targetDataSources = new HashMap();
        // Add data sources for each tenant
        targetDataSources.put("TENANT_A", createDataSource("TENANT_A"));
        targetDataSources.put("TENANT_B", createDataSource("TENANT_B"));

        MultiTenantDataSource dataSource = new MultiTenantDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(createDataSource("TENANT_A"));
        return dataSource;
    }

    private DataSource createDataSource(String tenantId) {
        // Create a new data source for the given tenant
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql://localhost:5432/" + tenantId);
        dataSource.setUsername("postgres");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(multiTenantDataSource());
    }

    @Bean
    public MultiTenantInterceptor multiTenantInterceptor() {
        return new MultiTenantInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(multiTenantInterceptor());
    }
}

In this example, we have created a MultiTenantConfig class that configures the data source and interceptor for multi-tenancy. We have used the DriverManagerDataSource class to create a new data source for each tenant based on the tenant ID.

Interceptor

We need to intercept each request and set the current tenant ID in the context. We can achieve this by using a HandlerInterceptor, which provides pre- and post-processing for each request.

public class MultiTenantInterceptor extends HandlerInterceptorAdapter {

    private static final String TENANT_HEADER = "X-Tenant";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenantId = request.getHeader(TENANT_HEADER);
        if (tenantId == null) {
            response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing X-Tenant header");
            return false;
        }
        TenantContext.setCurrentTenant(tenantId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        TenantContext.clear();
    }
}

In this example, we have created a MultiTenantInterceptor class that intercepts each request and sets the current tenant ID in the TenantContext class. We have used the HttpServletRequest class to extract the tenant ID from the X-Tenant header.

Tenant Context

We need to store the current tenant ID in a thread-local variable to make it available to all parts of the application. We can achieve this by using the ThreadLocal class, which provides a way to store thread-local variables.

public class TenantContext {

    private static final ThreadLocal currentTenant = new ThreadLocal();

    public static String getCurrentTenant() {
        return currentTenant.get();
    }

    public static void setCurrentTenant(String tenantId) {
        currentTenant.set(tenantId);
    }

    public static void clear() {
        currentTenant.remove();
    }
}

In this example, we have created a TenantContext class that stores the current tenant ID in a thread-local variable. We have used the ThreadLocal class to ensure that the variable is thread-safe.

결론

In this article, we have discussed how to implement multi-tenancy in Spring Boot applications using the shared database architecture. We have discussed the different types of multi-tenancy architecture and their use cases. We have also provided a code example that demonstrates how to configure the data source and interceptor for multi-tenancy. By following these guidelines, you can ensure that your Spring Boot application supports multi-tenancy and provides data privacy and security for each tenant.