티스토리 뷰

반응형

서론

최근 부서에서 새로운 시스템 개발에 들어가며 MongoDB를 사용하게 됐습니다.

MongoDB를 선택하게 된 이유는 sub-query join이 많이 필요한 상황이 많기 때문입니다.

그러면서 가장 처음 보게 된 insert()와 save()로 저장하는 방법이 2개가 있습니다.

Spring Data Jpa는 save()를 통해 insert qeury를 실행하게 됩니다.

하지만 Spring Data MongoDB에서는 insert()라는 메서드도 제공이 되는걸 볼 수 있습니다.

따라서 이 글에서는 두 메서드의 차이점을 다뤄보고자 합니다.

 

MongoDB Repository

간단하게 MongoDB의 DAO를 생성해봅니다.

@Repository
public interface EntityRepository extends MongoRepository<Entity, ObjectId> {
}

Spring Data Jpa와 유사하게 생성이 가능하고

JpaRepository 대신 MongoRepository를 상속받아주면 됩니다.

또한 사용법도 비슷합니다.(사용법은 글의 목적과 맞지 않아 생략하겠습니다.)

 

그리고 아래처럼 insert나 save를 사용 가능한 걸 볼 수 있습니다.

@NoRepositoryBean
public interface MongoRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    <S extends T> S insert(S entity);

    <S extends T> List<S> insert(Iterable<S> entities);

    <S extends T> List<S> findAll(Example<S> example);

    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}

save()는 ListCrudRepository -> CrudRepository에서 확인할 수 있다.

이 부분에서 save()는 기존의 JpaRepository와 동일하다고 생각하면 된다.


insert()

SimpleMongoRepository

public <S extends T> S insert(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    return this.mongoOperations.insert(entity, this.entityInformation.getCollectionName());
}

MongoTemplate

protected Object insertDocument(String collectionName, Document document, Class<?> entityClass) {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug(String.format("Inserting Document containing fields: %s in collection: %s", document.keySet(), collectionName));
    }

    MappedDocument mappedDocument = this.queryOperations.createInsertContext(MappedDocument.of(document)).prepareId(entityClass);
    return this.execute(collectionName, (collection) -> {
        MongoAction mongoAction = new MongoAction(this.writeConcern, MongoActionOperation.INSERT, collectionName, entityClass, mappedDocument.getDocument(), (Document)null);
        WriteConcern writeConcernToUse = this.prepareWriteConcern(mongoAction);
        if (writeConcernToUse == null) {
            collection.insertOne(mappedDocument.getDocument());
        } else {
            collection.withWriteConcern(writeConcernToUse).insertOne(mappedDocument.getDocument());
        }

        return this.operations.forEntity(mappedDocument.getDocument()).getId();
    });
}

//...

public <T> T execute(String collectionName, CollectionCallback<T> callback) {
    Assert.notNull(collectionName, "CollectionName must not be null");
    Assert.notNull(callback, "CollectionCallback must not be null");

    try {
        MongoCollection<Document> collection = this.getAndPrepareCollection(this.doGetDatabase(), collectionName);
        return callback.doInCollection(collection);
    } catch (RuntimeException var4) {
        throw potentiallyConvertRuntimeException(var4, this.exceptionTranslator);
    }
}

insert()를 타고 들어가다 보면 insertDocument를 찾을 수 있고

여기에서 excute를 통해 넣는 쿼리를 실행시킵니다.

그리고 excute에서 catch문에 잡히는 potentiallyConvertRuntimeException을 볼 수 있습니다.

위를 따라가다 보면 MongoExceptionTranslator.class를 볼 수 있습니다.

MongoTemplate을 사용하는 예외를 번역해주는 클래스라고 생각하면 됩니다.


위 코드의 이해를 돕고 싶다면 Template Callback에 대해 공부하면 도움이 됩니다.


마지막으로 해당 class의 translateExceptoinIfPossible에서 아래 코드를 확인 가능합니다.

if (MongoDbErrorCodes.isDuplicateKeyCode(code)) {
    return new DuplicateKeyException(ex.getMessage(), ex);
}

