1. 소개

다중 테넌트 는 여러 클라이언트 또는 테넌트가 단일 리소스를 사용하거나 이 문서의 맥락에서 단일 데이터베이스 인스턴스를 사용하도록 허용합니다. 그 목적은 공유 데이터베이스에서 각 테넌트가 필요로 하는 정보를 분리하는 것 입니다.

이 사용방법(예제)에서는 Hibernate 5에서 다중 테넌시를 구성하는 다양한 접근 방식을 소개합니다.

2. 메이븐 의존성

pom.xml 파일 hibernate-core 의존성포함해야 합니다.

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
   <version>5.2.12.Final</version>
</dependency>

테스트를 위해 H2 메모리 내 데이터베이스를 사용하므로 이 의존성pom.xml 파일 에도 추가해 보겠습니다 .

<dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <version>1.4.196</version>
</dependency>

3. Hibernate의 멀티테넌시의 이해

공식 Hibernate User Guide 에서 언급했듯이 Hibernate 에는 다중 테넌시에 대한 세 가지 접근 방식이 있습니다.

  • 별도의 스키마 – 동일한 물리적 데이터베이스 인스턴스에서 테넌트당 하나의 스키마
  • 별도의 데이터베이스 – 테넌트당 하나의 별도 물리적 데이터베이스 인스턴스
  • 분할된(식별자) 데이터 – 각 테넌트의 데이터는 식별자 값으로 분할됩니다.

Partitioned (Discriminator) Data 접근 방식 은 아직 Hibernate에서 지원되지 않습니다. 향후 진행을 위해 이 JIRA 문제 에 대한 후속 조치를 취하십시오.

평소와 같이 Hibernate는 각 접근 방식의 구현에 대한 복잡성을 추상화합니다.

필요한 것은 다음 두 인터페이스의 구현을 제공하는 것입니다 .

데이터베이스 및 스키마 접근 예제를 살펴보기 전에 각 개념을 자세히 살펴보겠습니다.

3.1. MultiTenantConnectionProvider

기본적으로 이 인터페이스는 구체적인 테넌트 식별자에 대한 데이터베이스 연결을 제공합니다.

두 가지 주요 방법을 살펴보겠습니다.

interface MultiTenantConnectionProvider extends Service, Wrapped {
    Connection getAnyConnection() throws SQLException;

    Connection getConnection(String tenantIdentifier) throws SQLException;
     // ...
}

Hibernate가 사용할 테넌트 식별자를 확인할 수 없으면 getAnyConnection 메서드를 사용 하여 연결을 얻습니다. 그렇지 않으면 getConnection 메서드를 사용합니다 .

Hibernate는 데이터베이스 연결을 정의하는 방법에 따라 이 인터페이스의 두 가지 구현을 제공합니다.

  • Java의 DataSource 인터페이스 사용 – DataSourceBasedMultiTenantConnectionProviderImpl 구현 을 사용합니다.
  • Hibernate 에서 ConnectionProvider 인터페이스 사용 – AbstractMultiTenantConnectionProvider 구현 을 사용합니다.

3.2. CurrentTenantIdentifier 확인자

테넌트 식별자를 확인하는 방법 에는 여러 가지가 있습니다 . 예를 들어 우리의 구현은 구성 파일에 정의된 하나의 테넌트 식별자를 사용할 수 있습니다.

또 다른 방법은 경로 매개변수에서 테넌트 식별자를 사용하는 것입니다.

이 인터페이스를 보자:

public interface CurrentTenantIdentifierResolver {

    String resolveCurrentTenantIdentifier();

    boolean validateExistingCurrentSessions();
}

Hibernate는 resolveCurrentTenantIdentifier 메서드를 호출 하여 테넌트 식별자를 얻습니다. Hibernate가 모든 기존 세션이 동일한 테넌트 식별자에 속하는지 확인하기를 원하면, validateExistingCurrentSessions 메서드 는 true를 반환해야 합니다.

4. 스키마 접근

이 전략에서는 동일한 물리적 데이터베이스 인스턴스에서 다른 스키마 또는 사용자를 사용합니다. 이 접근 방식은 애플리케이션에 최상의 성능이 필요하고 테넌트별 백업과 같은 특수 데이터베이스 기능을 희생할 수 있는 경우에 사용해야 합니다.

또한 CurrentTenantIdentifierResolver 인터페이스를 조롱하여 테스트 중에 하나의 테넌트 식별자를 선택 항목으로 제공합니다.

public abstract class MultitenancyIntegrationTest {

    @Mock
    private CurrentTenantIdentifierResolver currentTenantIdentifierResolver;

    private SessionFactory sessionFactory;

    @Before
    public void setup() throws IOException {
        MockitoAnnotations.initMocks(this);

        when(currentTenantIdentifierResolver.validateExistingCurrentSessions())
          .thenReturn(false);

        Properties properties = getHibernateProperties();
        properties.put(
          AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, 
          currentTenantIdentifierResolver);

        sessionFactory = buildSessionFactory(properties);

        initTenant(TenantIdNames.MYDB1);
        initTenant(TenantIdNames.MYDB2);
    }

    protected void initTenant(String tenantId) {
        when(currentTenantIdentifierResolver
         .resolveCurrentTenantIdentifier())
           .thenReturn(tenantId);
        createCarTable();
    }
}

MultiTenantConnectionProvider 인터페이스 의 구현은 연결이 요청될 때마다 사용할 스키마를 설정합니다 .

