[프로젝트3] 2. MongoRepository 이용하기

rin·2020년 5월 29일
5
post-thumbnail

목표
1. MongoTemplate과 Repository의 차이점에 대해 이해한다.
2. 커스텀 Repository 인터페이스를 생성하고 이를 이용해 CRUD를 테스트한다.


이전글에서 MongoTemplate을 사용할 때 선언부도 MongoTemplate을 사용했다.
MongoTemplate은 MongoOperations, ApplicationContextAware, IndexOperationsProvider 인터페이스의 구현체이기 때문에 다음처럼 쓰는게 맞다고 한다,

MongoOperations mongoOperations = new MongoTemplate();

MongoRepository

public interface MongoRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> 를 상속받는 커스텀 인터페이스를 생성한다. 이 인터페이스는 메소드 네임 기반 쿼리를 지원한다.

JPA에서 이를 많이 사용해보았기 때문에 익숙한 형태이긴하다. 🤔

MongoTemplate을 소개하는 글을 보면 기존의 MongoDB 네이티브 쿼리를 이용해 개발을 해온 개발자들에게 익숙한 메소드를 제공한다고 했던게 이해가 간다.
MongoRepository에서 사용하는 메소드 네임은 기존의 RDB에서 사용하는 Query 형태에 가깝고, MongoTemplate에서 사용하는 메소드는 실제로 MongoDB client를 이용할 때 사용하는 메소드와 (거의)유사하다.


ref. What's the difference between Spring Data's MongoTemplate and MongoRepository?


아무튼, MongoRepository를 이용하여 CRUD를 테스트해보겠다.

PetRepository

domain/pet 하위에 PetRepository 인터페이스를 생성한다.

@Repository
public interface PetRepository extends MongoRepository<PetEntity, ObjectId> {
}

PetRepositoryTest

PetTest를 복사하여 PetRepositoryTest를 생성 한 뒤 아래처럼 MongoTemplateMongoOperations 대신 PetRepository를 추가해준다.

PetTestPetRepositoryTest

insertTest 메소드를 다음처럼 변경한 뒤 테스트해보았다.
테스트는 성공하였으니, 실제 DB에는 어떻게 저장되었는지 확인해보자.
MongoTemplate을 사용했을 때와 동일한 형식으로 잘 저장되었음을 확인 할 수 있다. 그럼 나머지 테스트도 Repository 형식에 맞춰 수정해보겠다.

CRUD 테스트

Read

🔎 MongoTemplate
🔎 MongoRepository

변경된 로직의 큰 공통점은 Query, Criteria가 사라졌다는 것이고 Repository에 추가적인 메소드 작성이 필요했다.

PetRepository에 메소드를 추가해주자.

List<PetEntity> findAllByKind(String kind);

Update

MongoRepository의 save 메소드는 _id 값을 비교하여

  1. save의 파라미터로 넘어온 도큐먼트의 _id가 컬렉션에 존재할 경우 update를 하고,
  2. 존재하지 않는 경우 insert를 한다.

따라서 MongoRepository를 이용해 update를 수행하기 위해선 수정하려는 도큐먼트의 id 값을 가지고 있는 Entity를 save해주어야한다.

예를 들어 아래와 같은 도큐먼트가 저장되어 있다고 생각해보자.

{_id: 1, name: '바둑이', age: 1}

이 때, name 필드의 값을 멍멍이로 바꾸고 싶으면

id = 1, name = '멍멍이', age = 1

인 Entity를 파라미터로 넘겨주면된다.

주의해야할 점은 바뀌는 값이 name 필드 뿐이니, Entity의 age 변수값을 설정하지 않아

id = 1, name = '멍멍이', age = null

인 Entity가 전달되면 age 필드는 사라진다는 점이다.

🔎 MongoTemplate
🔎 MongoRepository

Persistence Context에서 관리가 되고 있는지 테스트 하기 위해서 아래 코드를 추가적으로 짜보았다.

결과는 관리안됨🙅🏻 이었다.

당연히 실제 데이터베이스에는 업데이트 되어있다. 이런식으로 업데이트 하는 것은 여러 문제가 있어보이는데,

  1. setId() 메소드를 이용해 Id 값을 (변경하려는 도큐먼트의 갱신된 내용을 포함한 Entity에) 강제로 주입해주어야함.
  2. 터미널 출력을 보면 도큐먼트의 kind 필드가 사라졌다. (예시처럼 이전 값을 복제하는 작업을 잊는 경우가 많을 것 같다. 🤔)

