본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Spring framework/Spring Webflux

spring webflux 7 (웹플럭스 적용기, MongoDb)

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2020. 4. 1.

웹플럭스에서 몽고DB 연동은 "이렇게 해놨는데 동작해?" 라는 느낌이 들 정도로 매우 간단하다.

몇번의 환경설정만 해 주면 데이터베이스에서 이미 동작중인 모습을 볼 수 있다.

먼저 몽고db와의 연동을 위해서 라이브러리를 추가한다.

 

*Maven 기준

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>

 

라이브러리를 추가한 뒤에 application.properties에서 데이터베이스와 관련된 세팅을 해 준다.

spring.data.mongodb.database=db이름
#spring.data.mongodb.username=만약아이디가 필요하면
#spring.data.mongodb.password=만약비밀번호가 필요하면
spring.data.mongodb.host=주소
spring.data.mongodb.port=포트번호

 

위 2번의 세팅으로 몽고DB와의 연동이 끝이났다.

프로젝트를 동작시켰는데 아무런 문제가 없다면 이상없이 데이터베이스에 연결이 된 것 이다.

만약 에러가 발생하거나 문제가 존재한다면 데이터베이스에 대한 접속정보가 틀리거나, 메이븐 라이브러리가 다 받아지지 않았기 때문이다.

 

나머지 작업을 이어가보자.

JPA 방식으로 만들어야하기 때문에 데이터베이스를 매핑할 vo 객체를 만들어 준다.

클래스 이름은 MyDocument로 하였다.

import java.io.Serializable;

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

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

@Data
@Document(value="mydoc")  //조회할 컬렉션 이름은 mydoc 이다.
@Getter
@Setter
public class MyDocument implements Serializable{
    private static final long serialVersionUID = 142466781L;

    @Id
    private String id;
	
    private String text;
	
    private int number;
	
    private int counts;
	
    private int total;
	
    private String[] texts;
}

 

Document에서 정의한 value 값은 조회할 컬렉션 이름을 의미한다.

그리고 Id로 보이는 에노테이션이 바로 조회할 몽고DB 컬렉션의 고유 키 값을 의미한다.

기타 나머지 변수들은 몽고DB컬렉션에 존재하는 Document를 의미한다.

위 내용에 대한 실제 몽고DB 컬렉션 모양이다.

딱히 어렵지 않는 컬렉션의 모습이다.

 

저렇게 vo클래스를 만들어 준 다음 데이터베이스와 연동할 저장소 클래스를 만든다.

만들 저장소는 인터페이스 형태이어야 하며, ReactiveMongoRepository 라는 인터페이스를 상속 받아야 한다.

import org.springframework.data.domain.Pageable;

import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

import reactor.core.publisher.Flux;


@Repository
public interface MongoDbRepository extends ReactiveMongoRepository<MyDocument, String>  {
	
    Flux<MyDocument> findAll();  //내가만든 조회 메소드
	
    Flux<MyDocument> findByText(String text);  //내가만든 조건이 있는 조회 메소드
	
    @Query("{'text': {$regex: ?0 }}")
    Flux<MyDocument> findRegexByText(String text);  //내가만든 like 모양의 조회 메소드

    @Query("{'text': {$regex: ?0 }}")
    Flux<MyDocument> findRegexPagingByText(String text, Pageable page);  //내가만든 페이징 처리가 가능한 조회 메소드
    
}    

 

미리 몇개의 메소드를 만들어 보았다.

Query라는 에노테이션을 설정하면 세부적인 조건을 붙여줄 수 있으며 "?숫자" 형식으로 조건을 붙일 수 있다.

라우터 클래스에서 적용시키는 것은 AutoWired 나 AllArgsConstructor 에노테이션을 활용하여 해당 저장소 인터페이스를 불러오면된다.

 

실제 적용한 라우터 클래스를 한번 살펴보자.

조회, 등록, 수정 및 삭제에 대한 예를 작성하였다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Configuration
public class MyRouter {
	
    @Autowired MongoDbRepository db;
	
    @Bean
    public RouterFunction<ServerResponse> findAll() {  //find 라는 get방식이 요청오면 동작하는 메소드
        final RequestPredicate predicate = RequestPredicates.GET("/find").and(RequestPredicates.accept(MediaType.TEXT_PLAIN));		
        RouterFunction<ServerResponse> response = RouterFunctions.route(predicate, (request)->{
            Flux<MyDocument> mapper = db.findAll();  //만들어준 전체 조회 메소드
			
            db.findByText("good").collectList().subscribe(System.out::println);  //만들어준 단일 검색
            db.findRegexByText(".*go.*").collectList().subscribe(System.out::println);  //만들어준 like 검색
			
            Pageable page = PageRequest.of(0, 2);
            db.findRegexPagingByText(".*go.*", page).collectList().subscribe(System.out::println);  //만들어준 페이징처리 검색
			
            //등록, insert 메소드는 ReactiveMongoRepository 클래스 메소드이다.
            MyDocument doc = new MyDocument();
            doc.setNumber(1234567);
            doc.setText("insert database!");
            db.insert(doc).subscribe(System.out::println);			
			
            //수정, save 메소드는 ReactiveMongoRepository 클래스 메소드이다.
            db.findByText("good").flatMap( target ->{  //findByText 메소드는 만들어준 단일 검색 메소드이다.
                target.setNumber(555555);              //flatMap을 통해서 대상의 number필드값을 바꾼 뒤에
                return db.save(target);  //save를 통해서 해당 필드 내용을 교체하여 주고 있다.
            }).subscribe(System.out::println);

            //삭제, deleteById 메소드는 ReactiveMongoRepository 클래스 메소드이다.
            db.findByText("good")
                .flatMap( target -> db.deleteById(target.getId()))
                .subscribe(System.out::println);
                
			//위 내용을 토대로 응답은 아래처럼 해 주면 된다.
            Mono<ServerResponse> res = ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromProducer(mapper, MyDocument.class));
            return res;
        });
        return response;
    }	
}    

 