class SchemaMultiTenantConnectionProvider
  extends AbstractMultiTenantConnectionProvider {

    private ConnectionProvider connectionProvider;

    public SchemaMultiTenantConnectionProvider() throws IOException {
        this.connectionProvider = initConnectionProvider();
    }

    @Override
    protected ConnectionProvider getAnyConnectionProvider() {
        return connectionProvider;
    }

    @Override
    protected ConnectionProvider selectConnectionProvider(
      String tenantIdentifier) {
 
        return connectionProvider;
    }

    @Override
    public Connection getConnection(String tenantIdentifier)
      throws SQLException {
 
        Connection connection = super.getConnection(tenantIdentifier);
        connection.createStatement()
          .execute(String.format("SET SCHEMA %s;", tenantIdentifier));
        return connection;
    }

    private ConnectionProvider initConnectionProvider() throws IOException {
        Properties properties = new Properties();
        properties.load(getClass()
          .getResourceAsStream("/hibernate.properties"));

        DriverManagerConnectionProviderImpl connectionProvider 
          = new DriverManagerConnectionProviderImpl();
        connectionProvider.configure(properties);
        return connectionProvider;
    }
}

따라서 각 테넌트당 하나씩 두 개의 스키마가 있는 메모리 내 H2 데이터베이스 하나를 사용합니다.

스키마 다중 테넌시 모드와 MultiTenantConnectionProvider 인터페이스 구현을 사용하도록 hibernate.properties 를 구성해 보겠습니다 .

hibernate.connection.url=jdbc:h2:mem:mydb1;DB_CLOSE_DELAY=-1;\
  INIT=CREATE SCHEMA IF NOT EXISTS MYDB1\\;CREATE SCHEMA IF NOT EXISTS MYDB2\\;
hibernate.multiTenancy=SCHEMA
hibernate.multi_tenant_connection_provider=\
  com.baeldung.hibernate.multitenancy.schema.SchemaMultiTenantConnectionProvider

테스트 를 위해 두 개의 스키마를 생성 하도록 hibernate.connection.url 속성을 구성했습니다. 스키마가 이미 제자리에 있어야 하므로 실제 애플리케이션에는 필요하지 않습니다.

테스트를 위해 테넌트 myDb1 에 하나의 Car 항목을 추가합니다. 이 항목이 데이터베이스에 저장되었고 myDb2 테넌트에 없는지 확인합니다 .

@Test
void whenAddingEntries_thenOnlyAddedToConcreteDatabase() {
    whenCurrentTenantIs(TenantIdNames.MYDB1);
    whenAddCar("myCar");
    thenCarFound("myCar");
    whenCurrentTenantIs(TenantIdNames.MYDB2);
    thenCarNotFound("myCar");
}

테스트에서 볼 수 있듯이 whenCurrentTenantIs 메서드 를 호출할 때 테넌트를 변경합니다 .

5. 데이터베이스 접근

데이터베이스 다중 테넌시 접근 방식은 테넌트별로 서로 다른 물리적 데이터베이스 인스턴스를 사용합니다 . 각 테넌트는 완전히 격리되어 있으므로 최상의 성능보다 테넌트당 백업과 같은 특별한 데이터베이스 기능이 필요할 때 이 전략을 선택해야 합니다.

데이터베이스 접근 방식의 경우 위와 동일한 MultitenancyIntegrationTest 클래스와 CurrentTenantIdentifierResolver 인터페이스를 사용합니다.

MultiTenantConnectionProvider 인터페이스의 경우 Map 컬렉션을 사용하여 테넌트 식별자 별로 ConnectionProvider를 가져 옵니다 .

class MapMultiTenantConnectionProvider
  extends AbstractMultiTenantConnectionProvider {

    private Map<String, ConnectionProvider> connectionProviderMap
     = new HashMap<>();

    public MapMultiTenantConnectionProvider() throws IOException {
        initConnectionProviderForTenant(TenantIdNames.MYDB1);
        initConnectionProviderForTenant(TenantIdNames.MYDB2);
    }

    @Override
    protected ConnectionProvider getAnyConnectionProvider() {
        return connectionProviderMap.values()
          .iterator()
          .next();
    }

    @Override
    protected ConnectionProvider selectConnectionProvider(
      String tenantIdentifier) {
 
        return connectionProviderMap.get(tenantIdentifier);
    }

    private void initConnectionProviderForTenant(String tenantId)
     throws IOException {
        Properties properties = new Properties();
        properties.load(getClass().getResourceAsStream(
          String.format("/hibernate-database-%s.properties", tenantId)));
        DriverManagerConnectionProviderImpl connectionProvider 
          = new DriverManagerConnectionProviderImpl();
        connectionProvider.configure(properties);
        this.connectionProviderMap.put(tenantId, connectionProvider);
    }
}

ConnectionProvider 는 모든 연결 세부 정보가 있는 구성 파일 hibernate-database-<tenant identifier>.properties 를 통해 채워집니다 .

hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:<Tenant Identifier>;DB_CLOSE_DELAY=-1
hibernate.connection.username=sa
hibernate.dialect=org.hibernate.dialect.H2Dialect

마지막으로 데이터베이스 다중 테넌시 모드와 MultiTenantConnectionProvider 인터페이스 구현을 사용하도록 hibernate.properties 를 다시 업데이트해 보겠습니다.

hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
  com.baeldung.hibernate.multitenancy.database.MapMultiTenantConnectionProvider

스키마 방식에서와 똑같은 테스트를 실행하면 테스트가 다시 통과됩니다.

6. 결론

이 기사에서는 별도의 데이터베이스 및 별도의 스키마 접근 방식을 사용하여 다중 테넌시에 대한 Hibernate 5 지원을 다룹니다. 우리는 이 두 가지 전략 간의 차이점을 조사하기 위해 매우 단순한 구현과 예제를 제공합니다.

이 문서에 사용된 전체 코드 샘플은 GitHub 프로젝트 에서 사용할 수 있습니다 .

Persistence footer banner