티스토리 뷰

728x90
반응형

목차

     

    앞으로 쓰게 될 모든 웹플럭스 실습 글에 굳이 강조하겠지만,

     

    아래의 코드는 그야말로 내 마음대로 공식문서와 각종 레퍼런스를 짜깁기해서 만들어낸 결과물이다.

     

    당연히 Best Choice와는 거리가 멀 수밖에 없지만, 이거라도 잊지 않으려고 하나씩 적어본다.

     

    우선 리액티브 몽고 템플릿에 대한 소개로 시작하자.

     

    Reactive Mongo Template

     

    아직 한 번도 적지 않은 것 같은데, 리액티브 몽고디비를 사용하려면 그래들에 아래와 같은 의존성을 추가해야 한다.

    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'

    우선 여기서 알아갈 수 있는 사실은, 리액티브 몽고디비 역시 스프링 데이터의 일원이라는 사실이다.

     

    계속해서 리액티브 몽고 템플릿은 스프링부트와 스프링 데이터에 속한, 비동기 데이터 처리에 사용되는 템플릿이다.

     

    몽고디비의 리액티브 드라이버를 사용해서 비동기 처리를 수행하며, 주요 특징은 아래와 같다.

     

    1. 리액티브 스트림
      리액티브 스트림 API를 지원하며 비동기/논블로킹 프로그래밍과 데이터 액세스를 지원한다.
      따라서 함수형 프로그래밍을 쉽게 사용할 수 있으며, 데이터 처리 역시 함수형으로 구현할 수 있다.
      이로 인해 높은 성능과 함께 대규모 트래픽을 위한 확장성 역시 보장한다.
    2. 스프링 데이터
      위에서 언급했듯이 리액티브 몽고 템플릿은 스프링 데이터를 기반으로 구현된 프로젝트이다.
      때문에 스프링과 궁합이 좋으며, 아래에서 언급하겠지만 굉장히 편리한 쿼리 작성을 지원하고,
      JPA를 사용할 때와 마찬가지로 메서드 이름으로 쿼리를 생성하는 기능 역시 지원한다.
      추가로 자바 객체와의 변환 작업 역시 자동으로 처리해 준다.
    3. 데이터 스트림
      리액티브 스트림을 지원하기 때문에 데이터를 스트림으로, 즉 실시간으로 처리하며 모니터링할 수 있다.
    4. 인덱스 자동 생성
      리액티브 몽고 템플릿은 성능 향상을 위한 인덱스 작업을 자동으로 수행한다.
      이는 일반 몽고디비와는 다른 특성으로, 클래스에서 @Indexed만 필드에 추가해 주면 알아서 인덱스가 생성된다.

    추가로 일반 몽고디비를 사용하다가 리액티브로 마이그레이션 하려면 코드를 많이 수정해야 한다는 단점이 있다고 한다.

     

    하지만 나는 일반 몽고디비를 써본 적이 없으므로 대충 생략.

     

    요약하면 리액티브 몽고디비, 리액티브 몽고 템플릿은 비동기/논블로킹에 인덱스까지 알아서 해주는,

     

    대용량 고성능 디비라고 할 수 있다.

     

    실제로 조금 써보니 쿼리 속도가 매우 빠르긴 하다.

     

    계속해서 오늘의 주제인, 리액티브 몽고 디비를 사용한 쿼리, 정렬, 페이지네이션에 대해 알아보자.

     

    Reactive Mongo Template in Use

     

    다른 글에서 정리하게 되겠지만, 함수형 엔드포인트를 사용하는 스프링 웹플럭스 프로젝트는

     

    MVC패턴을 사용하는 프로젝트와 그 생김새가 전혀 다르다.

     

    여기선 우선 필요한 부분만 적어보겠다. 쿼리를 실행할 도메인의 이름은 <Rating>이다.

     

    • Router
    @Configuration
    public class RatingRouter {
    
    	@Bean
    	public RouterFunction<ServerResponse> ratingRoute(RatingHandler ratingHandler) {
    		return route()
    			.GET("/beers/{beerId}/ratings/get", serverRequest -> {
    				int page = Integer.parseInt(serverRequest.queryParam("page").orElse("1"));
    				String sort = String.valueOf(serverRequest.queryParam("sort").orElse("new"));
    				Mono<Page<Rating>> pageMono = ratingHandler.readRatingPageMono(serverRequest, sort, page);
    				return ServerResponse.ok().body(pageMono, new ParameterizedTypeReference<Page<Rating>>() {});
    			})
    			.build();
    	}
    }

    특이하게도 라우터 클래스를 따로 둔다.

     

    지금은 필요한 엔드포인트만 제외하고는 다 지운 상태.

     

    페이징과 정렬을 위한 쿼리 파라미터 설정이 굉장히 간단한 것을 확인할 수 있다.

     

    ParameterizedTypeReference 클래스는 제네릭 타입의 정보를 유지하며 쓸 수 있도록 도와주는 클래스라고 한다.

     

    나도 정확하게는 모르지만 서버의 응답을 비동기적으로 객체에 매핑시킬 때 사용한다고 한다.

     

    • Handler
    public Mono<Page<Rating>> readRatingPageMono(ServerRequest serverRequest, String sort, int page) {
    
    	String beerId = serverRequest.pathVariable("beerId");
    
    	PageRequest pageRequest = PageRequest.of(page - 1, 10);
    
    	return ratingMongoRepository.findRatingsPageByBeerId(beerId, sort, pageRequest);
    }

    라우터에서 지정해 준 핸들러메서드이다.

     

    서비스와 컨트롤러 클래스를 합친 것 같은 감각으로 사용 중이다.

     

    마지막에 몽고 레포지토리를 호출한다.

     

    • Repository
    @Service
    @RequiredArgsConstructor
    public class RatingMongoRepository {
    	private final ReactiveMongoTemplate reactiveMongoTemplate;
        
    	public Mono<Page<Rating>> findRatingsPageByBeerId(String beerId, String querySort, Pageable pageable) {
    
    		Query query = Query.query(Criteria.where("beerId").is(beerId)).with(pageable);
    
    		if (querySort == null) {
    			querySort = "new";
    		}
    
    		Sort sort = null;
    
    		switch (querySort) {
    			case "new":
    				sort = Sort.by(Sort.Direction.DESC, "createdAt");
    				break;
    			case "likes":
    				sort = Sort.by(Sort.Direction.DESC, "likeCount");
    				break;
    			case "comments":
    				sort = Sort.by(Sort.Direction.DESC, "commentCount");
    				break;
    		}
    
    		query.with(sort);
    
    		Mono<Long> countMono = reactiveMongoTemplate.count(query, Rating.class);
    		Mono<List<Rating>> ratingsMono = reactiveMongoTemplate.find(query, Rating.class).collectList();
    
    		return Mono.zip(countMono, ratingsMono)
    			.map(tuple -> new PageImpl<>(tuple.getT2(), pageable, tuple.getT1()));
    	}
    }

    본격적인 레포지토리의 쿼리 코드다. 리액티브 몽고 템플릿에서는 위와 같이 쿼리와 정렬을 편하게 정해줄 수 있다.

     

    먼저 쿼리에 Rating 중 맥주 아이디가 주어진 값과 같은 도큐먼트들을 페이징을 적용해서 불러오도록 작성한다.

     

    이어서 정렬(Sort) 객체를 생성해 주어지는 값에 따라 다른 방법, 동시에 내림차순으로 정렬하도록 작성한 뒤에,

     

    미리 생성했던 쿼리 객체가 해당 정렬을 사용하도록 해주었다.

     

    이후에 Mono 객체인 countMono를 생성해서 쿼리 결과의 총개수를 찾고,

     

    역시 쿼리로 불러올 수 있는 전체 데이터를 리스트로 작성한 뒤에 zip() 연산자를 이용해 PageImpl로 만들어서 리턴해주었다.

     

    그리고 결과.

    {
        "content": [
            {
                "id": "6429a096c68a8a650d121ec9",
                "beerId": "6429a08ec68a8a650d121ec8",
                "userId": "6415216b8b2d722fb558853e",
                "star": 5.0,
                "content": "content223",
                "likeCount": 1,
                "commentCount": 1,
                "beerTagList": [
                    "SOUR"
                ],
                "commentList": [
                    "642abd91be9ca372758e5283",
                    "642abddb4d759d2f981fd35a",
                    "642ac5983e37e459def5f65f"
                ],
                "createdAt": "2023-04-03 00:34:44",
                "modifiedAt": "2023-04-03 00:37:51"
            },
            // ... 생략
        ],
        "pageable": {
            "sort": {
                "empty": true,
                "sorted": false,
                "unsorted": true
            },
            "offset": 0,
            "pageNumber": 0,
            "pageSize": 10,
            "unpaged": false,
            "paged": true
        },
        "last": true,
        "totalPages": 1,
        "totalElements": 6,
        "sort": {
            "empty": true,
            "sorted": false,
            "unsorted": true
        },
        "first": true,
        "number": 0,
        "size": 10,
        "numberOfElements": 6,
        "empty": false
    }

    당연하게도 잘 돌아가는 것을 확인할 수 있다.

     

    이런 식으로 코드를 작성하기 전까지는 쿼리DSL을 JPA처럼 사용하지 못한다는 게 아쉬웠지만

     

    일단 구현에 성공하고 돌아보니, 전혀 그립지 않을 정도로 간편하게 쿼리를 작성할 수 있었다.

     

    좀 더 복잡한 쿼리에 대해서도 연습해봐야지.

     

    편리하고 직관적인 리액티브 몽고 템플릿의 쿼리, 끝!

    반응형
    댓글
    공지사항
    최근에 올라온 글
    최근에 달린 댓글
    Total
    Today
    Yesterday
    링크
    «   2024/05   »
    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
    글 보관함