본문 바로가기
Spring/DB Migrator

[DB Migrator] SpringBoot 동적으로 MongoRepository 생성 및 등록하기

by Taler 2022. 9. 21.

2022.09.20 - [Spring/DB Migrator] - [DB Migrator] SpringBoot 동적으로 JpaRepository 생성 및 등록하기

 

[DB Migrator] SpringBoot 동적으로 JpaRepository 생성 및 등록하기

DB Migrator는 사용자의 편의를 최대한 봐주면서 Data Migration이 진행되는 것을 목표로 한다. 처음 인턴 했던 스타트업의 기술 블로그에도 작성했듯 목표로 했던 기능 중 첫 번째 편의성 기능은 Reposit

taler.tistory.com

 

이번에도 이전 글과 거의 같은 Spring 프로젝트 빌드 이후에 동적으로 Repository Interface를 등록하고, 이를 사용할 수 있도록 하는 것이 목표이다. 하지만, MongoRepository이기에 다른 점이 있었다. 이번 과정을 거치며 MongoDB Setting이 잘못됐었다는 것도 알 수 있었다. 잘못이라기보단, 확장성 없이 세팅을 했었다.

 

그럼 동적 MongoRepository 생성 및 등록은 JpaRepository와 얼마나 다른지 확인해보자.

 

 

Dynamic Register MongoRepository


사실 같다. 프로세스도 같다. 아래 그림과 순서는 그대로 사용해도 무방할 정도로 같다.

Dynamic Repository Register 프로세스

1. ByteBuddy를 이용해 MongoRepository를 동적으로 생성한다.
2. 현재 ApplicationContext에 있는 ConfigurableListableBeanFactory를 들고 온다.
3. BeanDefinitionBuilder를 사용해 MongoRepositoryFactoryBean의 definition을 구성한다.
4. 가져온 BeanFactory에 해당 BeanDefinition을 등록한다.

참고로 ByteBuddy로 MongoRepository를 생성하는 부분은 다시 다루지는 않을 것이다. ByteBuddy를 활용해서 동적으로 Java Class를 생성하고자 한다면 https://taler.tistory.com/15 이 링크를 참조하자.

 

그렇다면 무엇이 다른가?

달라지는 점은 동적으로 BeanFactory에 등록하는 과정부터이다. 특이하게도 동일한 과정이겠거니 해서 MongoRepository를 생성 후 등록하고자 하니 다음과 같은 에러가 발생했다.

 

java.lang.IllegalArgumentException: MongoOperations must not be null!
	at org.springframework.util.Assert.notNull(Assert.java:201) ~[spring-core-5.3.22.jar:5.3.22]
	at org.springframework.data.mongodb.repository.support.MongoRepositoryFactory.<init>(MongoRepositoryFactory.java:74) ~[spring-data-mongodb-3.4.2.jar:3.4.2]
	at org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean.getFactoryInstance(MongoRepositoryFactoryBean.java:106) ~[spring-data-mongodb-3.4.2.jar:3.4.2]
	at org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean.createRepositoryFactory(MongoRepositoryFactoryBean.java:89) ~[spring-data-mongodb-3.4.2.jar:3.4.2]
	at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:297) ~[spring-data-commons-2.7.2.jar:2.7.2]
	at org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean.afterPropertiesSet(MongoRepositoryFactoryBean.java:119) ~[spring-data-mongodb-3.4.2.jar:3.4.2]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1154) ~[spring-context-5.3.22.jar:5.3.22]

 

MongoOperation이 반드시 존재해야 한다는 뜻이다. 이게 웬 에러인가 싶어서 call stack을 따라서 가봤다. 하나씩 타고 올라가면서 결국 MongoRepositoryFactory의 다음 코드를 발견하게 됐다.

 

MongoRepositoryFactoryBean은 결국 MongoRepositoryFactory로 정의된 Bean이다.

 

말 그래도 MongoRepositoryFactoryBean은 MongoRepositoryFactory를 Bean으로 등록해 사용하는 것이다. 즉, 실제로 동작하는 것은 MongoRepositoryFactory로써 동작하며, 그렇기 때문에 MongoRepositoryFactoryBean을 사용하려는 시점에 이런 에러가 발생했다.

 

위 코드를 보면 MongoOperations는 반드시 존재해야 하며, MongoRepositoryFactoryBean에서 MongoRepositoryFactory를 생성하는 코드를 살펴봐도 동일하게 자신이 가지고 있던 MongoOperations를 파라미터로써 넘겨주는 모습을 볼 수 있다.

 

