프로그램/Java

Spring Boot Micro Service Architecture 만들기 (3)

with-RL 2023. 7. 9. 14:51

이 포스트는 이전 포스트에서 생성한 Spring Boot MSA 프로젝트를 기반으로 MongoDB와 연동해서 간단한 CRUD를 해 보는 과정에 대한 설명입니다.

이 포스트는 다음 과정을 완료한 후에 참고하시길 바랍니다.

1. MongoDB 설치 (docker)

  • 우선 데이터를 저장할 MongoDB를 설치하겠습니다. 이번 과정에서는 docker를 이용해서 설치하도록 하겠습니다.
  • 우선 ‘Docker Desktop’을 실행합니다.

  • docker.yml 파일을 만들고 아래와 같이 편집합니다.
services:
  mongodb:
    image: mongo:latest
    container_name: mongodb-mymsa
    ports:
      - 27017:27017

  • 터미널에서 아래와 같이 명령을 실행합니다.
$ docker-compose -f docker.yml up

  • ‘Docker Desktop’에 ‘mongodb-mymsa’ 컨테이너가 추가되어 있는 것을 확인할 수 있습니다.

docker-compose 명령을 실행한 터미널에서 Ctrl+C를 입력해서 명령 실행을 종료합니다.

  • docker-compose 명령을 실행한 터미널에서 Ctrl+C를 입력해서 명령 실행을 종료합니다.

  • 방금 전에 실행한 명령으로 인해 MongoDB가 종료됐습니다.
  • ‘Docker Desktop’에서 ‘mongodb-mymsa’ 컨테이너에서 ‘Start’ 버튼을 눌러서 컨테이너를 재 시작합니다.

  • 이후로는 ‘Docker Desktop’에서 컨테이너의 시작, 정지를 관리하면 됩니다.

2. MongoDB 기능 구현

  • 이제 MongoDB를 이용해서 간단한 정보를 생성/조회/수정/삭제하는 기능을 만들어보겠습니다.
  • MongoDB의 아래와 같은 접속 관련 정보를 ‘app-web/src/main/resources/application.yml’에 추가합니다.
  data:
    mongodb:
      uri: mongodb://localhost:27017/mymsa
      auto-index-creation: true

  • 데이터 형식을 정의한 ‘BlogPost’ 클래스를 web-app의 ‘com.mymsa.web.model’ 패키지에 생성하고 아래와 같이 편집합니다.
package com.mymsa.web.model;

import lombok.*;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoId;

import java.util.Date;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Document
@ToString
public class BlogPost {

    @MongoId(value = FieldType.OBJECT_ID)
    private String post_id;

    private String title;

    private String context;

    private Date create_time;

}

  • MongoDB에 BlogPost 데이터를 저장/조회/수정/삭제하는 기능을 수행하는 BlogPostRepository 클래스를 ‘com.mymsa.web.repository’ 패키지에 생성하고 아래와 같이 편집합니다.
package com.mymsa.web.repository;

import com.mymsa.core.context.MSAContext;
import com.mymsa.web.model.BlogPost;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Date;

@Repository
public class BlogPostRepository {

    @Autowired
    private ReactiveMongoTemplate mongoClient;

    public Mono<BlogPost> create(MSAContext context, BlogPost blogPost) {
        blogPost.setCreate_time(new Date());
        return mongoClient.save(blogPost);
    }

    public Flux<BlogPost> findAll(MSAContext context) {
        return mongoClient.findAll(BlogPost.class);
    }

    public Mono<BlogPost> findById(MSAContext context, String post_id) {
        Query query = Query.query(Criteria.where("post_id").is(post_id));
        return mongoClient.findOne(query, BlogPost.class);
    }

    public Mono<BlogPost> update(MSAContext context, BlogPost blogPost) {
        Query query = Query.query(Criteria.where("post_id").is(blogPost.getPost_id()));
        Update update = Update.update("title", blogPost.getTitle())
                .set("context", blogPost.getContext());

        return mongoClient.findAndModify(query, update, BlogPost.class)
                .switchIfEmpty(Mono.error(new Exception(String.format("no blogPost: %s", blogPost.getPost_id()))));
    }

    public Mono<BlogPost> deleteById(MSAContext context, String post_id) {
        Query query = Query.query(Criteria.where("post_id").is(post_id));
        return mongoClient.findAndRemove(query, BlogPost.class);
    }

}

  • BlogPostRepository를 이용해 BlogPost 데이터를 관리하고 필요한 기능을 수행하는 BlogPostService 클래스를 ‘com.mymsa.web.service’ 패키지에 생성하고 아래와 같이 편집합니다.
