본문 바로가기

Database & ORM/MongoDB

MongoTemplate으로 Aggregation 사용하기

Map-Reduce

MongoDB의 Document를 다른 형태로 Mapping하는 것을 Map 이라고 한다.

 

Document나 Mapping한 데이터 내 여러 값을 취해 합계나 평균 등을 만드는 작업을 Reduce라고 한다.

이렇게 Doucment의 데이터를 매핑하고 취합하는 것을 Map-Reduce 작업이라고 한다.

Aggregation

MongoDB에서는 MapReduce보다 향상된 Aggregate라는 기능을 제공한다.

aggregate는 일종의 리눅스와 비슷한 pipeline을 가지면 각 단계별로 진행을 마친 후에 result을 가져오게 된다. 

아래와 같은 순서로 pipeline을 진행한 후에 결과값을 가져오게 된다.


collection > $project > $match > $group > $sort > $skip > $limit > $unwind > $out

 

$project, $match 등 각 단계를 stage라고 한다.

 

예시를 위해 test라는 Collection을 만들고, Doucment의 구조는 name, sex, subject, socre로 구성되어 있으며 아래 사진과 같다. 참고로, MongoDBCompass라는 프로그램을 사용하면 Doucments와 Aggregations, 인덱싱하는 작업 등을 편하게 할 수 있다.

 

MongoDBCompass에서 Aggregations로 Aggregate하는 작업을 하였다.

작업 단계는 $match > $group > $sort 이다.

 

1 단계: $match 로 성별이 여자인 Document만 필터링 하였다.

 

2 단계: $group 로 Doucment 내 필요한 정보만 새로운 객체로 Grouping 하였다.

1단계에서 $match로 필터링한 Document를 이용해서 $group으로 grouping 하는 작업을 하였다.

새로운 객체의 _id는 name으로 하였다. $sum을 이용해서 score의 총합을 totalScore로 구하고, $avg를 이용해서 score의 평균을 avgScore로 구했다.

{
	_id = "$name",    -- "$Document 내 필드 이름"
    "totalScore":{
    	$sum: "$score"
    },
    "avgScore":{
    	$avg: "$score"
    }
}

 

3 단계: $sort로 Grouping된  정보를 정렬하였다.

$sort에서 정렬 기준 필드와 정렬 방향을 정해주면 된다. 정렬 방향은 오름차순은 1이고, 내림차순은 -1이다. 

여기서는 Grouping된 정보의 totalScore를 정렬 기준으로 하고 내림차순으로 정렬했다.

 

Spring에서 MongoTemplate으로 Aggregation하기

MongoDBCompass에서 $match>$group>$sort 순서로 Aggregate한 것을 Spring의 MongoTemplate으로 구현하였다.

Document 데이터 입력

우선 테스트를 Document를 만들고 데이터를 입력했다.

 

아래는 Document class이다. @Document를 붙이고 사용하는 Collection을 정해주면 된다. 

여기서 사용한 Collection 이름은 test이다.

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@Document(collection = "test")
public class TestDocument {

    @Id
    private String id;
    
    private String name;
    private String sex;
    private String subject;
    private int score;
    
    public TestDocument(String name, String sex, String subject, int score) {
    	this.name = name;
    	this.sex = sex;
    	this.subject = subject;
    	this.score = score;
    }
    
}

 

데이터 입력을 위해 Post API를 만들고 AggregationTestService의 insertTestData() 메서드를 호출한다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/aggregation")
public class AggregationTestController {
	
	private final AggregationTestService aggregationTestService;
	
	@PostMapping
	public ResponseEntity<Void> insertTestData(){
		aggregationTestService.insertTestData();
		return ResponseEntity.ok().build();
	}
}

 

insertTestData()에서 아래와 같이 데이터를 만들어서 Collection에 insert 한다.

@Service
@RequiredArgsConstructor
public class AggregationTestService {
    private final MongoTemplate mongoTemplate;
    
    public void insertTestData() {
    	
    	List<TestDocument> dataList = new ArrayList<TestDocument>();
    	
    	dataList.add(new TestDocument("가몽고", "여자", "국어", 100));
    	dataList.add(new TestDocument("가몽고", "여자", "수학", 60));
    	
    	dataList.add(new TestDocument("나몽고", "여자", "국어", 100));
    	dataList.add(new TestDocument("나몽고", "여자", "수학", 100));
    	
    	dataList.add(new TestDocument("다몽고", "남자", "국어", 70));
    	dataList.add(new TestDocument("다몽고", "남자", "수학", 30));
    	
    	dataList.add(new TestDocument("라몽고", "남자", "국어", 80));
    	dataList.add(new TestDocument("라몽고", "남자", "수학", 40));
    	
    	mongoTemplate.insertAll(dataList);
    }
}

