개발 역량 장착하기(4)-마이크로서비스 초간단 실습③

Updated:

2차시 교육 ③ : 마이크로서비스간 동기식 통신

마이크로서비스 단위로 분할된 2개 이상의 시스템에서 상호 영향을 주는 데이터를 일관성 있게 처리하기 위해, 즉 마이크로서비스간 데이터 동기화를 위한 통신 방식에는 동기식 연동과 비동기식 연동이 있습니다. 이에 대해서는 관련 포스팅인 마이크로서비스의 통신 방식을 참조하시기 바랍니다.

이 중 이벤트에 의한 비동기 방식의 연동 방법은 지난 포스팅을 통해 경험해 보았고, 이번 시간에는 마이크로서비스간 동기식 통신(Sync)을 체험해 보도록 하겠습니다.

동기식 연동은 오랫동안 개발자 세계에서 일해오신 분들이라면 너무나 익숙한 개념입니다.
비즈니스 로직, 특히 복잡한 비즈니스 로직의 경우 각기 다른 비즈니스 도메인에 걸쳐 있는 데이터들을 무결성 원칙에 따라 수정해야 하는데, 과거에는 이러한 데이터 무결성을 유지하기 위해서 One Transaction 방식으로 처리하는 것을 선호하였습니다. 결과적으로 모든 데이터가 하나의 DBMS에 담는 모습을 지향하게 되었지요.
물론 하나의 DBMS에 담을 수 없는 이기종 간의 시스템들 간에는 규약(protocol)을 만들어 인터페이스하는 방법도 취하였습니다만… 2000년 이후로 H/W 가격이 점차 저렴해지면서 많은 차세대 프로젝트에서는 시스템 통합의 길을 선택하였습니다. 통합된 시스템에서는 데이터와 로직의 재사용율이 높아지고 서로 다른 업무간 연동 프로그램을 보다 쉽게 개발할 수 있어서 비용과 효율 측면의 장점이 있었지요.

사실, 마이크로서비스에 반대하는 많은 분들 중에는 하나의 단일 시스템(모놀리식)을 마이크로서비스 단위로 쪼개게 되면 여러 시스템에 걸쳐 분산된 데이터에 대하여 일관성과 무결성 처리가 어렵다는 견해를 갖고 있는 분이 많습니다.

사실 저도 개발을 구체적으로 경험해보기 전까지는 복잡도가 높지 않을까 생각했었지요. 그런데, 일단 여기서는 모놀리식과 마이크로서비스의 장단점을 논의하는 자리가 아니기 때문에 그에 대한 이야기는 다루지 않겠습니다. 다만, 마이크로서비스 시스템간 데이터 무결성 처리가 가능하며 이는 앞서 배운 비동기 연동 방식을 통한 방법 뿐 아니라 마이크로서비스에서도 동기식 연동이 가능하다는 것을 아래의 실습을 통해 전달하고자 합니다.

이벤트 다이어그램에서 동기식 연동 표현

앞에서 그렸던 이벤트 다이어그램을 다시 한번 살펴보겠습니다.

위에서 동기식 처리를 해야 할 대상은 무엇일까요?
우선 주문을 위한 결제처리 부분이 동기화해야 할 대상으로 여겨집니다만, 이 부분을 구현하려면 payment 마이크로서비스를 추가해야 하기 때문에 여기서는 다른 부분을 동기화 해보겠습니다. 다음과 같이 이벤트를 다이어그램을 변경하여 주문시 재고처리 하는 부분을 비동기에서 동기식으로 적용해보고자 합습니다.

앞서 비동기식 이벤트 연동에서는 상품주문이 완료된 후에 비동기 방식으로 이벤트를 전달받아 상품재고를 변경한다는 의미지만, 동기식 연동으로 바꾸게 되면 먼저 상품주문시 재고가 있는지를 확인하고 재고를 감소시킨 후에 주문을 완료시키겠다는 의미가 됩니다. :-)

이제 코딩으로 구현해 봅니다.

REST API 구현

마이크로서비스간 동기식 연동은 기본적으로 A가 B를 호출할 때 B에 해당 REST API가 구현되어 있어야 합니다. -

