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는 각 접근 방식의 구현에 대한 복잡성을 추상화합니다.
필요한 것은 다음 두 인터페이스의 구현을 제공하는 것입니다 .
-
MultiTenantConnectionProvider – 테넌트당 연결 제공
-
CurrentTenantIdentifierResolver – 사용할 테넌트 식별자를 확인합니다.
데이터베이스 및 스키마 접근 예제를 살펴보기 전에 각 개념을 자세히 살펴보겠습니다.
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 프로젝트 에서 사용할 수 있습니다 .