MongoRepositoryFactoryBean 내부 메소드인 getFactoryInstance. MongoOperation을 넘겨준다.

 

또한 MongoRepositoryFactoryBean의 생성자에는 분명 MongoOperations를 넣는 부분이 없지만, 멤버 변수에는 떡하니 존재했으며, 물론 이를 설정하는 함수 또한 존재했다.

 

MongoOperation이 @Nullable이 아니라 @Autowired이거나 그냥 DI 해줄 수 있게 설계되지 않은 이유는 아직 잘 모르겠다.

 

어쨌든 오류가 난 이유는 여기서 MongoOperations를 지정해주지 않았기 때문으로 보인다. 이를 해결해주기 위해선 MongoOperations가 무엇인지, 어떻게 설정하는지 알아보자.

 

MongoOperations


어떤 객체가 무엇인지 가장 빠르게 파악하는 방법은 직접 타고 들어가보는 것이다. 이번에는 MongoOperations 객체를 타고 들어가 보자.

MongoOperations의 javadoc

위 javadoc의  첫 문장을 보면 MongoTemplate에 의해서 구현된다고 적혀있다. 즉, MongoTemplate을 생성해서 위의 MongoRepositoryFactoryBean의 생성 시 함께 세팅해서 넣어줘야 하는 것이다. 그렇다면 MongoTemplate은 무엇일까?

 

MongoTemplate
MongoDB와 코드 사이의 인터페이스. MongoConverter 객체를 통해 Object - Document Mapping을 수행한다.

 

즉, ORM의 엔진과 같은 역할을 한다고 생각하면 쉽다. 어쨌든 MongoRepositoryFactoryBean에는 이런 MongoTemplate이 필수라는 점. 이를 주입해주기 위해서는 DBConfig 세팅을 다시 해야 했다..

 

원래 처음에는 간단히 MongoClient만 Configure 했었다. MongoDB에 연결하기 위해서는 MongoClientSetting을 이용해 MongoClient를 새로 생성해서 사용하면 됐기 때문이다. 다만, 이렇게 짜는 경우 MongoTemplate 등을 따로 수정 및 확장하기 어려워지며, DB 별로 같은 Template을 공유하거나 다른 것을 사용할 수도 있는데, 그런 상황에도 대처하기 위해서는 아래와 같이 DB Config를 변경하는 것이 좋다.

 

MongoDBConfig 파일. DBFactory를 따로 생성하고, Template과 DB를 연결한다.

 

이렇게 Configuration 코드를 세팅해서 등록해주면, 사용하고자 했던 MongoTemplate이 Bean으로 등록된다. 때문에 이제 MongoTemplate이 필요한 부분에 이를 추가해주면 된다.

 

MongoRepository를 동적으로 등록하는 부분. BeanFactory에서 MongoTemplate 빈을 가져와 사용한다.

 

주의할 점은 mongoTemplate을 등록하는 과정이다. MongoRepositoryFactoryBean에 들어가 보면 MongoOperations는 분명 'operations'라는 변수명으로 등록되어 있다. 그래서 당연히 이렇게 하는겠거니 하며 등록했었다.

 

	.addPropertyValue("operations", mongoTemplate);

 

하지만 계속해서 다음 에러가 뜨는 것이다.

 

org.springframework.beans.NotWritablePropertyException: Invalid property 'operations' of bean class [org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean]: Bean property 'operations' is not writable or has an invalid setter method. Does the parameter type of the setter match the return type of the getter?
	at org.springframework.beans.BeanWrapperImpl.createNotWritablePropertyException(BeanWrapperImpl.java:243) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:432) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:104) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:79) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1740) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.22.jar:5.3.22]
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1154) ~[spring-context-5.3.22.jar:5.3.22]

 

이름이 잘못됐다는 것은 알겠는데, 이름이 왜 잘못됐다는 것인지 몰랐다. 혹시 내가 변수 등록을 잘못하고 있는 것인가 싶어 소스를 뒤져봤지만 MongoRepository를 동적으로 등록하려는 사람은 나뿐인 것 같았다.

 

거의 포기하려던 시점에 GitHub에 "MongoRepositoryFactoryBean"과 "BeanDefinition"으로 검색해본 결과, 누군가 Spring-framework의 issue에 올린 것을 봤다.

 

