이전글에 이어서 진행하도록 하겠습니다.
@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver {
static final String DEFAULT_TENANT = "default";
@Override
public String resolveCurrentTenantIdentifier() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Predicate.not(authentication -> authentication instanceof AnonymousAuthenticationToken))
.map(Principal::getName)
.orElse(DEFAULT_TENANT);
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
username을 통해 어떤 테넌트를 사용할 지 결정해주는 클래스입니다.
인증 정보가 없다면 default라는 스키마를 사용하게 됩니다.
@Component
public class TenantConnectionProvider implements MultiTenantConnectionProvider {
private DataSource datasource;
public TenantConnectionProvider(DataSource dataSource) {
this.datasource = dataSource;
}
@Override
public Connection getAnyConnection() throws SQLException {
return datasource.getConnection();
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = getAnyConnection();
connection.createStatement()
.execute(String.format("SET SCHEMA \"%s\";", tenantIdentifier));
return connection;
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
connection.createStatement()
.execute(String.format("SET SCHEMA \"%s\";", TenantIdentifierResolver.DEFAULT_TENANT));
releaseAnyConnection(connection);
}
@Override
public boolean supportsAggressiveRelease() {
return false;
}
@Override
public boolean isUnwrappableAs(Class unwrapType) {
return false;
}
@Override
public <T> T unwrap(Class<T> unwrapType) {
return null;
}
}
실제로 DB에 SET SCHEMA 명령을 내려주는 클래스입니다.
@Configuration
public class HibernateConfig {
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaProperties jpaProperties,
MultiTenantConnectionProvider multiTenantConnectionProvider, CurrentTenantIdentifierResolver tenantIdentifierResolver) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan(MultiTenancyApplication.class.getPackage().getName());
em.setJpaVendorAdapter(jpaVendorAdapter());
Map<String, Object> jpaPropertiesMap = new HashMap<>(jpaProperties.getProperties());
jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
em.setJpaPropertyMap(jpaPropertiesMap);
return em;
}
}
하이버네이트 설정을 위한 클래스입니다. 앞서 생성한 Resolver, Provider 클래스가 사용됩니다.
@Configuration
public class FlywayConfig {
@Bean
public Flyway flyway(DataSource dataSource) {
Flyway flyway = Flyway.configure()
.locations("db/migration/default")
.dataSource(dataSource)
.schemas(TenantIdentifierResolver.DEFAULT_TENANT)
.load();
flyway.migrate();
return flyway;
}
@Bean
CommandLineRunner commandLineRunner(UserRepository repository, DataSource dataSource) {
return args -> {
repository.findAll().forEach(user -> {
String tenant = user.getUsername();
Flyway flyway = Flyway.configure()
.locations("db/migration/tenants")
.dataSource(dataSource)
.schemas(tenant)
.load();
flyway.migrate();
});
};
}
}
Flyway라는 마이그레이션 도구를 위한 설정 클래스입니다.
예를들면 철수 스키마에 Note라는 테이블이 있고, 영희 스키마에 Note라는 테이블이 있을때
Note 테이블을 수정한다면, 모든 스키마에 반영을 해주어야합니다.
이때 필요한것이 마이그레이션 도구입니다.
-- db/migration/default/V1__init_schema.sql
CREATE TABLE user
(
id BIGINT AUTO_INCREMENT,
username VARCHAR(255) UNIQUE,
password VARCHAR(255)
);
-- db/migration/tenants/V1__init_schema.sql
CREATE TABLE note
(
id BIGINT AUTO_INCREMENT,
text TEXT
);
resources 폴더 아래 db/migration/default 경로에 해당 이름으로 SQL 파일을 추가합니다.
default와 tenants로 폴더가 나뉘어져 있는데, default는 마스터, tenants는 자식 스키마로 생각하시면 됩니다.
파일 네이밍이 특이한데요 __ 앞에 V1 부분은 버전을 나타냅니다.
추후에 SQL 파일 수정 필요시 V1 파일을 수정하는것이 아닌 V2, V3 등으로 버전이 다른 SQL 파일을 추가해주면 됩니다.
@Component
public class TenantService {
private DataSource dataSource;
public TenantService(DataSource dataSource) {
this.dataSource = dataSource;
}
public void initDatabase(String schema) {
Flyway flyway = Flyway.configure()
.locations("db/migration/tenants")
.dataSource(dataSource)
.schemas(schema)
.load();
flyway.migrate();
}
}
새로 추가한 사용자 이름으로 스키마를 생성하기 위한 서비스도 구현합니다.
여기까지 멀티테넌시에 필요한 구현은 모두 끝났습니다.
curl을 통해 잘 동작하는지 확인해보도록 하겠습니다.
> curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"john\",\"password\":\"password\"}" http://localhost:8080/users
{"id":1,"username":"john"}
> curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"jane\",\"password\":\"qwerty123\"}" http://localhost:8080/users
{"id":2,"username":"jane"}
위 처럼 사용자를 생성합니다. 사용자는 default 스키마에 있기 때문에, 같은 테이블에서 생성된 걸 확인할 수 있습니다.
> curl -u john:password -X POST -H "Content-Type: application/json" -d "{\"text\":\"Hello from John!\"}" http://localhost:8080/notes
{"id":1,"text":"Hello from John!"}
> curl -u jane:qwerty123 -X POST -H "Content-Type: application/json" -d "{\"text\":\"Hello from Jane!\"}" http://localhost:8080/notes
{"id":1,"text":"Hello from Jane!"}
이번엔 생성한 사용자의 인증정보를 이용하여 각 스키마에 Note 엔티티를 추가해보았습니다.
사용자를 추가할때와 다른점은, 각 스키마에 생성되었기 때문에 id가 모두 1로 출력되는 것 입니다.
> curl -u jane:qwerty123 http://localhost:8080/notes
[{"id":1,"text":"Hello from Jane!"}]
jane의 Note 목록을 보면 아이템이 1개만 들어있는것을 통해 위 내용을 다시한번 확인할 수 있습니다.
사실 따라해보면 굉장히 쉬운 내용이지만, 한글로 된 자료가 많지 않아 초기 도입 시 어려움이 있었습니다.
궁금하신 내용이 있으시면 댓글로 남겨주세요. 고맙습니다.
ref : https://sultanov.dev/blog/schema-based-multi-tenancy-with-spring-data/
'Multi Tenancy' 카테고리의 다른 글
[Spring Boot] 스키마 기반 멀티테넌시 구현 (1/2) (0) | 2022.08.09 |
---|