본문 바로가기

개발 기록/Spring-boot

[Spring boot] EmbeddedMongo 기반 Multi DataSource(database) 설정

반응형

임베디드 MongoDB는 사실 라이브러리 Scope를 테스트에서만 돌리는것을 권장하여 testImplementaion 으로 설정을 한다.

 

한번 어플리케이션 레벨에 구동도 하고 싶고 다중 데이터 베이스 및 멀티 트랜잭션을 설정하는 방법을 찾아보았다.

 

구글링을 해봐도 내가 원하는 질문이나 답변은 없어서 하나하나 찾아서 결합 해보았다.

 

일단 찾아보니 조건 이러했다.

 

1. 멀티 트랜잭션을 설정하고 사용하려면 우선적으로 MongoDB가 Replication 설정이 되어 있어야 한다.

 

2. 멀티 트랜잭션을 통해 CRUD를 하는 경우 우선적으로 Collection이 생성 되어 있어야 한다.

 

3. 설정된 트랜잭션에 설정된 CollectionEntity Repository 외에 타 트랜잭션 및 데이터베이스의 CollectionEntity Repository의

    CRUD는 되지 않는다. (당연하지만 혹시나 해서 적어본다.)

 

 

- build.gradle 설정 (가장 기본적인거만 설정함)

embed.mongo 라이브러리를 implementaion으로 변경한다. (테스트코드에서만 구동할거라면 굳이 변경 안해도 된다.)

plugins {
    id 'org.springframework.boot' version '2.4.12'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo'
}

test {
    useJUnitPlatform()
}

 

- EmbeddedMongo Replication 설정 

기본적으로 EmbeddedMongo을 연결하게 되면 STANDALONE 모드로 구동된다. 그래서 별도로 Replication 으로 구동되도록 설정해주어야 한다. 아래 링크에서 해당 소스를 발췌해왔으며, 추가적으로 구동 후 kill not process가 발생하여 Embedded MongoDB를 종료시킬 수 없다라는 Exception이 발생한다. 하지만 발생했다고 테스트 케이스가 에러가 나는것도 아니며, 프로세스를 확인해보면 정상 kill이 되어 있는것을 확인할수 있기 때문에 신경 안써도 될 듯 하다.

 

@Profile("local")
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore({ MongoAutoConfiguration.class })
@ConditionalOnClass({ MongoClient.class, MongodStarter.class })
@Import({
    EmbeddedMongoAutoConfiguration.class,
    EmbeddedMongoReplisetConfig.DependenciesConfiguration.class
})
public class EmbeddedMongoReplisetConfig {

    public static final int DFLT_PORT_NUMBER = 11046;
    public static final String DFLT_REPLICASET_NAME = "emb";
    public static final int DFLT_STOP_TIMEOUT_MILLIS = 200;

    private Version.Main mFeatureAwareVersion = Version.Main.V4_0;
    private int mPortNumber = DFLT_PORT_NUMBER;
    private String mReplicaSetName = DFLT_REPLICASET_NAME;
    private long mStopTimeoutMillis = DFLT_STOP_TIMEOUT_MILLIS;

    @Bean
    public IMongodConfig mongodConfig() throws UnknownHostException, IOException {
        final IMongodConfig mongodConfig = new MongodConfigBuilder().version(mFeatureAwareVersion)
            .withLaunchArgument("--replSet", mReplicaSetName)
            .stopTimeoutInMillis(mStopTimeoutMillis)
            .cmdOptions(new MongoCmdOptionsBuilder().useNoJournal(false).build())
            .net(new Net(mPortNumber, Network.localhostIsIPv6())).build();
        return mongodConfig;
    }

    /**
     * Initializes a new replica set.
     * Based on code from https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/issues/257
     */
    class EmbeddedMongoReplicaSetInitialization {

        EmbeddedMongoReplicaSetInitialization() throws Exception {
            MongoClient mongoClient = null;
            try {
                final BasicDBList members = new BasicDBList();
                members.add(new Document("_id", 0).append("host", "localhost:" + mPortNumber));

                final Document replSetConfig = new Document("_id", mReplicaSetName);
                replSetConfig.put("members", members);

                mongoClient = MongoClients.create("mongodb://localhost:"+DFLT_PORT_NUMBER);
                final MongoDatabase adminDatabase = mongoClient.getDatabase("admin");
                adminDatabase.runCommand(new Document("replSetInitiate", replSetConfig));
            }
            finally {
                if (mongoClient != null) {
                    mongoClient.close();
                }
            }
        }
    }

    @Bean
    EmbeddedMongoReplicaSetInitialization embeddedMongoReplicaSetInitialization() throws Exception {
        return new EmbeddedMongoReplicaSetInitialization();
    }

    /**
     * Additional configuration to ensure that the replica set initialization happens after the
     * {@link MongodExecutable} bean is created. That's it - after the database is started.
     */
    @ConditionalOnClass({ MongoClient.class, MongodStarter.class })
    protected static class DependenciesConfiguration
        extends AbstractDependsOnBeanFactoryPostProcessor {

        DependenciesConfiguration() {
            super(EmbeddedMongoReplicaSetInitialization.class, null, MongodExecutable.class);
        }
    }

}