그 사람은 아래와 같이 MongoRepositoryFactoryBean을 만들고 있었다.

 

  public static BeanDefinition getCustomerRepositoryBeanDefinition() {
    ResolvableType beanType = ResolvableType.forClassWithGenerics(MongoRepositoryFactoryBean.class, CustomerRepository.class, Object.class, Object.class);
    RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
    beanDefinition.setLazyInit(false);
    beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, "com.example.data.mongo.CustomerRepository");
    beanDefinition.getPropertyValues().addPropertyValue("queryLookupStrategyKey", QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND);
    beanDefinition.getPropertyValues().addPropertyValue("lazyInit", false);
    beanDefinition.getPropertyValues().addPropertyValue("namedQueries", new RuntimeBeanReference("mongo.named-queries#0"));
    beanDefinition.getPropertyValues().addPropertyValue("repositoryFragments", new RuntimeBeanReference("mongodb.CustomerRepository.fragments#0"));
    beanDefinition.getPropertyValues().addPropertyValue("mongoOperations", new RuntimeBeanReference("mongoTemplate"));
    beanDefinition.getPropertyValues().addPropertyValue("createIndexesForQueryMethods", false);
    beanDefinition.setAttribute("factoryBeanObjectType", "com.example.data.mongo.CustomerRepository");
    beanDefinition.setInstanceSupplier(InstanceSupplier.of(MongoRepositoryFactoryBean__BeanDefinitions::getCustomerRepositoryInstance));
    return beanDefinition;
  }

 

여기서 mongoOperations를 사용하는 부분이 보이는가. 그렇다. addPropertyValue에서 "operations"가 아니라 "mongoOperations"로 등록해줘야 하는 것이었다. 왜 그래야 되는지는 잘 모르겠지만, 어쨌든 이렇게 진행하니 오류가 사라지고 등록된 MongoRepositoryFactory는 정상적으로 자기가 가지고 있던 MongoRepository의 구현체를 반환했다.

 

그렇게 그 어떤 Repository 코드를 추가하지 않아도 자동으로 Entity를 분석해서 데이터 migration 하는 프로그램이 1차로 완성됐다.

 

이외의 부분은 거의 모두 JpaRepository를 동적으로 등록했던 부분과 같다. 따라서 아래 글을 다시 한번 참조한다.

2022.09.20 - [Spring/DB Migrator] - [DB Migrator] SpringBoot 동적으로 JpaRepository 생성 및 등록하기

 

[DB Migrator] SpringBoot 동적으로 JpaRepository 생성 및 등록하기

DB Migrator는 사용자의 편의를 최대한 봐주면서 Data Migration이 진행되는 것을 목표로 한다. 처음 인턴 했던 스타트업의 기술 블로그에도 작성했듯 목표로 했던 기능 중 첫 번째 편의성 기능은 Reposit

taler.tistory.com

 

 

결론


이번에는 BeanDefinitionBuidler와 MongoRepositoryFactoryBean을 이용해서 MongoRepository를 동적으로 생성 및 등록하는 과정에 대해서 알아봤다. 이렇게 JpaRepository도 동적으로 등록하고, MongoRepository도 동적으로 등록했으니 사용자는 자신이 옮기고자 하는 Entity를 생성해주기만 하면 자동으로 RepositoryFactory들이 이를 위한 Repository를 생성하고, 데이터를 날라줄 것이다.

 

사실 이렇게 Repository를 동적으로 생성하는 것까지를 프로젝트 기본 세팅으로 생각하고 있었는데, 취준 + 학업 병행 + 블로깅까지 하니 생각보다 시간이 더 걸린 것 같다.

 

앞으로 프로젝트의 로드맵은 아래와 같다.

 

  1. 작업 분산 로직 구현하기
  2. 테스트 코드 작성하기
  3. 멀티스레딩 리팩터링
  4. 생각했던 다른 두 가지 멀티스레딩 모델에 대해서 구현해보기
  5. 각각의 모델에 대해서 성능테스트 진행하기
  6. 런타임에 사용자가 LegacyDB와 MigrationDB를 지정할 수 있도록 변경하기
  7. AWS 등의 원격 DB에도 연동할 수 있도록 변경하기

등등...

 

생각보다 갈길이 멀다.

 

 

Reference


MongoRepository 쪽은 정말 하나도 소스가 없어서 정말 포기 직전에 찾은 소스:

https://github.com/spring-projects/spring-framework/issues/28809

 

AOT generated code should consider visibility of FactoryBean target type · Issue #28809 · spring-projects/spring-framework

A FactoryBean<T> using a package private target type T leads to compile errors in the generated code when the FactoryBean implementation is not in the same package. Given a package private re...

github.com

 

댓글