더 코드를 들어가보진 않겠지만 이름에서도 유추가 가능합니다.

중복된 키값 예외. 즉, 키 값이 중복되면 터트려주는 예외가 발생합니다.

 

또한, excute() callback 함수를 보면 별도의 중복 키값에 대한 처리가 없음을 볼 수 있습니다.

반면 saveDocument(), save() 함수를 실행할 때의 함수를 보면 중복 키값에 대한 처리를 확인 할 수 있습니다.

protected Object saveDocument(String collectionName, Document dbDoc, Class<?> entityClass) {
    if (LOGGER.isDebugEnabled()) {
        LOGGER.debug(String.format("Saving Document containing fields: %s", dbDoc.keySet()));
    }

    return this.execute(collectionName, (collection) -> {
        MongoAction mongoAction = new MongoAction(this.writeConcern, MongoActionOperation.SAVE, collectionName, entityClass, dbDoc, (Document)null);
        WriteConcern writeConcernToUse = this.prepareWriteConcern(mongoAction);
        MappedDocument mapped = MappedDocument.of(dbDoc);
        MongoCollection<Document> collectionToUse = writeConcernToUse == null ? collection : collection.withWriteConcern(writeConcernToUse);
        if (!mapped.hasId()) {
            mapped = this.queryOperations.createInsertContext(mapped).prepareId((MongoPersistentEntity)this.mappingContext.getPersistentEntity(entityClass));
            collectionToUse.insertOne(mapped.getDocument());
        } else {
            MongoPersistentEntity<?> entity = (MongoPersistentEntity)this.mappingContext.getPersistentEntity(entityClass);
            QueryOperations.UpdateContext updateContext = this.queryOperations.replaceSingleContext(mapped, true);
            Document replacement = updateContext.getMappedUpdate(entity);
            Document filter = updateContext.getMappedQuery(entity);
            if (updateContext.requiresShardKey(filter, entity)) {
                if (entity.getShardKey().isImmutable()) {
                    filter = updateContext.applyShardKey(entity, filter, (Document)null);
                } else {
                    filter = updateContext.applyShardKey(entity, filter, (Document)collection.find(filter, Document.class).projection(updateContext.getMappedShardKey(entity)).first());
                }
            }

            collectionToUse.replaceOne(filter, replacement, (new ReplaceOptions()).upsert(true));
        }

        return mapped.getId();
    });
}

이를 확인해봤을 때, insert()와 save()의 차이는 명확하게 

중복 키값에 대한 처리를 예외를 터트리냐 업데이트 시켜주냐의 차이점을 확인 할 수 있습니다.

 

설명이 길어졌지만 아래로 명확하게 구분이 됩니다.

insert(): 중복된 키값을 넣을때 예외를 터뜨려준다.

save(): 중복된 키값을 넣을때 업데이트를 해준다.

 

JPA와 동일하게 여기서 겪을 수 있는 에러들은 업데이트를 하기 위한 Document가

새 Document로 인식되어 DuplicateKeyException이 나올 수 있다는 것이다.

 

SimpleMogoRepository(MongoRepository의 구현체)에 save를 보면 

isNew()로 새로운 Document인지 판단하고 아래 isNew의 코드를 참고하면 

위에서 제기했던 문제에 도움이 될 것 이다.

public boolean isNew(T entity) {
    ID id = this.getId(entity);
    Class<ID> idType = this.getIdType();
    if (!idType.isPrimitive()) {
        return id == null;
    } else if (id instanceof Number) {
        return ((Number)id).longValue() == 0L;
    } else {
        throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
    }
}

 

결론

글이 길어졌지만 결론은 간단합니다. insert는 중복된 키에 예외, save는 업데이트입니다. 사실 JPA와 동일하지만 Spring Data MongoDB를 새로하며 분명 Spring Data JPA와는 다른게 있을 것이라고 생각했습니다. 예를 들어 Dirty check나 transaction을 하려면 MongoDB가 replica set으로 구성돼야 한다는 것처럼. 이번에 코드 레벨까지 살펴보며, Spring Data MongoDB에 대한 공부도 많이 되었던 같아서 좋았습니다.

728x90
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함
250x250