참조 : https://apisimulator.io/spring-boot-auto-configuration-embedded-mongodb-transactions/

 

Spring Boot Embedded MongoDB with Support for Transactions

Spring Boot auto-configuration for a single node embedded MongoDB with support for multi-document transactions on a replica set.

apisimulator.io

 

- EmbeddedMongo DataSource 및 트랜잭션 설정 

@Configuration
public class MongodbConfig {

    private String mongodb_uri1 = "mongodb://localhost:11046/test1?replicaSet=emb&readPreference=primary&ssl=false";

    private String mongodb_uri2 = "mongodb://localhost:11046/test2?replicaSet=emb&readPreference=primary&ssl=false";

    @Bean(name = "primaryMongoTemplate")
    @Primary
    public MongoTemplate primaryMongoTemplate() {
        MongoDatabaseFactory factory = new SimpleMongoClientDatabaseFactory(mongodb_uri1);
        MappingMongoConverter converter = new MappingMongoConverter(
                new DefaultDbRefResolver(factory),
                new MongoMappingContext());
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));
        MongoTemplate mongoTemplate = new MongoTemplate(factory, converter);
        mongoTemplate.setSessionSynchronization(SessionSynchronization.ALWAYS);
        return mongoTemplate;
    }

    @Bean(name = "secondaryMongoTemplate")
    public MongoTemplate secondaryMongoTemplate() {
        MongoDatabaseFactory factory = new SimpleMongoClientDatabaseFactory(mongodb_uri2);
        MappingMongoConverter converter = new MappingMongoConverter(
                new DefaultDbRefResolver(factory),
                new MongoMappingContext());
        MongoTemplate mongoTemplate = new MongoTemplate(factory, converter);
        mongoTemplate.setSessionSynchronization(SessionSynchronization.ALWAYS);
        return mongoTemplate;
    }

    @Primary
    @Bean("primaryTransactionManager")
    public MongoTransactionManager primaryTransactionManager() {
        return new MongoTransactionManager(new SimpleMongoClientDatabaseFactory(mongodb_uri1));
    }

    @Bean("secondaryTransactionManager")
    public MongoTransactionManager secondaryTransactionManager() {
        return new MongoTransactionManager(new SimpleMongoClientDatabaseFactory(mongodb_uri2));
    }
}

 

- Database 설정 별 사용할 Repository 패키지 설정

@Configuration
@EnableMongoRepositories(basePackages = "com.example.mongodb.repository.test1",
        mongoTemplateRef = Test1MongoConfig.MONGO_TEMPLATE)
public class Test1MongoConfig {

    protected static final String MONGO_TEMPLATE = "primaryMongoTemplate";
}
@Configuration
@EnableMongoRepositories(basePackages = "com.example.mongodb.repository.test2",
        mongoTemplateRef = Test2MongoConfig.MONGO_TEMPLATE)
public class Test2MongoConfig {

    protected static final String MONGO_TEMPLATE = "secondaryMongoTemplate";
}

 

- Test 코드 작성

@BeforeEach와 @AfterEach 어노테이션으로 Collection을 생성 및 삭제 하는 경우 선언된 트랜잭션 어노테이션 기반으로 실행 되기 때문에 Collection 생성 및 삭제가 실행되지 않는다. 그래서 @PostContruct와 @PreDestroy 어노테이션을 통해 Collection을 생성하고 테스트 후 삭제되도록 해두었다.

 

@SpringBootTest
class Test1RepositoryTest {

    @Autowired
    @Qualifier("primaryMongoTemplate")
    private MongoTemplate primaryTemplate;

    @Autowired
    @Qualifier("secondaryMongoTemplate")
    private MongoTemplate secondaryTemplate;

    @Autowired
    private Test1Repository test1Repository;

    @Autowired
    private Test2Repository test2Repository;

    @PostConstruct
    void init() {
        primaryTemplate.createCollection("Test1");
        secondaryTemplate.createCollection("Test2");
    }

    @PreDestroy
    void delete() {
        primaryTemplate.dropCollection("Test1");
        secondaryTemplate.dropCollection("Test2");
    }

    @Transactional(transactionManager = "primaryTransactionManager")
    @Test
    void test() {
        Test1 test1 = Test1.builder().title("test").build();
        test1Repository.save(test1);

        List<Test1> list1 = test1Repository.findAll();

        for (Test1 test11 : list1) {
            System.out.println("test11.getTitle() = " + test11.getTitle());
        }

        test1Repository.deleteAll();
    }

    @Transactional(transactionManager = "secondaryTransactionManager")
    @Test
    void test2() {
        Test2 test2 = Test2.builder().title("test2").build();
        test2Repository.save(test2);

        List<Test2> list2 = test2Repository.findAll();
        for (Test2 test21 : list2) {
            System.out.println("test21.getTitle() = " + test21.getTitle());
        }

        test2Repository.deleteAll();
    }
}

 

 

github 소스 : https://github.com/realwater20/multimongodb

 

 

 

반응형