직접만든 MongoDbRepository 인터페이스에는 4개의 메소드만 존재해야 되는데, insert 와 save deleteById라는 메소드가 오버라이딩되어 사용 할 수 있었다.

ReactiveMongoRepository 인터페이스에서도 다른 메소드들도 많이 지원하여준다.

등록, 수정, 삭제는 굳이 메소드를 추가하지 않아도 오버라이딩된 메소드만 사용하여도 무방하다.

like처럼 검색할 때는 입력한 조건에  .* 을 붙여주면 된다.

 

수정 같은 경우는 조금 특별하다.

먼저 조회를 통해 대상을 찾는다. 그리고 찾은 결과에서 변경할 부분을 변경한 뒤에 save메소드를 동작시켜 주었다.

행위가 마치 selectAndUpdate 형태로 된 것을 볼 수 있다.

만약 save 메소드만 단독으로 동작시키면 "교체"가 되어버린다.

 

무슨말인고 하면, save를 단독으로 아무런 조건 없이 사용하면 교체할 필드값만 바뀌는게 아니라 해당 컬렉션 전부를 교체할 내용으로 save 하게 되어 버린다.

말이 어렵다면...연습삼아 한번 해 보는 것을 추천한다..^^

 

몽고DB의 집계 기능인 Aggreagate를 사용하는 것도 에노테이션만 잘 활용하여 준다면 쉽게 가능하다.

MongoDbRepository에 Aggreagate기능을 추가하여보았다.

import org.springframework.data.domain.Pageable;

import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

import reactor.core.publisher.Flux;


@Repository
public interface MongoDbRepository extends ReactiveMongoRepository<MyDocument, String>  {
	
    Flux<MyDocument> findAll();  //내가만든 조회 메소드
	
    Flux<MyDocument> findByText(String text);  //내가만든 조건이 있는 조회 메소드
	
    @Query("{'text': {$regex: ?0 }}")
    Flux<MyDocument> findRegexByText(String text);  //내가만든 like 모양의 조회 메소드

    @Query("{'text': {$regex: ?0 }}")
    Flux<MyDocument> findRegexPagingByText(String text, Pageable page);  //내가만든 페이징 처리가 가능한 조회 메소드
    
    //pipeline에 aggregate와 관련된 내용을 추가하여준다. 
    //aggregate와 관련된 문법은 MongoDB 문법과 동일하게 써 주면 된다.
    public static final String match ="{ $match : { text : {$regex : ?0 }, number : {$gte:?1} } }";
    public static final String group ="{ $group :  {  _id:null, counts:{$sum : 1}, total:{$sum : '$number'}, texts:{$addToSet : '$text' } }    }";
    public static final String sort ="{ $sort : {_id:-1} }";
    @Aggregation(pipeline = {match, group, sort})
    Flux<MyDocument> aggregateText(String text, int number);    
}    

 

몽고DB 문법과 동일하게 입력하여주면 되며, pipeline에 n개 만큼 넣어 줄 수 있다.

* group에서 grouping한 명칭이 매핑하는 vo 클래스에 해당 메소드가 없으면 오류가난다! 매핑할 대상까지 꼭 포함하도록 하자.

 

아..컨트롤러에는 아래 에노테이션이 추가되어야 한다!

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;

@EnableMongoAuditing  //요거랑!
@EnableReactiveMongoRepositories  //요거!
@SpringBootApplication
public class 클래스 {

    public static void main(String[] args) {
        SpringApplication.run(클래스.class, args);
    }

}

 

모든 행위는 Flux와 Mono로 조립되어 최종적인 RouterFunction<ServerResponse> 형태로 전달해 주는 것을 볼 수 있다.

JPA방식을 사용하는 것이 조금 더 깔끔하고 이해하는데 어렵지 않는 것 같다.

다음번에는 웹플럭스에서 필터, AOP 등을 어떻게 사용하는지에 대해서 작성하여 보겠다.

 

웹플럭스에서 JPA방식으로 몽고db 연결하다보면 언더바(_)형식의 이름의 키 값에 대해서는 상당히 오류가 나타나는 것을 볼 수 있다.

 * 예 : 컬렉션 이름을 good_collection 하던지, 아니면 컬렉션의 키 값을 user_id 이런식으로 하던지

가급적이면 카멜 표기법을 사용해서 이러한 오류를 만나지 말도록 하는 것이 정신건강에 이롭다..ㅠ

아니면 org.bson.Document 클래스를 상속받아서 해당 vo객체를 꾸미는 방법이 있는데..

가급적이면 카멜로....

spring webflux!

 

* 내용을 채우고 수정중입니다.

* 튜토리얼이나 가이드 목적보다도 개념정리에 목적을 두고 쓰고있습니다. 틀린부분이나 누락된 부분은 꼭 알려주세요!

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글