MongoRepository를 이용하여 작성한 테스트 코드를 보면 pet.update(updatedPet)이라는 로직이 있다. 해당 메소드는 PetEntity 내부의 메소드이며 updatedPet이 변경된 부분(e.g. name, age 필드)만 가지고 있다는 가정하에 다음처럼 작성하였다.

    public void update(PetEntity updatedPet){
        Optional.ofNullable(updatedPet.getKind()).ifPresent(none -> this.kind = updatedPet.getKind());
        Optional.ofNullable(updatedPet.getName()).ifPresent(none -> this.name = updatedPet.getName());
        Optional.ofNullable(updatedPet.getAge()).ifPresent(none -> this.age = updatedPet.getAge());
        Optional.ofNullable(updatedPet.getSibling()).ifPresent(none -> this.sibling = updatedPet.getSibling());
    }

Optional을 이용해 null이 아닌 값에 대해서만 재할당해준다.

Delete

🔎 MongoTemplate
🔎 MongoRepository 특별할 것 없는 코드다. PetRepository에 다음 메소드를 추가해주자.

List<PetEntity> deleteAllByKind(String kind);

[exception] findDynamicProjection 메소드를 찾을 수 없다

그리고 아마 나와같이 2.1.5 릴리즈 버전의 spring-data-mongodb를 사용했으면 위 쿼리 메소드를 실행하려고 할 때 exception이 발생할 것이다. 🤦🏻

Receiver class org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor does not define or inherit an implementation of the resolved method 'abstract java.lang.Class findDynamicProjection()' of interface org.springframework.data.repository.query.ParameterAccessor.

해석하자면 ConvertingParameterAccessorParameterAccessor 인터페이스의 findDynamicProjection 메소드가 정의되어있지 않단다.

해당 클래스로 가서 메소드를 찾아보니 존재하지 않았고, 이 메소드를 정의해 두었다는 인터페이스도 찾을 수 없었다. 🤔
구글에 해당 인터페이스를 검색했더니 2.2.5 릴리즈 버전에는 ParameterAccessor 인터페이스가 존재한단 것을 알아냈고 2.1.5 버전에서 2.2.5 버전으로 업데이트 시켜주었더니 문제가 해결되었다.

Create and Update

가장 복잡했던 형제관계의 pet들이 자신을 제외한 나머지 형제의 고유 id를 포함한 정보를 임베디드 도큐먼트로 저장할 수 있도록 하는 테스트 코드이다.

🔎 MongoTemplate
🔎 MongoRepository
PetRepository에 다음 메소드를 추가해주자.

List<PetEntity> findAllByIdIn(List<ObjectId> ids);

[exception] 순환 참조로 인한 StackOverFlow

🤦🏻 그리고 저 코드로 돌렸을 때 StackOverFlow가 발생하여 계속 삽질함..📌
updateSibling에서 for문의 두번째 인덱스부터 무한 루프가 발생하는데 updatePet에 모아진 형제 pet의 idnull로 설정하니 NullPointerException이 발생하는 것을 보고 뭐가 문제였는지 깨달았다.

nowPet은 파라미터로 들어온 List<PetEntity> sibling에 포함된 객체인데 for 문에서 nowPet.update(updatePet)를 수행하는 것은 파라미터 sibling 내부의 엔티티를 변경하는 작업이다.

두번째 인덱스부터 상호 참조하는 꼴이 될 수 밖에 없는 것.

결국 파란색 선처럼 pet1--pet1의 멤버변수 sibling<->pet2--pet2의 멤버변수 sibling이 순환참조하게 된다.

아래처럼 updateSibling 메소드 내 로직을 수정하니 잘 실행된다.

  1. sibling 멤버 변수를 업데이트 하고자 하는 현재 pet인 nowPetupdatedPet으로 복제한다. (새로운 객체)
  2. filter를 이용하여 savedPets을 순환하며 나와 아이디가 다른, 즉 형제인 Pet의 Entity를 nowSiblings에 저장한다. (이미 존재하는 객체)
  3. updatedPetnowSibling을 set 해준다. (새로운 객체에 이미 존재하는 객체 넣기)
  4. updatedPet을 파라미터로 save 메소드를 수행한다. → nowPetid도 복제되었으므로 update가 수행된다.