Document 데이터 Aggregate

Aggregate한 데이터를 가져오기 위해 Get API를 만들었다.

AggregationTestService의 mongoTemplateAggregate() 메서드를 호출한다.

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.tradlinx.zimgo.doc.model.TestResult;
import com.tradlinx.zimgo.doc.service.AggregationTestService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/aggregation")
public class AggregationTestController {
	
	private final AggregationTestService aggregationTestService;
	
    @GetMapping
	public ResponseEntity<List<TestResult>> aggregationTest() throws Exception {
    	List<TestResult> resultList = aggregationTestService.mongoTemplateAggregation();
        return ResponseEntity.ok().body(resultList);
    } 
    
	@PostMapping
	public ResponseEntity<Void> insertTestData(){
		aggregationTestService.insertTestData();
		return ResponseEntity.ok().build();
	}
}

 

mongoTemplateAggregate 메서드만 작성했다.

Aggregate의 결과가 담긴 Document 클래스는 org.bson.Document이다.

@Service
@RequiredArgsConstructor
public class AggregationTestService {
    private final MongoTemplate mongoTemplate;
    
	public List<TestResult> mongoTemplateAggregation() {
		
		// 1단계: $match
		// sex가 여자인 Document만 필터링
		MatchOperation matchOperation = 
				Aggregation.match(Criteria.where("sex").is("여자"));
		
		// 2단계: $group
		GroupOperation groupOperation =        	 // {
				Aggregation.group("name")       //    _id : "$name"
				.sum("score").as("totalScore")  //   "totalScore":{ $sum: "$score" },
				.avg("score").as("avgScore");   //   "avgScore":{ $avg: "$score" }
		                                       	 // }
		
		// 3단계: $sort
		// totalScore 내림차순으로 정렬
		SortOperation sortOperation = 
				Aggregation.sort(Direction.DESC, "totalScore");
		
		// match, group, sort Operation을 순서대로 넣기.
		Aggregation aggregation = 
				Aggregation.newAggregation(
						matchOperation
						,groupOperation
						,sortOperation
						);
		
		// org.bson.Document 객체를 이용해 결과를 가져온다.
		List<Document> results = 
                // aggregation 객체, collection 이름, 응답 받아올 객체 클래스
                mongoTemplate.aggregate(aggregation, "test", Document.class)
                .getMappedResults();		
		
		// Document의 결과를 Java에서 사용할 응답 객체로 매핑한다.
		List<TestResult> resultList = 
				results.stream()
						.map(TestResult::new)
						.collect(Collectors.toList());

		return resultList;
	}
}

 

아래는 응답 객체로 사용한 TestResult이다.

생성자에서 Aggregate의 결과로 나온 Document의 값을 TestResult의 필드와 매핑한다.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TestResult {

	private String name;
	private Integer totalScore;
	private Double avgScore;
	
	public TestResult(Document document) {
		name = document.getString("_id");
		totalScore = document.getInteger("totalScore");
		avgScore =  document.getDouble("avgScore");
	}
}

 

Get API를 호출하면 아래와 같이 응답을 받아온다.

MongoDBCompass의 Aggregations에서 Aggregate한 것과 같은 결과가 나왔다.

[GET] http://localhost:8080/aggregation

[
    {
        "name": "나몽고",
        "totalScore": 200,
        "avgScore": 100.0
    },
    {
        "name": "가몽고",
        "totalScore": 160,
        "avgScore": 80.0
    }
]

 

참고자료

https://www.mongodb.com/docs/manual/aggregation/

 

Aggregation Operations — MongoDB Manual

Docs Home → MongoDB ManualAggregation operations process multiple documents and return computed results. You can use aggregation operations to:Group values from multiple documents together.Perform operations on the grouped data to return a single result.

www.mongodb.com

 

https://m.blog.naver.com/ijoos/221312444591

 

MongoDB Aggregate

오늘은 MongoDB의 아주 강력한 툴인 aggregate에 대해서 배워보고자 한다. 간단한 예시로 시작을 해보자...

blog.naver.com