회사에서 신규 서비스를 개발하고 있는데, API개발과 배치서버 개발 업무를 맡게 되었다.
배치 서버에서는 여러 개의 스키마로 구성된 DB의 한 테이블에서 날짜를 받아 만료처리하는 배치 스케줄러와, 특정 데이터들을 한꺼번에 끌어모아 해당 유저에게 데이터들의 통계차트를 메일로 쏴주는 배치 스케줄러였다.
프로젝트의 특이한(?) DB 환경으로 만료처리 스케줄러에서도 문제가 많았다. DB가 PostgreSQL이고, Database-Schema-Table로 구성되어있는데, 하나의 데이터베이스에 여러 개의 스키마로 구성되어 그 스키마를 전부 돌아야 했다.
(Mysql로 생각하면 여러개의 Database를 사용하는 것)
유저들과, 공통으로 사용할 데이터들은 public 스키마에 존재하고, B2B모델이기 때문에 고객사 별로 스키마가 존재한다.
이 고객사 스키마들은 전부 똑같은 테이블, 칼럼을 가지고 있으며 API를 개발할 때는 토큰에 고객사 정보를 담아 테넌시를 변경해주고 있다.
이러한 환경에서 API를 개발할 때는 멀티테넌시를 사용해서 해결을 하였지만, 스프링 배치에서는 어떻게 해결할 지 감이 잘 오지 않았다.
멀티테넌시 환경을 구성하는 방법은 아래를 참고하여 구현하였다.
https://www.baeldung.com/multitenancy-with-spring-data-jpa
멀티테넌시 환경 구성
- DataConnectionProvider 클래스 생성
package org.obt.infrastructure.db.multitenant;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class DataConnectionProvider implements MultiTenantConnectionProvider,
HibernatePropertiesCustomizer {
private final DataSource dataSource;
@Override
public Connection getAnyConnection() throws SQLException {
return getConnection("PUBLIC");
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
@Override
public Connection getConnection(String schema) throws SQLException {
Connection connection = dataSource.getConnection();
connection.setSchema(schema);
return connection;
}
@Override
public void releaseConnection(String s, Connection connection) throws SQLException {
connection.setSchema("PUBLIC");
connection.close();
}
@Override
public boolean supportsAggressiveRelease() {
return false;
}
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this);
}
@Override
public boolean isUnwrappableAs(Class<?> unwrapType) {
return false;
}
@Override
public <T> T unwrap(Class<T> unwrapType) {
return null;
}
}
DataConnection을 세팅하거나, 가져오는 클래스이다. 멀티테넌시 설정을 하고, 스키마를 되돌리는 기능이 있다.
- TenantIdentifierResolver 클래스 생성
package org.obt.infrastructure.db.multitenant;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Slf4j
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer {
private final ThreadLocal<String> currentTenant = ThreadLocal.withInitial(() -> "public");
public void setCurrentTenant(String tenant) {
log.info(">>>>>>>> {} <<<<<<<<< 스키마 설정 완료", tenant.trim());
currentTenant.set(tenant.toLowerCase().trim());
}
@Override
public String resolveCurrentTenantIdentifier() {
return currentTenant.get();
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
}
public void removeCurrentTenant() {
currentTenant.remove();
}
}
ThreadLocal로 동시성이슈가 생기지 않도록 해당 테넌시가 위 코드에서만 유효할 수 있도록 쓰레드 영역 변수로 설정하였다.
setCurrentTenant로 현재 datasource가 바라보는 테넌시를 변경하도록 설정할 수 있다.
멀티테넌시 구조에서 스프링 배치 사용
멀티테넌시에 대한 레퍼런스가 너무 적어 스프링 배치에서 어떻게 스키마 별로 수행해야 하는지 전혀 감이 잡히지 않았다.
만료처리 배치 스케줄러는 여러 개의 스키마를 반복해서 돌아야 하지만 각각 사실 많은 쿼리가 필요하지 않았다. 그래서 Job을 분리시키기 보다는 한 방에 쿼리를 날려버리고 싶었다.
여기에서 내가 생각한 3가지 방법은 프로시저, 스키마별 Job, 스키마별 Step이 있다.
DataSource를 갈아끼면 되지 왜 이 방법을 사용하냐 하면, 현재 멀티테넌시를 변경하는게 DataSource를 바꾸는 것이 아닌 DataSource의 currentSchema 값만 바꾸어 주기 때문.
DataSource를 사용하는 방법도 있을것 같기는 한데 이것저것 시도해본 결과 계속 하나의 DataSource에만 고정이 된다던지, 빈 주입에 실패했다던지 하는 에러가 나타났다.
이 에러는 멀티테넌시 설정할 때도 나타났었던 기억이 있는데, 아마 멀티테넌시 설정을 한 것과 충돌이 났을거라고 예상이 간다.
유저들과, 공통으로 사용할 데이터들은 public 스키마에 존재하고, B2B모델이기 때문에 고객사 별로 스키마가 존재한다.
이 고객사 스키마들은 전부 똑같은 테이블, 칼럼을 가지고 있으며 API를 개발할 때는 토큰에 고객사 정보를 담아 테넌시를 변경해주고 있다.
1. 프로시저 사용하기
여러 개의 스키마를 반복적으로 조회하는 pg/pgSql을 사용하여 프로시저를 만들고, ItemReader에서 그 프로시저를 호출하는 방법이다.
이 프로시저는 반복문으로 모든 스키마를 조회하고, 조회할 때마다 Union으로 합치는 프로시저이다.
그리고 ItemReader에서 읽은 Entity들을 다시 ItemWriter로 보내어 고정적인 작업(만료처리 상태칼럼 = true)를 수행해 주었다.
물론 ItemWriter에서 사용된 쿼리도 프로시저이다. 여러 개의 스키마를 한 방에 쓰기작업하는 pl/pgSql로 된 프로시저를 호출하는 것.
사실 프로시저를 사용하는 것은 SI식 코드 방법이라고 이는 옳은 백엔드 개발이 아니라는 소리를 들었다.
(연산이 아닌 데이터 조회, 처리 로직이 백엔드 코드에 없고 DB코드에 있으며 버전관리가 불가능하여 유지보수에 어려움이 존재)
위 설명한 만료처리 배치 스케줄러는 많은 데이터 처리가 필요하지 않으므로 비동기 처리(AsyncItemProcessor/Writer)나 멀티쓰레드 전략(Parallel Step / Multi-Threaded-Step / ...) 을 사용할 필요는 없어 보여 추가로 구현하지 않았다.
2. JobParameter로 구분하여 Job을 스키마 별로 나누어 실행하기.
그나마 제일 안정적이지 않을까 싶은 방법이다. 테넌시를 변경하고 실행함으로써, 내부에서 멀티쓰레드 전략을 사용해도 좋다.
최대 단점은 스키마 개수만큼 Job이 생성된다는 것.
하지만 스키마 개수만큼 Step을 만드는 것도 이상하다. (Step을 동적으로 만드는 것은 좋지 못한 코드라고 생각함. -> Bean 등록이 매우 어려움)
3. StepListener에 테넌시 변경하는 로직을 심고 Step을 스키마 별로 나누어 실행하기.
결론만 말하자면 스키마 개수만큼 Step을 동적으로 생성하여 하나의 Job에서 각각의 스키마로 변경된 Step들을 수행하는 것이다.
Step의 @Bean을 빼야 하므로 Scope처리가 제대로 안된다는 문제점이 있지만, 테스트 결과 수행은 잘 되지만 너무 불안정하다..
이는 사실 AOP를 구현하여 적용시켜도 된다. (AOP와 같은 방법.)
결론 : 트랜잭션매니저와 DataSource도 더 심도있게 공부해야 할 것 같다.
'DEV > 개발일기 || 트러블슈팅' 카테고리의 다른 글
Coroutine과 ReactiveMongo 다중DB 환경에서 @Transactional을 사용해 보자 (0) | 2025.03.10 |
---|---|
데이터 압축을 위한 Gorilla 알고리즘 적용 사례: 사내 솔루션 개발기 및 회고 (5) | 2024.09.03 |
JPA에서 대용량 데이터를 읽거나 수정/삭제 할때, 쿼리를 어떻게 작성해야 할까? (1) | 2024.06.10 |
공유 자원에서의 동시성 이슈는 어떻게 해결해야 할까? (0) | 2023.12.15 |