즉, 상품의 재고 처리 로직을 REST API로 구현하기 위하여 먼저 아래와 같이 product 서비스 내에 상품 전용 RestController를 작성합니다.

  • ProductController.java
      @RestController
      public class ProductController {
    
          @Autowired ProductRepository productRepository;
    
          // create bus schedule on given month
          @RequestMapping(value = "/products/{productId}/decreaseStock/{qty}",
                  method = RequestMethod.PUT,
                  produces = "application/json;charset=UTF-8")
          public void decreaseStock(@PathVariable Long productId,
                                  @PathVariable int qty) throws Exception {
    
              Optional<Product> optionalP = productRepository.findById(productId);
              if (!optionalP.isPresent()) {
                  throw new RuntimeException("$$$ 존재하지 않는 상품ID입니다. :: " + productId);
              }
    
              Product product = optionalP.get();
    
              int stocks = product.getStock();
              if (stocks < qty) {
                  throw new RuntimeException("$$$ 상품(" + product.getName() + ")의 현재 재고가 부족합니다. 현재재고 ::  " + stocks);
              }
              product.setStock(stocks - qty);
    
              productRepository.save(product);
          }
    
      }
    
  • 해당 REST API가 잘 동작되는지 테스트해봅니다.
    ~ ❯❯ http PUT localhost:8080/products/1/decreaseStock/10
    

Feign Client 구현

이제, order 서비스 쪽에는 주문 발생시 상품재고를 변경하는 REST API를 호출하는 로직을 Feign Client를 사용하여 구현해 보도록 하겠습니다. Feign Client가 무엇이고, 어떻게 동작하는지에 대해서는 https://engineering-skcc.github.io/msa/jhipster-feign/을 참조하시기 바랍니다.

  • Feign Client를 사용하기 위해 먼저 pom.xml에 feign dependency를 추가합니다.
      <dependencies>
          ...
    		<dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-openfeign</artifactId>
          </dependency>
      </dependencies>
    
    
  • Feign Client가 활성화되도록 OrderApplication에 대해 어노테이션을 추가합니다.
    @EnableBinding(StreamProcessor.class)
    @SpringBootApplication
    @EnableFeignClients
    public class OrderApplication {
    
      public static ApplicationContext applicationContext;
    
      ...
    
    
  • order에서 product 서비스를 호출하기 위해 Feign Client 클래스를 다음과 같이 구현합니다.
    @FeignClient(name="product", url="${api.url.product}")
    public interface ProductService {
      @RequestMapping(method= RequestMethod.PUT, path="/products/{productId}/decreaseStock/{qty}")
      public void decreaseStock(@PathVariable("productId") Long productId,
                                @PathVariable("qty") int qty);
    }
    
  • 마지막으로 Order.java 에서 주문이 완료되기 전에 REST API를 호출하는 로직을 구현합니다. 아래와 같이 @PrePersit를 사용하면 해당 메소드는 order 엔티티가 생성 완료되기 전에 먼저 수행이 됩니다.
    public class Order {
        ...
        @PrePersist
        public void onPrePersist() {
          // ProductService 호출
          OrderApplication.applicationContext.getBean(ProductService.class)
                  .decreaseStock(productId, qty);
      }
    

동기식 연동 테스트

이제 상품 주문을 넣을 때, 상품 재고 변경이 잘 되는지 확인해 보겠습니다.

  # 상품 등록
  ~ ❯❯ http POST localhost:8080/products name="socks" stock=10
  ...

  # 상품 등록 결과 확인
  ~ ❯❯ http GET localhost:8080/products 
  ...
  {
    "_links": {
        "product": {
            "href": "http://localhost:8080/products/1"
        },
        "self": {
            "href": "http://localhost:8080/products/1"
        }
    },
    "name": "socks",
    "stock": 10
  }

  # 상품 주문
  ~ ❯❯ http POST localhost:8081/orders productId=1 qty=2
  ...

  # 상품 재고 상태 확인
  ~ ❯❯ http GET localhost:8080/products 
  ...
  {
    "_links": {
        "product": {
            "href": "http://localhost:8080/products/1"
        },
        "self": {
            "href": "http://localhost:8080/products/1"
        }
    },
    "name": "socks",
    "stock": 8
  }

만약, 주문하려는 상품이 없거나 재고가 부족하다면 어떻게 될까요?

  # 없는 상품 주문
  ~ ❯❯ http POST localhost:8081/orders productId=2 qty=2
  ...

  # 부족한 상품 주문
  ~ ❯❯ http POST localhost:8081/orders productId=1 qty=20
  ...

잘 되는 것 보이시쥬? ^^

이제까지 마이크로서비스간 데이터 동기화를 위한 비동기 통신과 동기 통신 2가지 방식에 대해 공부하였습니다. 다음 시간에는 좀더 난이도가 있는 취소 기능을 만들어 보겠습니다.


< EOF >