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

Updated:

2차시 교육 ① : 일단 마이크로서비스 만들어보기

지난 모델링 수업에 이어 이제 본격적인 개발 수업에 들어가게 되었습니다. 코딩 수업 내용을 일일히 다 설명하며 언급하기에는 너무 방대하기에 여기서는 코딩 흐름과 작업 결과 위주로만 이야기하겠습니다. 진행하다가 모르는 개념과 용어가 나오면 찾아서 따로 공부를 하시면 크게 도움이 되실 것입니다.

개발 환경

개발을 하려면 당연히 환경이 먼저 준비되어 있어야겠지요.
제가 설치했던 것들은 다음과 같습니다.

1. 개발 도구 설치

  • JDK 설치 : oraclejdk 최신 버전을 설치했습니다.
  • httpie
    • REST API를 테스트하기 위해 필요합니다
    • curl 명령어 또는 postman 같은 도구도 편리하지만 http가 명령어도 짧고, 명령어 콘솔에서 실행하기에 굉장히 편리합니다.
  • VSCode 또는 IntelliJ나 STS 설치
    • 본인이 익숙한 Java 개발 툴을 사용하면 됩니다.
    • 제 경우에는 새로 배우기 시작한 IntelliJ가 좀 재미있어서 IntelliJ를 사용했습니다.
    • 강사님의 경우 Visual Studio를 사용했는데, VSCode는 여러 개의 프로젝트를 동시에 띄울 수 있는 장점이 있지만 패키지 import가 자동으로 안되는 단점이 있습니다.
    • Java 개발에는 아무래도 STS가 제일 편하지만, 터미널 창을 지원하지 않고 동시에 여러개 Console창을 띄우지 못하는 단점이 있습니다.

2. kafka 설치

  • 마이크로서비스간 데이터 송/수신을 위해서는 Rabbit MQ, Kafka와 같은 메시지 브로커가 필요합니다. 활용분야와 예시가 많은 kafka를 사용하여 개발해보도록 하겠습니다.
  • 카프카 설치 방법은 인터넷에 굉장히 많이 소개되어 있습니다만… 일단, 2.xx 버전대 중에서 가장 최신의 것을 설치하기 바랍니다. (구글 검색 ^^) 저의 경우에는 맥북용으로 설치하였는데 처음에 kafka가 동작하지 않아 애를 좀 먹었습니다. 살펴보니 server.properites에 listner 설정이 막혀 있어서 이 부분도 잘 확인하시기 바랍니다. listeners=PLAINTEXT://127.0.0.1:9092 와 같이 되어 있어야 합니다.

3. kafka 연습
카프카가 제대로 되는지 강사님이 알려준대로 한번 테스트를 해 봅니다.

  • 토픽생성
    ~ > cd [kafka설치 디렉토리]
    ~/kafka > bin/kafka-topics.sh --bootstrap-server http://localhost:9092 --topic my-topic --create --partitions 1 --replication-factor 1
    
  • 토픽 리스트 보기
    ~/kafka > bin/kafka-topics.sh --bootstrap-server http://localhost:9092 --list    
    
  • 새로운 터미널 창에서 kafka producer 연결 후 메세지 publish
    ~/kafka > bin/kafka-console-producer.sh --broker-list http://localhost:9092 --topic my-topic
    

  • 새로운 터미널 창에서 kafka consumer 연결 후 메세지 subscribe 확인
    ~/kafka > bin/kafka-console-consumer.sh --bootstrap-server http://localhost:9092 --topic my-topic --from-beginning
    


    producer에서 작성한 메시지가 consumer에 잘 나타납니다. 오호 신기하여라~~~

DDD 모델링하기

개발환경이 마련되었다고 이제 코딩을 바로 시작할 수 있는 게 아닙니다.
앞에서 이벤트 스토밍을 통한 마이크로서비스 도출 모델링을 경험해보았지만, 코딩을 위해서는 보다 세부적인 명세가 필요합니다. 이를 서비스 스펙이라고합니다.
서비스 스펙은 도메인 주도 설계(Domain-Driven Design)라고 불리우는 모델링 과정을 통해 정의될 수 있습니다. 서비스 스펙 정의하는 방법은 “서비스 스펙 정의” 블로그 내용을 참고해서 보십시오.