위 과정을 거치면서 savedPets에 포함된 객체인 nowPetJava 코드내에서 전혀 변경되지 않는다. 실제 데이터베이스에서는 변경되나 이것이 객체에 반영되지 않기 때문에 가능한 것.

전체 테스트 코드는 아래와 같다.
🔎 PetRepositoryTest

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/applicationContext.xml"})
@Transactional
@Rollback
public class PetRepositoryTest {

    @Autowired
    PetRepository petRepository;

    @Test
    public void insertTest() {
        PetEntity pet = PetEntity.builder().kind("CAT").name("나비").age(2).build();
        petRepository.save(pet);

        PetEntity findPet = petRepository.findById(pet.getId()).get();

        assertThat(pet.getId(), equalTo(findPet.getId()));
        assertThat(pet.getName(), equalTo(findPet.getName()));
        assertThat(pet.getKind(), equalTo(findPet.getKind()));
        assertThat(pet.getAge(), equalTo(findPet.getAge()));
    }

    @Test
    public void findTest() {
        final String KIND = "CAT in findTest(" + randomString() + ")";
        final int INSERT_SIZE = 10;
        insertFindAllTestData(KIND, INSERT_SIZE);

        List<PetEntity> findPets = petRepository.findAllByKind(KIND);

        assertThat(findPets.size(), equalTo(INSERT_SIZE));
    }

    void insertFindAllTestData(String KIND, int INSERT_SIZE) {
        for (int i = 0; i < INSERT_SIZE; ++i) {
            PetEntity pet = PetEntity.builder().age(2).kind(KIND).name("Test Name").build();
            petRepository.save(pet);
        }
    }

    @Test
    @DisplayName("pet을 저장한 뒤 해당 도큐먼트의 name을 변경하고 age를 5 더한다.")
    public void updateTest() {
        PetEntity pet = PetEntity.builder().kind("CAT").name("나비").age(0).build();
        petRepository.save(pet);

        String updatedName = "노랑이";
        int increaseAge = 5;
        PetEntity updatedPet = PetEntity.builder().age(pet.getAge() + increaseAge).name(updatedName).kind(pet.getKind()).build();

        pet.update(updatedPet);

        petRepository.save(pet);

        PetEntity findPet = petRepository.findById(pet.getId()).get();
        assertThat(findPet.getName(), equalTo(updatedName));
        assertThat(findPet.getAge(), equalTo(increaseAge));
    }

    @Test
    @DisplayName("pet을 저장한 뒤 해당 도큐먼트의 name을 변경하고 inc를 이용해 age를 0으로 만든다.")
    public void updateTest2() {
        int age = 2;
        PetEntity pet = PetEntity.builder().kind("CAT").name("나비").age(age).build();
        petRepository.save(pet);

        String updatedName = "노랑이";
        int decreaseAge = -1 * age;
        PetEntity updatedPet = PetEntity.builder().age(pet.getAge() + decreaseAge).name(updatedName).build();

        pet.update(updatedPet);

        petRepository.save(pet);

        PetEntity findPet = petRepository.findById(pet.getId()).get();
        assertThat(findPet.getName(), equalTo(updatedName));
        assertThat(findPet.getAge(), equalTo(0));
        assertThat(findPet.getKind(), equalTo(pet.getKind()));
    }

    @Test
    @DisplayName("Persistence Context 테스트")
    @Disabled
    public void persistenceTest() {
        int age = 2;
        PetEntity pet = PetEntity.builder().kind("CAT").name("나비").age(age).build();
        petRepository.save(pet);

        String updatedName = "노랑이";
        int decreaseAge = -1 * age;
        PetEntity updatedPet = PetEntity.builder().age(pet.getAge() + decreaseAge).name(updatedName).build();
        updatedPet.setId(pet.getId());

        // pet과 같은 id를 가진 updatedPet을 save = update
        petRepository.save(updatedPet);

        // update 내용이 pet에 반영되었는지 확인
        assertThat(pet.getName(), equalTo(updatedName));
        assertThat(pet.getAge(), equalTo(0));
    }


    @Test
    @DisplayName("pet을 저장한 뒤 remove를 이용해 모두 삭제한다.")
    public void deleteTest() {
        final String KIND = "CAT in findTest(" + randomString() + ")";
        final int INSERT_SIZE = 10;
        insertFindAllTestData(KIND, INSERT_SIZE);

        List<PetEntity> deletedPets = petRepository.deleteAllByKind(KIND);

        assertThat(deletedPets.size(), equalTo(INSERT_SIZE));
    }