package com.mymsa.web.service;

import com.mymsa.core.context.MSAContext;
import com.mymsa.web.model.BlogPost;
import com.mymsa.web.repository.BlogPostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class BlogPostService {

    @Autowired
    private BlogPostRepository blogPostRepository;

    public Mono<BlogPost> create(MSAContext context, BlogPost blogPost) {
        return blogPostRepository.create(context, blogPost);
    }

    public Flux<BlogPost> findAll(MSAContext context) {
        return blogPostRepository.findAll(context);
    }

    public Mono<BlogPost> findById(MSAContext context, String post_id) {
        return blogPostRepository.findById(context, post_id);
    }

    public Mono<BlogPost> update(MSAContext context, BlogPost blogPost) {
        return blogPostRepository.update(context, blogPost);
    }

    public Mono<BlogPost> deleteById(MSAContext context, String post_id) {
        return blogPostRepository.deleteById(context, post_id);
    }

}

  • HTTP를 이용해서 외부로부터 BlogPost 데이터를 관련 명령을 받고 처리 결과를 리턴하는 기능을 수행하는 BlogPostController 클래스를 ‘com.mymsa.web.controller’ 패키지에 생성하고 아래와 같이 편집합니다.
package com.mymsa.web.controller;

import com.mymsa.core.context.MSAContext;
import com.mymsa.web.model.BlogPost;
import com.mymsa.web.service.BlogPostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RequestMapping("/app-web")
@RestController
@Slf4j
public class BlogPostController {

    @Autowired
    private BlogPostService blogPostService;

    @PostMapping("/blog-post")
    public Mono<BlogPost> create(@RequestHeader(value = "sessionID", required = false) String sessionID, @RequestBody BlogPost blogPost) {
        MSAContext context = new MSAContext(sessionID);
        log.info("{}|create-blog-post||", context);
        return blogPostService.create(context, blogPost)
                .doOnSuccess(result -> {
                    log.info("{}|create-blog-pos|Y|{}", context, result);
                })
                .doOnError(error -> {
                    log.error("{}|create-blog-pos|N|", context, error);
                });
    }

    @GetMapping("/blog-post")
    public Flux<BlogPost> findAll(@RequestHeader(value = "sessionID", required = false) String sessionID) {
        MSAContext context = new MSAContext(sessionID);
        log.info("{}|findAll-blog-post||", context);
        return blogPostService.findAll(context)
                .doOnComplete(() -> {
                    log.info("{}|findAll-blog-post|Y|{}", context);
                })
                .doOnError(error -> {
                    log.error("{}|findAll-blog-post|N|", context, error);
                });
    }

    @GetMapping("/blog-post/{post_id}")
    public Mono<BlogPost> findById(@RequestHeader(value = "sessionID", required = false) String sessionID, @PathVariable String post_id) {
        MSAContext context = new MSAContext(sessionID);
        log.info("{}|find-blog-post||{}", context, post_id);
        return blogPostService.findById(context, post_id)
                .doOnSuccess(result -> {
                    log.info("{}|find-blog-post|Y|{}", context, result);
                })
                .doOnError(error -> {
                    log.error("{}|find-blog-post|N|", context, error);
                });
    }

    @PutMapping("/blog-post")
    public Mono<BlogPost> update(@RequestHeader(value = "sessionID", required = false) String sessionID, @RequestBody BlogPost blogPost) {
        MSAContext context = new MSAContext(sessionID);
        log.info("{}|update-blog-post||{}", context, blogPost);
        return blogPostService.update(context, blogPost)
                .doOnSuccess(result -> {
                    log.info("{}|update-blog-post|Y|{}", context, result);
                })
                .doOnError(error -> {
                    log.error("{}|update-blog-post|N|", context, error);
                });
    }

    @DeleteMapping("/blog-post/{post_id}")
    public Mono<BlogPost> deleteById(@RequestHeader(value = "sessionID", required = false) String sessionID, @PathVariable String post_id) {
        MSAContext context = new MSAContext(sessionID);
        log.info("{}|delete-blog-post||{}", context, post_id);
        return blogPostService.deleteById(context, post_id)
                .doOnSuccess(result -> {
                    log.info("{}|delete-blog-post|Y|{}", context, result);
                })
                .doOnError(error -> {
                    log.error("{}|delete-blog-post|N|", context, error);
                });
    }

}

  • 상단 툴바의 ‘Configurations’에서 ‘my-msa[clean]’을 선택하고 실행을 눌러서 기존의 모든 build 정보를 삭제합니다. 이 명령을 실행하기 전에는 my-msa 관련 모든 프로그램은 정지된 상태이어야 합니다.