회사에서 제공한 과정에서는 이벤트 스토밍 결과만으로도 바로 소스코드를 자동 생성해주는 msaez라는 툴을 사용하였기에, 이벤트 스토밍 결과를 가지고 어떻게 도메인 다이어그램을 그려야할지는 수업과 별개로 공부를 해야 했습니다.
시간이 좀 걸리겠지만 단순히 따라 하는 것보다는 인프런(http://inflean.com)과 같은 학습 사이트에서 JPA 및 DDD 수업을 들어보시거나 관련 도서를 보시는 것이 개념 정립에 크게 도움이 됩니다.

여하간 수업에서 코딩 실습을 위해서 사용한 비즈니스 시나리오는 “Shopping” 업무였으며, 다음과 같이 Order와 Delivery, Product 3개의 마이크로서비스를 사용하기로 하였습니다.

각 마이크로서비스별로 Aggregate이 가져야 하는 속성은 다음과 같이 정의하였습니다.

Order Delivery Product
•orderId
•productId
•productName
•userId
•quantity
•price
•deliveryId
•orderId
•userId
•deliveryAddress
•productId
•productName
•price
•stock


스프링부트 프로젝트 만들어 보기

이제 본격적으로 아주 심플한 마이크로서비스를 구현해보겠습니다.

프로젝트 파일 생성

맨처음 따라 해 본 것은 Spring Initializer를 활용하여 CNA를 구현해보는 것입니다. 제 경우는 먼저 Product 마이크로서비스를 구현해보았습니다. https://start.spring.io 사이트에서 Maven Project 및 Java 버전을 선택하고 Dependency는 다음과 같이 Spring Data JPA, Rest Repository, H2를 택한 후 generate를 눌러줍니다.

내려받은 파일을 unzip한 후 IntelliJ나 VSCode에서 Maven 프로젝트로 열어 봅니다. 프로젝트 탐색기에서 아래와 같은 구조로 파일이 열립니다.

(maven 프로젝트를 경험해보신 분들은 다들 이정도는 이해하시겠지요. 만약 아래의 디렉토리 구조가 이해되지 않으신다면 먼저 spring 개발 관련 공부를 따로 하셔야 합니다.)

이제 pom.xml을 확인해 봅니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>product</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>product</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-rest</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Aggregate 구현

이제 Product 마이크로서비스의 Aggregate을 구현해 보도록 합니다.

[ Product.class ]

package com.example.product;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Product {
  @Id @GeneratedValue
  Long id;
  String name;
  int stock;
}

위에서 Product 클래스는 Enitity 클래스이므로 get/set 메소드를 구현해야 하는데, 이는 아래와 같이 직접 구현하셔도 되고 아니면 lombok을 이용해도 됩니다.

① 직접 get/set 메소드를 구현하는 경우

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getStock() {
        return stock;
    }

② lombok
lombok을 사용하기 위한 먼저 lombok을 설치 및 설정이 필요합니다. PC 환경 및 선택한 개발도구에 따라 조금씩 달라지므로, 본인 환경에 맞게 구글링으로 검색하여 설정을 마치기 바랍니다.
lombok 환경 설정이 완료되면 pom.xml 파일에서 dependencies 내에 다음과 같이 추가해야 실제로 사용이 가능합니다.

  <!--https://mvnrepository.com/artifact/org.projectlombok/lombok-->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>

그리고 아래와 같이 Product 클래스에 @Getter @Setter 어노테이션을 설정해주면 위와 같이 복잡한 get~ /set~ 메소드를 전부 코딩할 필요가 없어집니다.

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter@Setter
public class Product {
  

Command 구현하기

Product 마이크로서비스의 Command는 바로 REST API를 구현하는 것입니다. REST API 구현을 위해 Spring MVC에서 보통 사용하는 Controller를 사용하지 않고 Spring에서 제공하는 Repository 패턴을 사용하여 다음과 같이 Product의 Repository를 생성할 수 있습니다.

package com.example.product;
import org.springframework.data.repository.CrudRepository;

public interface ProductRepository extends CrudRepository<Product, Long> {
}

스프링 부트 실행

이제 바로 Product 마이크로서비스를 실행해보겠습니다. 그 전에 먼저 application.yml을 만들어주어야 합니다. resource 디렉토리 밑에 있는 application.properties 파일을 yml로 변경해주고 다음과 같이 내용을 작성합니다.

spring:
  profiles: default
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true
  h2:
    console:
      enabled: true
logging:
  level:
    org.hibernate.type: trace
    org.springframework.cloud: debug
server:
  port: 8080

터미널 창을 하나 열고, product 프로젝트 파일이 있는 디렉토리로 이동한 다음 다음과 같이 명령어를 입력합니다.

~/product > mvn spring-boot:run    

또다른 터미널 창을 열어 제대로 실행되는지 테스트 해봅니다.

> http GET http://localhost:8080 

다른 REST API들도 제대로 동작되는지 테스트 해보았습니다.

> http http://localhost:8080/products
> http POST localhost:8080/products name="치킨" stock=10
> http "http://localhost:8080/products/1"
> http PATCH "http://localhost:8080/products/1" stock=15
> http DELETE "http://localhost:8080/products/1"
> http "http://localhost:8080/products/1"

모두 잘 됩니다. :-)
Product라는 Entity를 가진 아주 기본적인 마이크로서비스?를 만들는데 성공했습니다 !!

이벤트 구현

그러나 아직 마이크로서비스라 불리우기에는 부족합니다. 바로 핵심인 이벤트 구현이 빠져있기 때문입니다.
기존 RDB 기반 코딩에서는 변경사항이 발생하면 DB에 데이터를 반영하는 것으로 할 일이 끝나지만, 마이크로서비스에서는 타 서비스에게 데이터의 변경, 곧 이벤트가 발생했음을 전파하는 것이 필요합니다.
즉, 이벤트는 다른 서비스에게 변경사항을 알려주고 타 서비스에서 필요한 데이터를 전달하기 위한 목적으로 구현됩니다.

이벤트 구현이 왜 중요한지 이야기하려면 MSA의 특징이 무엇이고 장점이 무엇인지 개념부터 다시 설명해야 하므로 생략합니다. ^^ 일단 구현에 들어가 봅니다.

먼저 이벤트를 전달하기 위해서는 구체화된 형태가 필요한데 Product 클래스 생성 or 변경시 속성값을 담는 용도로 POJO 클래스를 선언하게 됩니다.

이벤트 전달용 POJO 클래스 선언

[ ProductChanged.java ]

package com.example.product;
public class ProductChanged {
    String eventType;
    Long productId;
    String productName;
    int productStock;
    
    public ProductChanged(){
        this.eventType = this.getClass().getSimpleName();
    }
    
    // get/set 메서드 선언해줘야 합니다. 
    
}

위와 같이 선언된 클래스는 ProductChanged라는 이벤트 메시지로 사용됩니다.

스프링 클라우드 스트림

스프링에서는 마이크로서비스를 구현하기 위한 라이브러리로 Spring Cloud와 Spring Cloud Stream을 제공합니다. 스프링 클라우드 스트림은 외부 시스템에 연결할 수 있는 애플리케이션을 신속하게 구축할 수 있는 경량의 마이크로서비스 프레임워크로 Apache Kafka 또는 RabbitMQ 등을 사용하여 Spring Boot 어플리케이션간에 메세지를 주고 받을 수 있게 합니다.

Spring Cloud Stream Application이 Kafka 바인더를 사용할 수 있도록 다음과 같이 라이브러리를 pom.xml 에 추가하였습니다.

<!-- kafka streams -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

또한, Spring Cloud 는 spring-boot 버전에 대한 종속성이 있기 때문에 pom.xml 에 다음과 같이 dependency를 추가해 주어야 하며, ${spring.cloud-version}에 해당하는 실제 버전 값은 properties 내에 변수로 처리하는 것이 편리합니다.

가장 최신의 spring-cloud.version은 2020.0.x 이지만, kafka 2.x와 호환하기 위해서는 Downgrade가 필요합니다. (Hotxton.SR3 사용)
또한 spring-boot 버전도 같이 다운을 시켜주어야 합니다. (2.5.6 –> 2.2.5.RELEASE로 변경)

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.5.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>11</java.version>
    <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Spring Cloud를 어떻게 사용하고 spring-cloud.version을 매핑할지에 대한 정보는 스프링 클라우드 Site (https://spring.io/projects/spring-cloud) 에서 확인할 수 있습니다.

출처 : Spring Cloud Stream이란?

카프카 바인딩

Product 마이크로서비스가 메시지를 보내기 위해서 먼저 spring cloud stream를 사용한 input, output 채널 선언이 필요합니다. 아래와 같이 스트림 처리를 위한 Processor를 선언합니다.

package com.example.product;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface StreamProcessor {
    String INPUT = "input";
    String OUTPUT = "output";
    @Input(INPUT)
    SubscribableChannel inboundTopic();
    @Output(OUTPUT)
    MessageChannel outboundTopic();
}

그리고 ProductApplication이 이 Processor를 사용할 수 있도록 바인딩하기 위하여 @EnableBinding 어노테이션을 추가합니다.

import org.springframework.boot.SpringApplication;

import com.example.product.StreamProcessor;

@SpringBootApplication
@EnableBinding(StreamProcessor.class)
public class ProductApplication {
    public static ApplicationContext applicationContext;  public static void main(String[] args) {
        applicationContext = SpringApplication.run(ProductApplication.class, args);
    }
}

application.yaml 파일에는 스트림과 실제 메시지 브로커인 kafka가 연결되도록 다음 설정을 추가 합니다.

spring:
  cloud:
    stream:
      kafka:
        binder:
          brokers: localhost:9092
      bindings:
        input:                # input channel
          group: product      # consumer group
          destination: shop  # listening topic name
          contentType: application/json
        output:             # output channel
          destination: shop  # publishing topic name
          contentType: application/json   

이벤트 메시지 발행

자, 이제 모든 준비가 되었고 마지막으로 다시 Product 엔티티 클래스로 돌아갑니다. 위에서 Product 엔티티가 신규 생성되거나 변경될 경우 ProductChanged라는 이벤트 메시지를 브로커에게 보내주어야 한다고 했습니다. 이렇게 메시지를 보내어 주기 위해서 여러가지 방법이 있겠지만, Product 엔티티 스스로 자신의 생명주기를 인지하고 메시지를 발행하는 것이 편리합니다.

먼저 아래와 같이 Product.java 클래스 안에 ProductChanged라는 이벤트를 발송하는 로직을 추가합니다.

[ Product.java ]

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Product {
  
  public void eventPublish(){
    ProductChanged productChanged = new ProductChanged();
    productChanged.setProductId(this.getId());
    productChanged.setProductName(this.getName());
    productChanged.setProductStock(this.getStock());
    ObjectMapper objectMapper = new ObjectMapper();
    String json = null;
    try {
        json = objectMapper.writeValueAsString(productChanged);
    } catch (JsonProcessingException e) {
        throw new RuntimeException("JSON format exception", e);
    }

    StreamProcessor processor = ProductApplication.applicationContext.getBean(StreamProcessor.class);
    MessageChannel outputChannel = processor.output();
    outputChannel.send(MessageBuilder
      .withPayload(json)
      .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
      .build());
    System.out.println(json);
  }
}

이제 이 메소드가 Product의 생명주기에 따라 실행이 되도록 방금 작성한 eventPublish 메소드 위에 @PostPersist 또는 @PostUpdate 어노테이션을 붙여 줍니다. @PostPersist는 엔티티가 새로 생성된 경우, @PostUpdate는 엔티티 속성이 수정된 경우 해당 메소드를 자동 실행시켜 줍니다.


import javax.persistence.PostPersist;

public class Product {
  
  @PostPersist @PostUpdate
  public void eventPublish(){
    
  }
}

이벤트 메시지 점검

메시지가 제대로 발송되는지 확인하기 위해 스프링 부트를 실행하고 상품을 등록해 봅니다.

  • 스프링 부트 실행
    ~/.../product > mvn spring-boot:run
    
  • POST 메소드로 Product 생성
    ~ > http POST localhost:8080/products name="socks" stock=10
    
  • Kafka Consumer를 통해 shop 토픽을 모니터링해봅니다.
    ~ > cd [kafka 설치폴더]
    …/kafka_2.13-2.8.0 > bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic shop --from-beginning
    

이상으로 초간단? 마이크로서비스를 구현해보고 이벤트 메시지를 브로커에 전달하는 것을 실습해보았습니다. 실제 실습하는 데에는 4시간 정도 걸린 것 같은데 글로 옮겨적자니 며칠 걸립니다. 헉헉… 😂

물론 발행된 이벤트를 실제로 다른 마이크로서비스에서 수신하여 처리하는 것도 해보아야 하고 아직 남아 있는 숙제들이 많이 있습니다.

다음 포스팅에 게재하도록 하겠습니다.

< EOF >