다중 테넌트 애플리케이션을 구축하는 방법: 최대 절전 모드 자습서
게시 됨: 2022-03-11각 클라이언트에 별도의 데이터가 있는 클라우드 애플리케이션에 대해 이야기할 때 이 데이터를 저장하고 조작하는 방법에 대해 생각해야 합니다. 훌륭한 NoSQL 솔루션이 모두 나와 있지만 때때로 우리는 여전히 좋은 오래된 관계형 데이터베이스를 사용해야 합니다. 데이터를 분리할 때 생각할 수 있는 첫 번째 솔루션은 모든 테이블에 식별자를 추가하여 개별적으로 처리할 수 있도록 하는 것입니다. 작동하지만 클라이언트가 데이터베이스를 요청하면 어떻게 될까요? 다른 기록들 사이에 숨겨진 모든 기록을 검색하는 것은 매우 성가신 일입니다.
Hibernate 팀은 얼마 전에 이 문제에 대한 해결책을 제시했습니다. 데이터를 검색해야 하는 위치를 제어할 수 있는 몇 가지 확장 지점을 제공합니다. 이 솔루션에는 식별자 열, 여러 데이터베이스 및 여러 스키마를 통해 데이터를 제어하는 옵션이 있습니다. 이 기사에서는 다중 스키마 솔루션을 다룹니다.
자, 작업을 시작하겠습니다!
시작하기
경험이 더 많은 Java 개발자이고 모든 것을 구성하는 방법을 알고 있거나 자체 Java EE 프로젝트가 이미 있는 경우 이 섹션을 건너뛸 수 있습니다.
먼저 새로운 Java 프로젝트를 생성해야 합니다. 저는 Eclipse와 Gradle을 사용하고 있지만 IntelliJ 및 Maven과 같은 빌드 도구와 선호하는 IDE를 사용할 수 있습니다.
저와 같은 도구를 사용하려면 다음 단계에 따라 프로젝트를 생성하세요.
- Eclipse에 Gradle 플러그인 설치
- 파일 -> 새로 만들기 -> 기타...를 클릭합니다.
- Gradle(STS)을 찾고 다음을 클릭합니다.
- 이름을 알려주고 샘플 프로젝트에 대해 Java 빠른 시작을 선택합니다.
- 마침을 클릭
엄청난! 다음은 초기 파일 구조여야 합니다.
javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle
소스 폴더 안에 있는 모든 파일은 샘플 파일이므로 삭제할 수 있습니다.
프로젝트를 실행하기 위해 Wildfly를 사용하고 구성 방법을 보여 드리겠습니다(여기서 선호하는 도구를 다시 사용할 수 있음).
- Wildfly 다운로드: http://wildfly.org/downloads/ (저는 버전 10을 사용하고 있습니다)
- 파일 압축 풀기
- Eclipse에 JBoss 도구 플러그인 설치
- 서버 탭에서 빈 영역을 마우스 오른쪽 버튼으로 클릭하고 새로 만들기 -> 서버를 선택합니다.
- Wildfly 10.x를 선택합니다(Eclipse 버전에 따라 10을 사용할 수 없는 경우 9.x도 작동함).
- 다음을 클릭하고 새 런타임 생성(다음 페이지)을 선택한 후 다시 다음을 클릭합니다.
- Wildfly를 홈 디렉토리로 압축을 푼 폴더를 선택하십시오.
- 마침을 클릭
이제 데이터베이스를 인식하도록 Wildfly를 구성해 보겠습니다.
- Wildfly 폴더 내의 bin 폴더로 이동합니다.
- add-user.bat 또는 add-user.sh 실행(OS에 따라 다름)
- 단계에 따라 사용자를 관리자로 생성하십시오.
- Eclipse에서 다시 서버 탭으로 이동하여 생성한 서버를 마우스 오른쪽 버튼으로 클릭하고 시작을 선택합니다.
- 브라우저에서 관리 인터페이스인 http://localhost:9990에 액세스합니다.
- 방금 생성한 사용자의 자격 증명을 입력합니다.
- 데이터베이스의 드라이버 jar를 배포합니다.
- 배포 탭으로 이동하여 추가를 클릭합니다.
- 다음을 클릭하고 드라이버 jar 파일을 선택하십시오.
- 다음을 클릭하고 마침
- 구성 탭으로 이동
- 하위 시스템 -> 데이터 소스 -> 비XA를 선택합니다.
- 추가를 클릭하고 데이터베이스를 선택하고 다음을 클릭합니다.
- 데이터 소스에 이름을 지정하고 다음을 클릭합니다.
- 드라이버 검색 탭을 선택하고 방금 배포한 드라이버를 선택합니다.
- 데이터베이스 정보를 입력하고 다음을 클릭합니다.
- 이전 단계의 정보가 올바른지 확인하려면 연결 테스트를 클릭하십시오.
- 마침을 클릭
- Eclipse로 돌아가 실행 중인 서버를 중지합니다.
- 그것을 마우스 오른쪽 버튼으로 클릭하고 추가 및 제거를 선택하십시오.
- 오른쪽에 프로젝트 추가
- 마침을 클릭
자, Eclipse와 Wildfly가 함께 구성되었습니다!
이것은 프로젝트 외부에 필요한 모든 구성입니다. 프로젝트 구성으로 넘어 갑시다.
부트스트랩 프로젝트
이제 Eclipse와 Wildfly가 구성되고 프로젝트가 생성되었으므로 프로젝트를 구성해야 합니다.
우리가 할 첫 번째 일은 build.gradle을 편집하는 것입니다. 다음과 같이 표시됩니다.
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse' apply plugin: 'eclipse-wtp' sourceCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { jcenter() } eclipse { wtp { } } dependencies { providedCompile 'org.hibernate:hibernate-entitymanager:5.0.7.Final' providedCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.14.Final' providedCompile 'javax:javaee-api:7.0' }
이 명령은 최종 war 파일에 종속성을 추가하지 않기 때문에 종속성은 모두 "providedCompile"로 선언됩니다. Wildfly에는 이미 이러한 종속성이 있으며 그렇지 않으면 앱의 종속성과 충돌이 발생할 수 있습니다.
이 시점에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Gradle(STS) -> 모두 새로 고침을 선택하여 방금 선언한 종속성을 가져올 수 있습니다.
Hibernate가 필요로 하는 정보를 포함하는 파일인 "persistence.xml" 파일을 생성하고 구성할 시간:
- src/main/resource 소스 폴더에 META-INF라는 폴더를 만듭니다.
- 이 폴더 안에 persistence.xml이라는 파일을 만듭니다.
파일의 내용은 다음과 같아야 하며, Wildfly에서 생성한 데이터 원본과 com.toptal.andrehil.mt.hibernate package com.toptal.andrehil.mt.hibernate
를 다음에 생성할 데이터 원본과 일치하도록 jta-data-source를 변경해야 합니다. 섹션(동일한 패키지 이름을 선택하지 않는 한):
<?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="pu"> <jta-data-source>java:/JavaEEMTDS</jta-data-source> <properties> <property name="hibernate.multiTenancy" value="SCHEMA"/> <property name="hibernate.tenant_identifier_resolver" value="com.toptal.andrehil.mt.hibernate.SchemaResolver"/> <property name="hibernate.multi_tenant_connection_provider" value="com.toptal.andrehil.mt.hibernate.MultiTenantProvider"/> </properties> </persistence-unit> </persistence>
최대 절전 모드 클래스
persistence.xml에 추가된 구성은 두 개의 사용자 정의 클래스 MultiTenantProvider 및 SchemaResolver를 가리킵니다. 첫 번째 클래스는 올바른 스키마로 구성된 연결을 제공하는 역할을 합니다. 두 번째 클래스는 사용할 스키마의 이름을 확인하는 역할을 합니다.