상단 툴바의 ‘Configurations’에서 ‘my-msa[build]’을 선택하고 실행을 눌러서 프로그램을 빌드합니다.

 

3. 서버 실행 및 기능 시험

  • 이제 MongoDB에 정보를 생성/조회/수정/삭제하는 기능을 확인해 보겠습니다.
  • Power Shell을 3개 이상 실행하고 각각 Power Shell에서 cd 명령을 이용해 my-msa 프로젝트 폴더로 이동합니다.
  • 첫 번째 Power Shell에서 아래 명령을 실행해서 app-eureka 서버를 실행합니다.
$ java "-Dspring.profiles.active=local" -jar .\app-eureka\build\libs\app-eureka-1.0.0.jar
  • 두 번째 Power Shell에서 아래 명령을 실행해서 app-gateway 서버를 실행합니다.
$ java "-Dspring.profiles.active=local" -jar .\app-gateway\build\libs\app-gateway-1.0.0.jar
  • 나머지 Power Shell에서 아래 명령을 실행해서 app-web 서버를 실행합니다.
$ java "-Dspring.profiles.active=local" -jar .\app-web\build\libs\app-web-1.0.0.jar

  • 이제 REST API를 툴을 이용해서 기능을 시험해 보겠습니다. 저는 크롬 플러그인으로 쉽게 사용할 수 있는 Talend API Tester – Free Edition을 사용했습니다.
  • 우선 생성 테스트를 위해서 아래와 같이 json을 POST 합니다.
     Method: POST
     URL: http://localhost:9001/app-web/blog-post
     BODY: <아래 내용>
{
  "title": "blog post titie #1",
  "context": "context #1"
}

  • 이제 MongoDB의 내용을 확인해 보겠습니다. MongoDB의 내용을 확인하기 위한 툴로 저는 Studio 3T를 이용했습니다.
  • localhost의 MongoDB에 접속하고 mymsa 데이터베이스의 blogPost 컬렉션을 확인하면 방금 전에 입력한 데이터가 저장된 것을 확인할 수 있습니다.

  • 테스트를 위해서 위 내용을 조금씩 변경하면서 POST 요청을 해서 여러 개의 BlogPost 정보를 생성해 봅니다. DB의 정보를 새로고침 해보면 아래와 같이 여러 개의 데이터가 생성된 것을 확인할 수 있습니다.

  • 이번에는 아래와 같은 GET 명령을 이용해 생성된 데이터를 조회해 보겠습니다.
     Method: GET
     URL: http://localhost:9001/app-web/blog-post
  • 여러 개의 BlogPost 정보를 확인할 수 있습니다.

  • 이번에는 아래와 같이 GET 명령을 이용해 한 개의 데이터를 조회해 보겠습니다.
    Method: GET
    URL: http://localhost:9001/app-web/blog-post/<post_id>
  • URL의 마지막 부분의 <post_id>는 조회하고자 하는 BlogPost의 post_id 값입니다.
  • 특정한 한 개의 BlogPost를 확인할 수 있습니다.

  • 이번에는 아래와 같이 PUT 명령을 이용해 데이터의 내용을 수정해 보겠습니다.
     Method: PUT
     URL: http://localhost:9001/app-web/blog-post
     BODY: <아래 내용>
{
  "post_id": "<post_id>",
  "title": "blog post modified title #1",
  "context": "context modify for test #1"
}
  • 위 내용 중 post_id는 수정하고자 하는 BlogPost의 post_id로 변경해야 합니다.

  • DB의 정보를 새로고침 해보면 아래와 같이 한 개의 데이터가 수정된 것을 확인할 수 있습니다.

  • 이번에는 아래와 같이 DELETE 명령을 이용해 데이터를 삭제해 보겠습니다.
     Method: DELETE
     URL: http://localhost:9001/app-web/blog-post/<post_id>
  • URL의 마지막 부분의 <post_id>는 삭제하고자 하는 BlogPost의 post_id 값입니다.

  • DB의 정보를 새로고침 해보면 아래와 같이 한 개의 데이터가 삭제된 것을 확인할 수 있습니다.

  • app-gateway 창을 확인하면 수행하는 명령에 관련된 로그가 잘 찍히고 있는 것을 확인할 수 있습니다.

  • app-web 창을 확인하면 수행하는 명령에 관련된 로그가 잘 찍히고 있는 것을 확인할 수 있습니다.
  • app-gateway와 app-web의 sessionID 값을 비교해서 값이 같으면 동일한 명령에 대한 처리 로그인 것을 알 수 있습니다.

참고

이 포스트와 관련 포스트입니다.