    @Test
    @DisplayName("pet 컬렉션을 멤버변수로 가지고 있는 pet 객체를 insert한다.")
    public void insertTest2() {
        final int SIBLING_SIZE = 5;
        PetEntity pet = PetEntity.builder().kind("DOG").age(7).name("바둑이").sibling(getPets(SIBLING_SIZE)).build();
        petRepository.save(pet);

        PetEntity findPet = petRepository.findById(pet.getId()).get();
        assertThat(pet.getSibling().size(), equalTo(findPet.getSibling().size()));
    }

    private List<PetEntity> getPets(int size) {
        List<PetEntity> petEntities = new ArrayList<>();
        for (int i = 1; i <= size; ++i) {
            PetEntity pet = PetEntity.builder().name("sibling" + i).age(7).kind("DOG").build();
            petEntities.add(pet);
        }
        return petEntities;
    }

    @Test
    @DisplayName("pet 컬렉션을 멤버변수로 가지고 있는 pet 객체와 멤버 변수의 M:M 관계를 유자하여 insert한다.")
    public void insertTest3() {
        final int SIBLING_SIZE = 3;
        List<PetEntity> sibling = getPets(SIBLING_SIZE);
        petRepository.saveAll(sibling);

        updateSibling(SIBLING_SIZE, sibling);

        List<PetEntity> updatedSibling = getUpdatedPetEntities(sibling);

        for (int i = 0; i < SIBLING_SIZE; ++i) {
            List<PetEntity> thisSibling = updatedSibling.get(i).getSibling();
            List<ObjectId> thisSiblingIds = thisSibling.stream().map(pet -> pet.getId()).collect(Collectors.toList());
            assertThat(thisSiblingIds, not(contains(updatedSibling.get(i).getId())));
        }
    }

    void updateSibling(int SIBLING_SIZE, List<PetEntity> savedPets) {
        for (int i = 0; i < SIBLING_SIZE; ++i) {
            PetEntity nowPet = savedPets.get(i);
            PetEntity updatedPet = PetEntity.duplicate(nowPet);

            List<PetEntity> nowSiblings = savedPets.stream().filter(pet -> pet.getId().equals(nowPet.getId()) == false).collect(Collectors.toList());
            updatedPet.setSibling(nowSiblings);

            petRepository.save(updatedPet);
        }
    }

    List<PetEntity> getUpdatedPetEntities(List<PetEntity> sibling) {
        return petRepository.findAllByIdIn(sibling.stream().map(pet -> pet.getId()).collect(Collectors.toList()));
    }

    private String randomString() {
        String id = "";
        for (int i = 0; i < 10; i++) {
            double dValue = Math.random();
            if (i % 2 == 0) {
                id += (char) ((dValue * 26) + 65);   // 대문자
                continue;
            }
            id += (char) ((dValue * 26) + 97); // 소문자
        }
        return id;
    }
}

static 메소드인 duplicate가 추가된 PetEntity 클래스는 다음과 같다.
🔎 PetEntity

@Document(collection = "pets")
@Getter
public class PetEntity {

    @Id
    @Setter
    private ObjectId id;

    private String kind;

    private String name;

    private int age;

    @Setter
    private List<PetEntity> sibling;

    @Builder
    public PetEntity(String kind, String name, int age, List<PetEntity> sibling) {
        this.kind = kind;
        this.name = name;
        this.age = age;
        this.sibling = sibling;
    }

    public void update(PetEntity updatedPet) {
        Optional.ofNullable(updatedPet.getKind()).ifPresent(none -> this.kind = updatedPet.getKind());
        Optional.ofNullable(updatedPet.getName()).ifPresent(none -> this.name = updatedPet.getName());
        Optional.ofNullable(updatedPet.getAge()).ifPresent(none -> this.age = updatedPet.getAge());
        Optional.ofNullable(updatedPet.getSibling()).ifPresent(none -> this.sibling = updatedPet.getSibling());
    }

    public static PetEntity duplicate(PetEntity petEntity) {
        PetEntity duplicatedEntity = new PetEntity(petEntity.getKind(), petEntity.getName(), petEntity.getAge(), petEntity.getSibling());
        duplicatedEntity.setId(petEntity.getId());
        return duplicatedEntity;
    }

}

전체 코드는 github에서 확인 할 수 있습니다.

profile
🌱 😈💻 🌱

0개의 댓글