[MongoDB] MongoTemplate과 MongoRepository를 함께 사용할 순 없는 걸까?

rin·2020년 6월 3일
0
post-thumbnail

[MongoDB] MongoTemplate vs. MongoRepository에 나와있는 레파지토리 구성을 따라해보았다. 결과적으론 실패했음 😑

그 구조를 어디서 많이 봤다했더니 QueryDsl을 JpaRepository에 확장할 때 사용한 구조와 동일하다.
상속 관계를 따져보면 아래 그림과 같은데, UserRepository는 CustomUserRepository 인터페이스를 확장하는 것만으로도 CustomUserRepository인터페이스에 정의되고 UserRepositoryImpl에 구현된 코드를 사용할 수 있게 된다.

MongoTemplate의 특정 작업을 위한 로직은 꽤 복잡하기 때문에 이런식으로 숨겨두는 것이 좋을 것 같아 시도해보았다.

내 계획은 MongoRepository로는 메소드 명명 규칙으로 간편하게 작성 가능한 최소한의 CRD를 사용하고, MongoTemplate으로는 복잡한 구문이나 Update를 하는 것이었다.
처음에 내가 작성한 이름은 updateSiblingOneById였는데, Repository는 update라는 명명을 사용하지 않으므로 (save키워드로 insert와 update 모두 수행) 오류가 나는 것이었다.

java.lang.IllegalStateException: Failed to load ApplicationContext

	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:123)
	...
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'petRefactoringRepository': Invocation of init method failed; nested exception is org.springframework.data.mapping.PropertyReferenceException: No property setList found for type PetEntity!
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1796)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:595)
	...
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property setList found for type PetEntity!
	at org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:94)

QueryDSL에서는 JPA 메소드 명명 규칙에 따라서 커스텀 메소드를 작성하는데, 명명 규칙을 따르지 않으면 두 인터페이스를 모두 상속받는 과정에서 오류가 발생한다.

관련 내용 : https://stackoverflow.com/questions/19583540/spring-data-jpa-no-property-found-for-type-exception

테스트 해 볼 겸 이름을 findById라고 고쳤더니 오류가 바뀌었다.

org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate void using constructor NO_CONSTRUCTOR with arguments 

	at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:67)
	at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:84)
    ...
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [void]: No default constructor found; nested exception is java.lang.NoSuchMethodException: void.<init>()
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:146)
	at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:64)
	... 76 more
Caused by: java.lang.NoSuchMethodException: void.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3349)
	at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2553)
	at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:139)
	... 77 more 

기본 생성자를 찾을 수 없다고 하는데 런타임오류가 해당 메소드를 직접 사용하는 순간에 생겼다. 처음에는 PetRepositoryImplCustom의 구현체인 PetRepositoryImpl가 문제라고 생각했다.

아래 이미지처럼 PetRepositoryImpl 클래스는 MongoTemplate을 자동 주입 받아야하기 때문에 기본생성자를 만들지 않았기 때문이다.
하지만 void.<init>()이라는 문구가 걸려 void findById(ObjectId id, List<T> siblings)를 명명규칙에 알맞게 반환값을 void가 아닌 제네릭타입 T로 변경해주었다.

다시 테스트 코드를 돌려보니 계속 에러가 났던 구간은 통과하는데 NullPointerException이 새롭게 발생하는 것이었다. 🤦🏻
디버깅해보니 이름은 findById지만 내부 로직은 업데이트 쿼리였던 PetRepositoryImpl 클래스의 메소드를 타지않고 말그대로 "find By Id"를 수행하고 있었다.

update 쿼리가 따로 제공되지 않는 Repository 인터페이스를 사용하려는 것이 문제였을까 🤔

이렇게는 사용이 불가할 것 같아 Template을 사용하는 레포지토리는 분리하였더니, 이번엔 프록시 문제가 말썽이다. 😔

🔎 PetRepositoryImplCustom 인터페이스

public interface PetRepositoryImplCustom {
    void updateSiblingsById(ObjectId id, List<PetEntity> siblings);
}

🔎PetRepositoryImpl 클래스

@Repository
public class PetRepositoryImpl implements PetRepositoryImplCustom {

    private MongoOperations operations;

    final static String SIBLING = "sibling";

    @Autowired
    public PetRepositoryImpl(MongoTemplate template) {
        Assert.notNull(template, "MongoTemplate must not be null!");
        this.operations = template;
    }

    @Override
    public void updateSiblingsById(ObjectId id, List<PetEntity> siblings) {
        Query query = new Query(Criteria.where("_id").is(id));
        operations.updateFirst(query,
                Update.update(SIBLING, siblings),
                PetEntity.class
        );
    }
}

Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'petRepositoryImpl' is expected to be of type 'com.freeboard03.domain.pet.PetRepositoryImpl' but was actually of type 'com.sun.proxy.$Proxy72'

PetRepositoryImpl 클래스에 명시되어있는 @Repository 어노테이션을 제거하면서 문제는 해결됐는데 빈으로 등록되지 않은 클래스가 테스트 코드에서 자동주입이 되는지 잘 모르겠다. spring-aop를 사용하지 않고도 proxy가 자동으로 생성(java dynamic proxy?)되는 것인지도 의문이다. 🤔 찾아서 정리하도록 하겠음.

아무튼 업데이트 쿼리만 PetRepositoryImpl 클래스를 사용한 방식으로 작성한 테스트 코드가 드디어 성공 할 수 있었다.

++++추가
petRepositoryImpl가 PetRepositoryImplCustom의 구현체가 아닌 일반 클래스일 경우 @Repository를 붙여줘야만 자동 주입이 가능하다.

대체 인터페이스가 하고 있는 일이 뭔지 모르겠다. 프록시와 연관이 있는걸까? 그럼 프록시와 빈은 어떤 관계를 갖는걸까.. 공부하고 정리해봐야겠다.

profile
🌱 😈💻 🌱

0개의 댓글