MongoDB에서 다수의 데이터베이스를 운영해야 하는 경우 ScopedValue (JDK 21 preview)를 이용하여 동적으로 해당 데이터베이스에 연결하는 방법에 대해서 소개하고자 한다.
많은 수의 데이터베이스가 동적으로 생성되는 경우에 유용할 것이라고 생각한다. (ex. 테넌트별로 데이터베이스를 생성해야 하는 경우)
사용하는 데이터베이스 수가 정해져 있고 소수의 경우에는 각각의 데이터베이스에 대해서 Configuration 구성을 하는 방법도 있는데 해당 방법은 아래 링크를 참고하면 도움이 될 것 같다.
https://www.baeldung.com/mongodb-multiple-databases-spring-data
ScopedValue 대신에 ThreadLocal을 활용해도 되지만 ScopedValue에 비해서 몇 가지 단점이 있는데 해당 내용은 아래 ScopedValue관련 포스팅을 참고하기 바란다.
2023.11.26 - [자바] - java21 - scoped value에 대해서 알아보자
ScopdedValue 활성화
JDK21 버전에서 preview 기능으로 도입된 ScopedValue를 활성화 하기 위해서는 --enable-preview 옵션을 지정하거나 IDE에서 지원하는 Preview 설정을 하면 되겠다.
Language level을 21(preview)로 지정했는데 간혹 Preview 기능이 적용되지 않는 경우가 있는 듯하다. 이러한 경우에는 maven이나 gradle 빌드 구성에서 --enable-preview를 활성화할 수 있다.
maven (pom.xml)
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>
--enable-preview
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
gradle (build.gradle)
java {
sourceCompatibility = '21'
}
compileJava {
options.compilerArgs += ['--enable-preview']
}
추가로 intellij IDE에서 gradle을 사용하는 경우 Setting >> Build, Execution, Deployment >> Build tools >> Gradle >> Gradle JVM 설정에서 JDK 버전을 21로 지정해야 한다.
auto-index-creation 설정
- auto-index-creation 설정은 Entity 도메인 클래스에 @Indexed 어노테이션이 지정된 필드에 대해서 자동으로 Index를 생성하는 옵션이다.
- auto-index-creation 옵션이 true로 지정된 경우 MongoTemplate 빈이 생성될 때 MongoTemplate 클래스의 아래 생성자 내에서 index를 생성한다.
public MongoTemplate(MongoDatabaseFactory mongoDbFactory, @Nullable MongoConverter mongoConverter) {
Assert.notNull(mongoDbFactory, "MongoDbFactory must not be null");
this.mongoDbFactory = mongoDbFactory;
this.exceptionTranslator = mongoDbFactory.getExceptionTranslator();
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter(mongoDbFactory) : mongoConverter;
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.operations = new EntityOperations(this.mongoConverter, this.queryMapper);
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDbFactory);
this.eventDelegate = new EntityLifecycleEventDelegate();
// We always have a mapping context in the converter, whether it's a simple one or not
mappingContext = this.mongoConverter.getMappingContext();
// We create indexes based on mapping events
if (mappingContext instanceof MongoMappingContext mappingContext) {
if (mappingContext.isAutoIndexCreation()) {
//아래 indexCreator 인스턴스 생성 시점에 기본 데이터베이스에 대해서 @Indexed 필드가 지정된 entity에 대해서 index를 생성한다.
indexCreator = new MongoPersistentEntityIndexCreator(mappingContext, this);
eventPublisher = new MongoMappingEventPublisher(indexCreator);
mappingContext.setApplicationEventPublisher(eventPublisher);
}
}
}
- MongoTemplate 생성자 내에서 index를 생성하기 때문에 모든 데이터베이스에 대해서 auto-index-creation 옵션으로 index를 자동으로 생성하려면 데이터베이스 수만큼 MongoTemplate 빈을 등록해야 한다.
- 데이터베이스 수만큼 MongoTemplate 빈을 등록해야 한다는 것은 곧 모든 데이터베이스에 대해서 Configuration을 구성해야 한다는 것이다.
- 이렇게 사용할 수 있는 경우는 데이터베이스가 미리 정의되어 있고 그 수가 적은 경우에 가능하다.
- 동적으로 많은 수의 데이터베이스가 생성되어야 하는 경우에는 auto-index-creation 옵션을 적용하기 어렵다는 것이다.
- 이번 포스팅에서는 많은 수의 데이터베이스가 동적으로 생성되는 경우로, 각 데이터베이스에 index를 직접 생성하도록 하고 auto-index-creation 프로퍼티는 비활성화 할 것이다.
spring:
data:
mongodb:
auto-index-creation: false
ScopedValue 관리 클래스
이번 포스팅에서 핵심 역할을 하는 ScopedValue 인스턴스를 관리할 클래스 샘플 코드다.
public class ScopedValues {
private static final ScopedValue<String> DATABASE_NAME = ScopedValue.newInstance();
public static ScopedValue.Carrier getCarrierForDBName(String dbName) {
return ScopedValue.where( DATABASE_NAME, dbName );
}
public static ScopedValue<String> getScopedValueForDBName() {
return DATABASE_NAME;
}
}
SimpleMongoDatabaseFactory 빈 생성
- 동적으로 여러 데이터베이스에 접속을 하기위한 방법으로 SimpleMongoDatabaseFactory의 doGetMongoDatabase() 메서드를 재정의한 빈을 생성한다.
- 해당 빈의 doGetMongoDatabase() 메서드는 ScopedValue에 바인딩된 데이터베이스 이름으로 연결되도록 하는 것이다.
//MongoDatabaseFactory 빈을 직접 생성하는 경우 MongoClient 빈도 생성해 줘야 한다.
@Bean
public MongoClient mongo(ObjectProvider<MongoClientSettingsBuilderCustomizer> builderCustomizers,
MongoClientSettings settings) {
return new MongoClientFactory(builderCustomizers.orderedStream().toList()).createMongoClient(settings);
}
@Bean
public MongoDatabaseFactory mongoDatabaseFactory( MongoClient mongoClient,
MongoProperties properties,
MongoConnectionDetails connectionDetails) {
String defaultDatabase = properties.getDatabase();
if (defaultDatabase == null) {
defaultDatabase = connectionDetails.getConnectionString().getDatabase();
}
//SimpleMongoClientDatabaseFactory를 상속하여 doGetMongoDatabase를 override
//doGetMongoDatabase는 ScopedValue 타입의 DATABASE_NAME에 db명이 지정된 경우 해당 database를 리턴한다.
//ScopedValue 타입의 DATABASE_NAME에 db명이 지정되지 않은 경우 기본 database를 리턴한다.
return new SimpleMongoClientDatabaseFactory(mongoClient, defaultDatabase) {
@Override
protected MongoDatabase doGetMongoDatabase( String dbName ) {
return super.doGetMongoDatabase( ScopedValues.getScopedValueForDBName().orElse( dbName ) );
}
};
}
- MongoDatabaseFactory 빈을 직접 생성하는 경우에는 MongoClient 빈도 직접 생성해 줘야 한다.
- MongoDB operation을 수행할 때 MongoDatabaseFactory 빈의 doGetMongoDatabase 메서드가 호출되어 연결하고자 하는 데이터베이스 인스턴스를 가져오는데 이때 ScopedValue에 바인딩된 데이터에 연결하고자 하는 데이터베이스 이름을 설정하여 해당 데이터베이스 인스턴스를 가져오도록 하는 것이다.
- 이를 위해서는 MongoDB operation을 수행하는 지점에서 ScopedValue에 연결하고자 하는 데이터베이스 이름을 바인딩 해야 한다.
- ScopedValue에 바인딩된 값이 없다면 기본 데이터베이스 인스턴스를 리턴하도록 한다.
MongoDB operation 코드
SimpleMongoDatabaseFactory 빈 생성 섹션에서 설명한대로 MongoDB operation을 수행하는 지점에서 ScopedValue에 연결하고자 하는 데이터베이스 이름을 바인딩해야 하는 데 사용법은 다음과 같다.
//MongoTemplate 빈 주입
private final MongoTemplate mongoTemplate;
....
String dynamicallyAddedDBName = "test2";
ScopedValue.Carrier carrierForDBName = ScopedValues.getCarrierForDBName( dynamicallyAddedDBName );
carrierForDBName.run( () -> {
Person person = Person.builder()
.personId( UUID.randomUUID() )
.personName( "test-name-other-database" )
.personAge( 50 )
.build();
person = mongoTemplate.insert( person );
Query query = new Query();
query.addCriteria( where( "_id" ).is( person.getPersonId()) );
mongoTemplate.findOne( query, Person.class, COLLECTION_NAME );
} );
ScopedValues.getCarrierForDBName(dbName) 메서드 내에서 'DATABASE_NAME' ScopedValue 인스턴스에 인자로 전달된 데이터베이스 이름을 바인딩한다.
데이터베이스 존재 여부 체크 및 DB 생성시 index 생성
추가로 auto-index-creation 프로퍼티를 false로 비활성화했으므로 새로운 데이터베이스를 생성 시에는 데이터베이스에 들어갈 collection들에 대해서 index를 미리 생성해 주는 것이 좋겠다. 다음은 그에 대한 샘플코드다.
데이터베이스 존재 여부 체크 코드
private final static String COLLECTION_NAME = "person";
//MongoClient 빈 주입
private final MongoClient mongoClient;
...
public boolean isNotExistDatabase(String databaseName) {
return StreamUtils.createStreamFromIterator( mongoClient.listDatabaseNames().iterator() )
.noneMatch( dbName -> dbName.equals( databaseName ) );
}
index 생성 관리 클래스
@Service
@RequiredArgsConstructor
@Slf4j
public class MongoDBIndexService {
private final MongoTemplate mongoTemplate;
private final static String COLLECTION_NAME = "person";
/**
* person collection에 index를 생성하는 메서드
*/
public void createIndexPerson() {
IndexOperations indexOperations = mongoTemplate.indexOps( COLLECTION_NAME );
//personName 오름차순 인덱스 생성
//auto-index-creation이 true 인 경우 @Indexed가 붙은 필드명 이름으로 인덱스가 자동 생성된다.
//conflict 오류를 피하려면 named를 지정하여 자동으로 생성되었던 필드명으로 인덱스 이름을 지정해야 한다.
//혹은 auto-index-creation을 false로 지정하고 직접 인덱스를 생성한다.
indexOperations.ensureIndex(
new Index().on( "personName", Sort.Direction.ASC )
.named( "personName" ));
indexOperations.ensureIndex( new CompoundIndexDefinition(
Document.parse( "{personGender: 1, personNation: -1}" )
).named( "GenderNationIdx" )
);
}
}
- 위 샘플 코드는 person collection에 personName 필드를 오름차순으로 index를 생성하고 personGender를 오름차순 + personNation을 내림차순으로 복합 index를 생성하는 샘플코드다.
데이터베이스 생성 및 index 생성 메서드 호출 코드
String dynamicallyAddedDBName = "test2";
if (isNotExistDatabase(dynamicallyAddedDBName)) {
createDatabase(dynamicallyAddedDBName);
}
public void createDatabase(String databaseName) {
ScopedValue.Carrier carrierForDBName = ScopedValues.getCarrierForDBName( databaseName );
carrierForDBName.run( mongoDBIndexService::createIndexPerson );
}
- 인자로 전달된 데이터베이스 이름을 ScopedValue에 바인딩하여 해당 데이터베이스에 접속(test2) 하고 person collection에 index를 생성하는 메서드를 호출하는 코드다.
- 만약 test2 데이터베이스가 없다면 자동으로 test2 데이터베이스와 person collection이 생성되고 person collection에 지정된 index가 생성된다.
지금까지 ScopedValue를 활용하여 동적으로 생성되는 여러 데이터베이스에 접속하는 방법에 대해서 적어보았다.
아직까지 ScopedValue가 JDK21 preview 기능이긴 하지만 추후 자바 릴리즈에서 정식 기능으로 되지 않을까 한다.
ScopedValue 대신 ThreadLocal을 대체해도 될 것이다. (ThreadLocal은 직접 확인 해보시길 바랍니다.)
샘플 코드에서 사용된 mongodb는 spring boot docker compose support를 사용하였다.
spring boot docker compose에 대해서는 아래 포스팅을 참고하기 바란다.
2023.10.22 - [스프링부트] - spring boot 3.1 docker compose support
'스프링부트' 카테고리의 다른 글
spring data MongoDB - Repository 사용하기 (0) | 2024.02.20 |
---|---|
spring data MongoDB - 필드 변환하기 (Converter) (0) | 2024.02.04 |
spring data MongoDB - MongoTemplate 설정하기 (0) | 2024.01.01 |
spring boot data MongoDB - 연결 설정 하기 (0) | 2023.12.29 |
spring boot 가상 스레드 적용해보기 (0) | 2023.12.03 |
댓글