개요
- 1,2 편에서는 MongoDB Aggregation에 대해서 알아보고, Spring Data MongoDB가 제공해 주는 기능도 알아보았다.
- 나는 실무에서 실제로 QueryDsl-JPA로 구현된 로직을 Spring Data MongoDB로 변환하는 표준을 만들었다.
- 나의 목표는 MongoDB Aggregation 쿼리 문법를 알지 못하는 개발자도 QueryDsl로 짜인 모든 로직을 손쉽게 MongoDB Aggregation으로 바꿀 수 있는 방법을 제시하는 것이었다.
- 하지만, 2편에서 소개했듯이 MongoDB Aggregation으로 코드를 짜면, 코드가 너무 길어지고, 복잡했다.
- 나는 위의 문제를 해결하기 위해서 AggregationBuiler 클래스를 구현했고, 모든 QueryDsl 로직을 MongoDB Aggregation으로 변환할 수 있었다.
- 그 덕에 이제는 MongoDB만을 사용하는 솔루션에서도 우리의 프레임워크를 사용할 수 있게 되었다.
- 물론 실무에서 사용된 코드는 아래의 내용보다 고려할 점이 많지만, 구현 원리를 간단하게나마 정리해 보려고 한다.
Spring Data MongoDB Aggregation의 문제점
- 만약 순수하게 Spring Data MongoDB에서 제공하는 Aggregation 메서드만으로 구현하면 아래와 같다.
- 2편에서 언급한 대로 MongoDB Aggregation을 Java 코드로 구현하면 위와 같이 복잡하고, 실수할 여지가 많다.
- 그리고 쿼리를 읽는 것이 직관적이지 않다.
- 또한, MongoDB aggregation에 대한 지식이 없으면, 코드를 짜기 어려울 것이다.
- 나의 목표는 MongoDB Aggregation에 대해서 몰라도 다른 개발자 분들이 JPA QueryDsl 코드를 MongoDB Aggregation 코드로 전환하는 것이었다.
- 이제부터, 위의 코드를 AggregationBuiler를 통해서 좀 더 쉽게 전환해 보자.
AggregationBuilder
- 먼저, 두 가지 빌더 클래스를 만들었다. 클래스 구조는 아래와 같다.
- AggregationBuilder는 operationList를 가지고 있다.
- 아래 보이는 leftJoin(), innerJoin(), project(), match(), sort()가 실행되면서, operationList에 각각의 AggregationOperation을 담는다.
- 그리고 최종적으로 aggregate() 메서드를 통해서 쿼리가 실행된다.
- 이제부터 각각의 메소드를 좀 더 자세히 살펴보자
leftJoin(String from, DocumentBuiler leftBuilder, DocumentBuiler onBuilder)
public AggregationBuilder leftJoin(String from, DocumentBuilder letBuilder, DocumentBuilder onBuilder) {
Document let = letBuilder.getDocument();
Document letByOn = onBuilder.getDocument();
letByOn.forEach(let::append);
List<Document> match = onBuilder.getDocumentList();
lookup(from, let, match);
unwind(from, true);
return this;
}
public void lookup(String from, Document let, List<Document> match) {
AggregationOperation lookupOperation = makeLookupOperation(from, let, match);
this.operationList.add(lookupOperation);
}
public AggregationOperation makeLookupOperation(String from, Document let, List<Document> match) {
return context -> new Document(
"$lookup",
new Document("from", from)
.append("let", let)
.append("pipeline", Arrays.<Object> asList(
new Document("$match",
new Document("$expr",
new Document("$and", match)
)
)
))
.append("as", from)
);
}
public void unwind(String field, boolean preserveNullAndEmptyArrays) {
UnwindOperation unwindOperation = Aggregation.unwind(field, preserveNullAndEmptyArrays);
this.operationList.add(unwindOperation);
}
- leftJoin() 메소드를 보면 내부적으로 lookup(), unwind() 메소드를 가지고 있다.
- 2편에서 언급했듯이 RDB의 left join은 MongoDB Aggregation의 $lookup + $unwind을 한 것과 유사하다.
- 그렇기 때문에 leftjoin() 메서드 안에 lookup(), unwind()를 넣었고, 각각의 메서드에서 lookupOperation, unwindOperation를 만들어서 operationList에 추가하는 것을 확인할 수 있다.
- 그리고, letBuilder와 onBuilder가 있는데, 이건 후반부에 정리하도록 하자.
innerJoin
public AggregationBuilder innerJoin(String from, DocumentBuilder letBuilder, DocumentBuilder onBuilder) {
Document let = letBuilder.getDocument();
Document letByOn = onBuilder.getDocument();
letByOn.forEach(let::append);
List<Document> match = onBuilder.getDocumentList();
lookup(from, let, match);
unwind(from, false);
return this;
}
- innerJoin은 leftJoin과 다른 부분은 동일하고, unwind 메서드의 preserveNullAndEmptyArrays만 false로 설정하면 된다.
preserveNullAndEmptyArrays는 단어 그래도 null인 데이터를 표시할지 말지를 선택하는 옵션이다.
project(ProjectionOperation projectionOperatioin)
public AggregationBuilder project(ProjectionOperation projectionOperation) {
this.operationList.add(projectionOperation);
return this;
}
- project() 메서드는 projectionOperation을 파라미터로 받아서 operationList에 추가한다.
- projectionOperation는 뒤에서 최종 결과 코드를 보면 알 수 있다.
match(List creteriaList)
public AggregationBuilder match(List<Criteria> criteriaList) {
MatchOperation matchOperation = Aggregation.match(new Criteria()
.andOperator(
criteriaList.toArray(new Criteria[0])
)
);
operationList.add(matchOperation);
return this;
}
- match() 메소드는 criteriaList를 받아서 Aggregation.match() 메소의 파라미터로 넘겨준다.
- 그 결과물로 matchOperation를 받아서 operationList에 추가한다.
sort(SortOperation sortOperation)
public AggregationBuilder sort(SortOperation sortOperation) {
operationList.add(sortOperation);
return this;
}
- sort() 메소드는 sortOperation을 파라미터로 받아서 operationList에 추가한다.
- sortOperation는 뒤에서 최종 결과 코드를 보면 알 수 있다.
aggregate(String collectionName, Clazz entityClazz)
public <T> List<T> aggregate(String collectionName, Class<T> entityClass) {
List<AggregationOperation> operations = new ArrayList<>();
ProjectionOperation projectionOperation = null;
// ProjectionOperation 맨 뒤로 이동
for (AggregationOperation operation : operationList) {
if (operation instanceof ProjectionOperation) projectionOperation = (ProjectionOperation)operation;
else operations.add(operation);
}
operations.add(projectionOperation);
Aggregation aggregation = Aggregation.newAggregation(operations.toArray(new AggregationOperation[0]));
log.debug("{}", aggregation);
return mongoTemplate.aggregate(aggregation, collectionName, entityClass)
.getMappedResults();
}
- ProjectionOperation 은 가장 마지막에 실행되어야 하기 때문에 맨 뒤로 보내준다.
- 그다음, operationList에 담긴 여러 Operation을 Aggregation.newAggregation() 메서드를 통해서 실행시킨다.
aggregate(Pageable pageable, String collectionName, Clazz entityClazz)
public <T> PageImpl<T> aggregate(Pageable pageable, String collectionName, Class<T> entityClass) {
List<T> result = aggregate(collectionName, entityClass);
final int start = (int) pageable.getOffset();
final int end = Math.min((start + pageable.getPageSize()), result.size());
return new PageImpl<>(result.subList(start, end), pageable, result.size());
}
- Paging 처리가 하고 싶다면, 이전에 aggregate() 메소드를 실행시킨 뒤, PageImpl <>로 감싸주면 된다.
변환 결과
- 위의 과정대로 AggregationBuilder 클래스를 이용하면, 아래와 같이 변환할 수 있다.
DocumentBuilder
- 위에서 lookupOperation을 설명할 때, DocumentBuilder를 설명한다고 언급했다.
- 나는 아래와 같은 lookupOperation을 구성하기 위해서 DocumentBuilder를 구현했다.
- DocumentBuilder를 사용하면 아래와 같이 구현이 가능하다.
- DocumentBuilder를 사용하면 RDB에서 여러 개의 중첩된 join 도 간단하게 구현이 가능하다.
결론
- JPA QueryDsl와 Spring Data MongoDB Aggregation를 비교해 보면 아래와 같다.
- 실무에서는 이 구조보다 더 복잡하게 구현했지만, 기본 동작과정은 위와 같다.
- 위의 방식을 정리해서 가이드를 만들어서 배포했고, 팀원분들은 MongoDB Aggregation에 대한 개념이 없이도, QueryDsl 로직을 MongoDB Aggregation으로 변환할 수 있었다.
- 하지만, MongoDB의 Aggregation만 공부했지, MongoDB에 대해서는 많이 공부하지 못했다.
- Spring Data MongoDB를 사용해 보면서 MongoDB도 깊게 공부해 보고 싶다는 생각이 들었다. ㅎㅎ
'MongoDB' 카테고리의 다른 글
MongoDB Aggregation를 활용해서 QueryDSL-JPA 대체하기 #1 (0) | 2023.02.10 |
---|