다음은 두 클래스의 구현입니다.
public class MultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService { private static final long serialVersionUID = 1L; private DataSource dataSource; @Override public boolean supportsAggressiveRelease() { return false; } @Override public void injectServices(ServiceRegistryImplementor serviceRegistry) { try { final Context init = new InitialContext(); dataSource = (DataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name } catch (final NamingException e) { throw new RuntimeException(e); } } @SuppressWarnings("rawtypes") @Override public boolean isUnwrappableAs(Class clazz) { return false; } @Override public <T> T unwrap(Class<T> clazz) { return null; } @Override public Connection getAnyConnection() throws SQLException { final Connection connection = dataSource.getConnection(); return connection; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); try { connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { try { connection.createStatement().execute("SET SCHEMA 'public'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [public]", e); } connection.close(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } }
위의 명령문에서 사용되는 구문은 PostgreSQL 및 일부 다른 데이터베이스에서 작동합니다. 데이터베이스에 현재 스키마를 변경하기 위한 다른 구문이 있는 경우 이 구문을 변경해야 합니다.
public class SchemaResolver implements CurrentTenantIdentifierResolver { private String tenantIdentifier = "public"; @Override public String resolveCurrentTenantIdentifier() { return tenantIdentifier; } @Override public boolean validateExistingCurrentSessions() { return false; } public void setTenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; } }
이 시점에서 이미 응용 프로그램을 테스트할 수 있습니다. 지금은 해석기가 하드 코딩된 공개 스키마를 직접 가리키고 있지만 이미 호출되고 있습니다. 이렇게 하려면 실행 중인 서버를 중지하고 다시 시작하십시오. 디버그 모드에서 실행하고 위의 클래스 중 아무 곳에나 중단점을 배치하여 작동하는지 확인할 수 있습니다.
리졸버의 실제 사용
그렇다면 어떻게 리졸버가 스키마의 올바른 이름을 실제로 포함할 수 있을까요?
이를 달성하는 한 가지 방법은 모든 요청의 헤더에 식별자를 유지한 다음 스키마 이름을 삽입하는 필터를 만드는 것입니다.
사용법을 예시하기 위해 필터 클래스를 구현해 보겠습니다. resolver는 Hibernate의 SessionFactory를 통해 액세스할 수 있으므로 이를 활용하여 올바른 스키마 이름을 주입할 것입니다.
@Provider public class AuthRequestFilter implements ContainerRequestFilter { @PersistenceUnit(unitName = "pu") private EntityManagerFactory entityManagerFactory; @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory(); final SchemaResolver schemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver(); final String username = containerRequestContext.getHeaderString("username"); schemaResolver.setTenantIdentifier(username); } }
이제 클래스가 데이터베이스에 액세스하기 위해 EntityManager를 가져오면 이미 올바른 스키마로 구성되어 있습니다.
단순화를 위해 여기에 표시된 구현은 헤더의 문자열에서 직접 식별자를 가져오는 것이지만 인증 토큰을 사용하고 토큰에 식별자를 저장하는 것이 좋습니다. 이 주제에 대해 더 알고 싶다면 JSON 웹 토큰(JWT)을 살펴보는 것이 좋습니다. JWT는 토큰 조작을 위한 훌륭하고 간단한 라이브러리입니다.
이 모든 것을 사용하는 방법
모든 것이 구성되면 EntityManager
와 상호 작용하는 엔터티 및/또는 클래스에서 수행할 다른 작업이 없습니다. EntityManager에서 실행하는 모든 것은 생성된 필터에 의해 해결된 스키마로 전달됩니다.
이제 클라이언트 측에서 요청을 가로채서 서버 측으로 보낼 헤더에 식별자/토큰을 삽입하기만 하면 됩니다.
기사 끝에 있는 링크는 이 기사를 작성하는 데 사용된 프로젝트를 가리킵니다. Flyway를 사용하여 2개의 스키마를 생성하고 Car라는 엔티티 클래스와 프로젝트를 테스트하는 데 사용할 수 있는 CarService
라는 나머지 서비스 클래스를 포함합니다. 아래의 모든 단계를 따를 수 있지만 고유한 프로젝트를 만드는 대신 복제하여 이 프로젝트를 사용할 수 있습니다. 그런 다음 실행할 때 간단한 HTTP 클라이언트(예: Chrome용 Postman 확장)를 사용하고 헤더 key:value를 사용하여 http://localhost:8080/javaee-mt/rest/cars에 GET 요청을 할 수 있습니다.
- 사용자 이름: 조; 또는
- 사용자 이름:프레드.
이렇게 하면 요청은 다른 스키마에 있는 다른 값을 반환합니다. 하나는 joe라고 하고 다른 하나는 "fred"라고 합니다.
마지막 단어
이것은 Java 세계에서 다중 테넌트 응용 프로그램을 만드는 유일한 솔루션은 아니지만 이를 달성하는 간단한 방법입니다.
명심해야 할 한 가지는 다중 테넌트 구성을 사용할 때 Hibernate가 DDL을 생성하지 않는다는 것입니다. 제 제안은 데이터베이스 생성을 제어하는 훌륭한 라이브러리인 Flyway 또는 Liquibase를 살펴보는 것입니다. Hibernate 팀이 프로덕션에서 자동 데이터베이스 생성을 사용하지 말라고 조언하므로 다중 테넌트를 사용하지 않을 경우에도 이것은 좋은 일입니다.
이 기사와 환경 구성을 만드는 데 사용된 소스 코드는 github.com/andrehil/JavaEEMT에서 찾을 수 있습니다.