클라우드 네이티브 애플리케이션 부트캠프 알림

티스토리 뷰

이번 장에서는 마이크로서비스 패턴 개발을 지원하는 Axon Framework에 대해 배워 보겠습니다. 

실습으로 내용이 너무 길어져 몇개의 하위 글로 나누어서 올리겠습니다.   

Axon Framework는 Command Handler, Event Sourcing with Event Store, Event Handler, Query Handler로 구성되어 있습니다. 

[출처]  https://docs.axoniq.io/reference-guide/architecture-overview

[1편 마이크로서비스 패턴 이해]에서 Command, Event, Query에 대해 설명했지만 명확히 용어를 다시 정리부터 하겠습니다. 

또한 [DDD 핵심만 빠르게 이해하기]의 Event Storming에서도 Command와 Event라는 용어가 등장하는데 다른 의미로 해석될 수 있을 듯 해서 함께 정리해 보겠습니다. 

개념 DDD 마이크로서비스 패턴 통합정리
Command - Domain Event를 발생시키는 명령
- 현재형으로 기술
Data Write(CUD: Create, Update, Delete) 요청 Domain Model(Data) 상태를 변화시키는 요청
예) 제품 주문 요청
Event - Command를 수행하여 발생한 결과
- 제품/서비스가 사용자에게 주는 가치
- 과거형으로 기술
Command를 수행하여 발생한 결과  Domain Model(Data) 상태의 변화 결과
예) 제품 주문이 성공적으로 접수됨
Query Command 중 조회 요청 Data Read 요청 Domain Model(Data) 최종 상태에 대한 조회 요청
예) 주문된 제품 진행상황 조회 요청

따라서 Axon Framework의 각 컴포넌트는 아래와 같이 정리될 수 있습니다. 

개념 기능 Axon Framework에서의 구현
Command
Handler
Domain Model 상태를 변화 시키는 요청을 처리 Aggregate의 CommandHandler로 구현
Event Sourcing
with Event Store
Domain Model의 최종 상태를 Event Store에 기록된
Event의 Replay를 통해 계산하고 새로운 Event를 저장 
Aggregate의 EventSourcingHandler로 구현
Event Handler Domain Model 상태의 변화 결과를 처리 EventHandler class로 구현
Query Handler Domain Model 최종 상태의 조회 요청 처리 QueryHandler class로 구현

Axon Framwork를 이용한 전체적인 처리 프로세스는 아래와 같습니다. 

그럼 이 프로세스대로 간단한 예제를 통해 실습해 보도록 하겠습니다. 

예제는 Divide and Conquer(복잡하고 어려운 문제는 잘게 쪼개서 각각 해결한다)라는 유명한 명제의 예제로 많이 거론되는 '코끼리 냉장고 넣기'입니다. 

 

예제 소스를 아래 repository에 있습니다. 실습은 Command, Query, 코끼리 삭제의 순서대로 개발하면서 설명하겠습니다. 

https://github.com/happykubepia/axonsample.git


새 프로젝트 생성 및 셋업

1) 새 프로젝트 생성 

- 프로젝트 기본 구조 생성

IntelliJ CE가 Spring boot를 기본적으로 지원 안하기 때문에 Spring Initializer를 이용하여 생성 합니다. 

http://start.spring.io를 접속합니다. 

아래 그림 대로 기본정보와 Dependency를 추가 합니다. 

하단의 [EXPLORE] 버튼을 클릭하여 미리 생성될 프로젝트 구조를 볼 수 있습니다. 

[GENERATE] 버튼을 눌러 zip 파일로 PC의 작업 디렉토리(실습에서는 {사용자홈 디렉토리}/workspace)에 저장 합니다. 

zip파일 압축을 해제 합니다. '{사용자홈 디렉토리}/workspace/axon-elephant'라는 디렉토리 밑으로 파일들이 생성됩니다. 

IntelliJ를 실행하고 [File]-[Open]을 클릭하고 '{사용자홈 디렉토리}/workspace/axon-elephant' 디렉토리를 선택 합니다. 

- 프로젝트 셋업: build.gradle

build.gradle 파일에 axon framework, Swagger, Apache utilities 라이브러리를 추가 합니다. 

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//axon framework
	implementation 'org.axonframework:axon-spring-boot-starter:4.9.1'
	//Swagger
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
	//Apache utilities
	implementation 'org.apache.commons:commons-lang3:3.14.0'
}

그리고 아래와 같이 빌드 할 때 생성될 jar파일 이름을 지정합니다. 사실 안 해도 되지만 나중에 k8s에 배포할 때 편하기 위해서 입니다.  

bootJar {
	archiveFileName = "axon-elephant.jar"
}

참고로 archiveFileName 값이 없으면 {settings.gradle에 정의된 rootProject.name} + "-" + {build.gradle에 정의된 version}.jar파일로 생성 됩니다. jar파일은 build/libs 디렉토리에 생성 됩니다. 

테스트 해 보시려면 아래 명령을 프로젝트 루트 디렉토리에서 수행 하십시오. 

axon-elephant> ./gradlew build -x test

 

2) 프로젝트 셋업

- 프로젝트 셋업: src > main > resources > application.properties 

아래 내용을 붙여 넣습니다. 

server.port=${SERVER_PORT:19080}
spring.application.name=axonsample
#spring.main.allow-bean-definition-overriding=true
#spring.main.allow-circular-references=true

#DataSource
spring.datasource.url=jdbc:mysql://${DB_SERVER:localhost}:${DB_PORT:3306}/${DB_NAME:elephantDB}?useUnicode=true&characterEncoding=utf-8&createDatabaseIfNotExist=true
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD:P@ssw0rd$}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#jpa configuration
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.hibernate.ddl-auto=update 
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

#Axon Server
axon.serializer.general=xstream
axon.axonserver.servers=${AXON_HOST:localhost}:${AXON_PORT:18124}
#axon.eventhandling.processors.name.mode=tracking

# swagger
springdoc.packages-to-scan=org.axon.controller
springdoc.paths-to-match=/**

# Logging
logging.level.org.axon=info
logging.level.org.axonframework=info
logging.pattern.console=%clr(%d{MM/dd HH:mm:ss}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}){magenta} %clr(---){faint} %clr(%-40.40logger{39}){cyan} %clr(%m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}){faint}

각 셋팅의 의미를 설명하겠습니다. 

  • server.port: Application이 외부의 요청을 받는 포트 번호 입니다. 실행하는 머신의 다른 포트와 충돌하면 안됩니다. 아래 예에서는 'SERVER_PORT}라는 환경 변수값을 먼저 참조하고 값이 없으면 '19080'포트를 쓴다는 의미입니다. 
  • sping.application.name: 어플리케이션 이름이며 setting.gradle의 rootProject.name과 동일하게 지정합니다.

그 아래 2개는 사용하지 않는데 아래와 같은 의미 입니다. 

  • spring.main.allow-bean-definition-overriding: Spring Bean으로 정의된 class에 대해 재정의를 허용한다는 의미입니다. 
  • spring.main.allow-circular-references: class 간 상호 참조를 허용한다는 의미입니다. 진짜 특수한 경우 외에는 Loosely Coupling을 위해 허용하지 마십시오. 
server.port=${SERVER_PORT:19080}
spring.application.name=axon-elephant
#spring.main.allow-bean-definition-overriding=true
#spring.main.allow-circular-references=true

Spring Bean이란 용어를 처음 접하시는 분들을 위해 Spring Boot의 기본 용어를 몇가지 이해하고 넘어 가겠습니다. 

참고) Spring Bean 이란 ?
  • Java Bean은 DTO(Data Transfer Object)나 VO(Value Object)와 같이 Getter나 Sette로만 프라퍼티값을 접근할 수 있고, 파라미터가 없는 생성자를 갖는 객체를 의미함 
  • Spring Bean은 구동 시에 생성되고 Spring IOC(Inversion Of Control) 엔진에 의해 관리되는 객체를 의미함 
참고: https://jjingho.tistory.com/10
참고) Spring IOC(Inversion Of Control)이란 ?
  • IOC(제어의 역전)은 어플리케이션에서 사용하는 의존성 객체의 라이프 사이클(생성, 의존성 주입, 폐기)을 개발자가 제어하는 것이 아니라 Spring 엔진이 제어하게 한다는 개념임
  • Class 중 @Bean, @Component로 명시된 class는 자동으로 Spring IOC Container에 등록되어 Spring 엔진이 라이프 사이클을 관리하게 됨 
참고) @Configuration, @Data, @Service, @Repository 어노테이션도 @Component 어노테이션을 포함 하고 있음
 
참고) Entity, DTO, VO 란? 
  • Entity: DB와의 interface에 사용될 데이터 구조를 정의한 객체. JPA사용 시 Table과 매핑됨. 또한 Entity는 각 객체들간에 특정 key로 구별이 됨
    예1) 사용자ID, 사용자명, 연락처 필드를 담고 있는 사용자 객체 
  • DTO(Data Transfer Object): 메소드 또는 어플리케이션 간 통신을 위한 데이터 구조를 정의한 객체. 보통 class명 끝에 DTO를 붙임 
  • VO(Value Object): 각 객체들 간에 구별이 필요 없는 데이터 구조를 정의한 객체
    예1) 우편번호,  시군구, 상세주소 등의 필드를 가지는 주소 데이터 객체
    예2) 리턴코드, 리턴메시지, 리턴 객체등을 담고 있는 리턴 데이터 객체
참고) JPA 란 ?
Java Persistent API의 약자로 물리적인 DB와의 CRUD를 SQL없이 API로 처리할 수 있도록 하는 라이브러리입니다. 
이렇게 물리적인 DB와 어플리케이션의 CRUD를 중계하는 개념을 ORM(Object Relation Mapping)이라고 합니다. 
즉, JPA는 ORM을 Java에서 사용할 수 있도록 만든 라이브러리라고 할 수 있습니다. 
이번 실습에서도 JPA를 사용하여 MySQL에 데이터를 CRUD 합니다.  

 

DB에 대한 정의 입니다. 

  • url: DB서버의 Host, Port, DB명을 지정하고, 파라미터로 charset과 DB자동생성 여부를 지정 합니다. 
  • username, password: DB를 접근할 DB계정명과 암호입니다. 
  • driver-class-name: JDBC 드라이버로 MySQL과 통신하기 때문에 JDBC Driver명을 지정 합니다. 
#DataSource
spring.datasource.url=jdbc:mysql://${DB_SERVER:localhost}:${DB_PORT:3306}/${DB_NAME:elephantDB}?useUnicode=true&characterEncoding=utf-8&createDatabaseIfNotExist=true
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD:P@ssw0rd$}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
참고) DB 계정 관리 
실제 운영환경에서는 root로 DB를 접근하지 않고 DB 계정을 생성하고 접근할 DB에만 권한을 부여합니다. 
그 방법을 알고 싶은 분들은 아래 글을 참고 하십시오.  '6. Database 생성, User 생성 및 권한 부여'를 보시면 됩니다. 

https://happycloud-lee.tistory.com/229

 

JPA(Java Persistent API)에 대한 설정 입니다. 

  • properties.hibernate.show_sql: JPA는 요청 받은 API를 수행하기 위해 'hibernate'라는 모듈을 이용하여 물리적인 DB에 맞는 SQL을 생성합니다. 그 SQL을 콘솔에 표시할 지 여부를 지정 합니다. false로 한 이유는 콘솔에 계속 SQL이 나타나 보기 안 좋아서입니다. 
  • properties.hibernate.format_sql: 생성하는 SQL의 보기 좋게 formatting할지 여부를 지정 합니다. 
  • hibernate.ddl-auto: 개발 시에만 none외의 옵션을 이용하고 실제 운영 시에는 none으로 지정 합니다.   
    • create: Entity에 정의한 구조대로 구동 시마다 테이블을 삭제하고 다시 생성 합니다. 테이블 구조만 삭제하는 것이지 데이터는 보존 됩니다. 
    • create-drop: 어플리케이션 종료 시 테이블을 삭제 합니다. 
    • update: 변경된 내용만 테이블에 반영 합니다. 
    • validate: Entity에 정의한 구조와 테이블 구조가 다른 지 체크만 합니다. 
    • none: 아무것도 수행하지 않습니다.
  • database-platform: DB종류에 따라 사용할 hibernate 엔진의 종류를 지정 합니다.   
#jpa configuration
spring.jpa.properties.hibernate.show_sql=false
spring.jpa.properties.hibernate.format_sql=false
spring.jpa.hibernate.ddl-auto=update 
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

 

Axon 서버에 대한 설정 입니다. 

  • axon.serializer.general: Event Store에 Event를 저장(Serialize)하고 읽는데(Deserialize) 사용할 방식을 지정 합니다. 
    제가 테스트 했을때 xstream 이 외의 값을 지정했을 땐 제대로 동작하지 않았습니다. 방법 아시는 분은 댓글 부탁요^^ 
    • xstream: XML로 Event를 저장하고 읽습니다. 
    • java: java class로 Event 저장과 읽기를 구현할 때 사용합니다. 
    • jackson: Json형식으로 Event를 저장하고 읽습니다. 
  • axon.axonserver.servers: Axon Server의 Host, Port를 지정합니다. 
#Axon Server
axon.serializer.general=xstream
axon.axonserver.servers=${AXON_HOST:localhost}:${AXON_PORT:18124}
#axon.eventhandling.processors.name.mode=tracking

 

Swagger 에 대한 설정입니다. 

  • springdoc.packages-to-scan: API를 찾을 java package명을 입력 합니다. 
  • springdoc.paths-to-match: 탐색된 API에 대한 filetering 조건입니다. 예제에서는 모든 API를 표시 합니다. 
# swagger
springdoc.packages-to-scan=org.axon.controller
springdoc.paths-to-match=/**

 

로깅 라이브리로 사용할 Slf4J(Simple Logging Facade For Java)에 대한 설정 입니다. 

  • logging.level.org.axon: org.axon패키지 하위 class의 로깅 Level을 지정 합니다. Level에는 info, debug, error 가 있습니다. 
  • logging.level.org.axonframework: org.axonframework 패키지는 Axon Framework의 패키지입니다. 즉, Axon Framework에서 제공하는 모든 class에 대한 로깅 Level을 'info'로 하겠다는 겁니다. 좀 더 자세하게 로그를 보려면 'debug'로 변경 하세요.
  • logging.pattern.console: 로깅 format을 지정 합니다. 지정 하지 않으면 기본 format으로 표시 됩니다.  
# Logging
logging.level.org.axon=info
logging.level.org.axonframework=info
logging.pattern.console=%clr(%d{MM/dd HH:mm:ss}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}){magenta} %clr(---){faint} %clr(%-40.40logger{39}){cyan} %clr(%m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}){faint}
참고) Log 기본 포맷 
%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} 
%clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint}
%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} 
%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}

 

3) IntelliJ 환경 설정

- Annotation processing 활성화

Spring Boot는 많은 Annotaion을 사용 합니다. ('@'붙어 있는 것들. 예: @Setter, @Value) 

그림을 참조하여 'Annotaion Processors'메뉴를 클릭한 후 'Enable annotaion processing'을 체크 하세요. 

 

- Typo(오타) 체크 비활성화

주석에 있는 글자의 오타까지 검사하여 불필요한 warning 표시를 하는 걸 방지하기 위해 설정 합니다. 

왼쪽 위 검색 박스에 'typo'를 입력한 후 'Inspections' 메뉴를 클릭합니다. 그리고 우측에서 스크롤을 쭉 내리면 마지막에 나오는 'Typo'가 포함된 옵션 두개를 uncheck합니다. 

 


코끼리 생성, 넣기, 꺼내기  API 작성

그럼 첫번째로 코끼리 생성, 넣기, 꺼내기 API를 구현해 보겠습니다.

전체 프로세스에서 1번 과정입니다.  

 

1)  코끼리 생성, 넣기, 꺼내기 요청: APIController, ElephantService 

- org.axon.controller 패키지 만들기 

아래 그림과 같이 org.axon 하위에 'controller'라는 패키지를 만듭니다. 

 - APIController.java 만들기 

만들어진 'controller'패키지 하위에 'APIController.java'를 만듭니다. 확장자 'java'는 입력하지 않습니다. 

아래 소스를 붙여넣기 하여 만듭니다. 

소스 내용을 보면 대략 파악할 수 있어 자세한 설명은 생략 합니다. 

'ElephantService' class가 없어 아직 에러가 날 것입니다. 

package org.axon.controller;

import lombok.extern.slf4j.Slf4j;
import org.hibernate.annotations.Parameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Order service API", description="Order service API" )
@Slf4j
@RestController
@RequestMapping("/api/v1")
public class APIController {
    private final ElephantService elephantService;

    @Autowired
    public APIController(ElephantService elephantService) {
        this.elephantService = elephantService;
    }

    @PostMapping("/create")
    @Operation(summary = "코끼리 생성 API")
    private ResultVO<CreateElephantCommand> create(@RequestBody ElephantDTO elephant) {
        log.info("[@PostMapping '/create'] Executing create: {}", elephant.toString());

        return elephantService.create(elephant);
    }

    @PostMapping("/enter/{id}")
    @Operation(summary = "냉장고에 넣기 API")
    @Parameters({
            @Parameter(name = "id", in= ParameterIn.PATH, description = "코끼리ID", required = true)
    })
    private ResultVO<String> enter(@PathVariable(name = "id") String id) {
        log.info("[@PostMapping '/enter'] Id: {}", id);

        return elephantService.enter(id);
    }

    @PostMapping("/exit/{id}")
    @Operation(summary = "냉장고에서 꺼내기 API")
    @Parameters({
            @Parameter(name = "id", in= ParameterIn.PATH, description = "코끼리ID", required = true)
    })
    private ResultVO<String> exit(@PathVariable(name = "id") String id) {
        log.info("[@PostMapping '/exit'] Id: {}", id);

        return elephantService.exit(id);
    }

}

 

- ElephantService 초안 만들기  

일단 에러를 없애기 위해 'ElephantService'를 껍데기만 만듭니다. 

package 'org.axon.service'를 만들고 그 하위에 'ElephantService' class를 만듭니다. 

package org.axon.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ElephantService {

    public ResultVO<CreateElephantCommand> create(ElephantDTO elephant) {
        log.info("[ElephantService] Executing create: {}", elephant.toString());

        ResultVO<CreateElephantCommand> retVo = new ResultVO<>();

        return retVo;
    }

    public ResultVO<String> enter(String id) {
        log.info("[ElephantService] Executing enter for Id: {}", id);
        ResultVO<String> retVo = new ResultVO<>();

        return retVo;
    }


    public ResultVO<String> exit(String id) {
        log.info("[ElephantService] Executing exit for Id: {}", id);
        ResultVO<String> retVo = new ResultVO<>();


        return retVo;
    }

}
참고) @Service 어노테이션의 역할
APIController의 소스를 다시 보시면 ElephantService의 객체를 생성자 메소드에서 @Autowired를 이용하여 만들고 있습니다. 
이렇게 @Autowired로 객체를 만드는 class는 Spring Bean 클래스여야 합니다. 
Spring Bean 클래스로 등록하는 방법은 @Bean이나 @Component 어노테이션으로 지정하여야 합니다. 
@Service 어노테이션을 사용하면 Bean class로 등록될 뿐 아니라 Service Layer의 class라는 뜻도 표현할 수 있습니다.  

여전히 에러가 발생 합니다. 소스를 보면 'ResultVO', 'CreateElephantCommand', 'ElephantDTO'가 없기 때문이라는 걸 알 수 있습니다. 

'ResultVO'는 리턴값을 담은 class이고 'CreateElephantCommand'는 요청을 보낼 Command class 입니다. 그리고 'ElephantDTO'는 swagger page에서 요청 받을 데이터 구조를 담은 class 입니다.  

- ResultVO 만들기

먼저 package 'org.axon.vo'를 만들고 그 하위에 'ResultVO'를 작성 합니다. 

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVO<T> {
    private boolean returnCode;
    private String returnMessage;
    private T result;
}

 

- CreateElephantCommand 만들기

package 'org.axon.command'를 만들고 그 하위에 'CreateElephantCommand'도 만듭니다. 

import lombok.Builder;
import lombok.Value;
import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Value
@Builder
public class CreateElephantCommand {
    @TargetAggregateIdentifier
    String id;

    String name;
    int weight;
    String status;
}
중요) @TargetAggregateIdentifier
CreateElephantCommand는 이후 ElephantAggregate로 전달이 됩니다. 
ElephantAggregate는 어떤 코끼리 객체에 대해 Event Replay를 할 지 알아야 합니다. 
따라서 각 코끼리 객체를 구별할 수 있는 Unique key인 'id'를 @TargetAggregateIdentifier 어노테이션으로 지정해 주는 겁니다. 

아마 @TargetAggregateIdentifier를 import 하지 못해 에러가 날 겁니다. 오른쪽 쯤에 있는 코끼리 아이콘을 클릭하여 IntelliJ에 변경 사항을 반영하면 에러가 없어질 겁니다. 

- ElephantDTO 제작

다음으로 package 'org.axon.dto'를 만들고, 그 하위에 ElephantDTO class를 만듭니다. 

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ElephantDTO {
    private String name;
    private int weight;
}

 

- Compile 에러 제거

이제 ElephantService로 돌아가 아래 그림과 같이 새로 만든 class이름 위에 마우스를 올리면 'Import class'메뉴가 나타날 겁니다. 

클릭하여 class를 import 하십시오. 

APIController도 동일하게 필요한 class를 import 합니다. 

오른쪽 위에 있는 컴파일 결과에서 에러가 있으면 안됩니다. 

 

- Axon Configuration 작성 

application.properties의 Axon 설정에서 Event Store에 Event를 저장/조회 하기 위한 방식으로 'xstream'을 지정했었습니다. 

Axon Server의 최신 버전은 Security를 높이기 위해 Serialization을 허용할 package나 class를 지정하도록 하고 있습니다.   

axon.serializer.general=xstream #xstrean, java, jackson

package 'org.axon.config'를 만들고 하위에 'AxonConfig'를 만듭니다.  class명은 바꿔도 상관 없습니다. 

import com.thoughtworks.xstream.XStream;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AxonConfig {
    @Bean
    public XStream xStream() {
        XStream xStream = new XStream();

        xStream.allowTypesByWildcard(new String[] {
                "org.axon.**"
        });
        return xStream;
    }
}
중요) xStrem.allowTypesByWildcard 항목에 package를 정확하게 지정
package명을 제가 가이드 하는것과 다르게 만드시는 분들이 있을 겁니다. 
이 값을 정확하게 바꾸셔야 제대로 돌아 갑니다. 

또 하나 중요하게 해야 할 것은 이 설정을 활성화 시켜야 한다는 것입니다. 

org.axon 밑에 있는 main class인 'AxonElephantApplication'에 '@Import' 어노테이션으로 설정 Spring Bean class를 로딩 합니다.  위에서 Axon config의 class명을 다른 걸로 했다면 이름을 똑같이 맞춰 주셔야 합니다. 

@SpringBootApplication
@Import({ AxonConfig.class })
public class AxonElephantApplication {

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

}

 

- Swagger Configuration 작성

Swagger page가 생성되려면 application.properties 설정과 더불어 Swagger Config class도 만들어 줘야 합니다. 

package 'org.axon.config' 밑에 'OpenAPIConfig'라는 class를 만듭니다. 역시 class 명은 바꾸셔도 됩니다. 

info 하위의 title, descriptoin, version과 externalDocs 하위의 description, url은 적절히 바꾸십시오. 

externalDocs는 없애도 됩니다. 

import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenAPIConfig {
    @Bean
    OpenAPI openAPI() {
        return new OpenAPI()
                .info(new Info().title("Axon framework API")
                        .description("Axon Sample application")
                        .version("v0.0.1")
                        .license(new License().name("Apache 2.0").url("http://springdoc.org")))
                .externalDocs(new ExternalDocumentation()
                        .description("About Application")
                        .url("https://happykubepia.github.io/axonsample"));
    }
}

 

- JPA Repository 작성 

JPA를 사용하므로 application.properties의 JPA 관련 설정과 더불어 DB와의 CRUD를 위한 API class도 만들어 줘야 합니다. 

package 'org.axon.repository'를 만들고 하위에 ElephantRepository라는 interface를 작성 합니다. 

import org.axon.entity.Elephant;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ElephantRepository extends JpaRepository<Elephant, String> {

}
참고) interface 란 ? 

interface는 class가 아닙니다. class는 실제 메소드의 내용까지 구현 하지만 interface는 메소드의 이름과 파라미터만 정의 합니다. 
실제 구현은 그 interface를 구현하는 class에서 합니다. 
좀 더 자세한 내용은 아래 글의 'interface와 abstract class'의 차이를 보십시오. 

https://happycloud-lee.tistory.com/159

참고) 어떻게 JpaRepository라는 것을 확장한 ElephantRepository라는 interface만 만들어도 동작하는가 ?
JPA는 대상 Entity에 대한 CRUD API를 기본으로 제공 합니다.
- findById({primary key})
- findAll()
- save({entity 객체})
- delete({entity 객체})

위 예에서는 Elephant라는 Entity에 대해 기본적인 CRUD API를 제공하게 됩니다. 
Entity 'Elephant' class에 정의된 대로 물리적인 Table도 자동으로 생성하고 entity 내용이 변경되면 스키마도 자동으로 수정해 줍니다. 
이러한 기본 JpaRepository를 사용하려면 확장(extends)한 interface 를 만들어 주기만 하면 됩니다. 
또한, 이 interface에 JPA의 문법대로 메소드를 만들면 자동으로 API를 만들어 동작하게까지 해 줍니다. 
예를 들어 Entity에 'name'이라는 column을 만들고 그 column으로 데이터를 찾고 싶다면 'findByName(String name)'이라는 메소드를 선언만 해주면 됩니다.  

 

- Entity 작성

위 ElephantRepository에서 지정한 Entity 'Elephant'도 만들어 줍니다. 

package 'org.axon.entity'를 만들고 하위에 'Entity'라는 class를 만듭니다. 

import jakarta.persistence.*;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

@Data
@Entity
@Table(name="elephant")
public final class Elephant implements Serializable {
    /*
    Serialize(DB에 저장 시 binary로 변환)와 Deserialize(DB에 저장된 binary 데이터를 원래 값으로 변환)시 사용할 UID
    'serialVersionUID에 마우스를 올려놓고 전구 아이콘 메뉴에서 <Randomly change 'serialVersionUID' initializer> 선택
    */
    @Serial
    private static final long serialVersionUID = 3867844350539085719L;

    @Id     //Primary key 필드를 나타냄
    @Column(name="id", nullable = false, length = 3)
    private String id;

    @Column(name="name", nullable = false, length = 30)
    private String name;
    @Column(name="weight", nullable = false)
    private int weight;

    @Column(name="status", nullable = false, length = 20)
    private String status;
}

 

- 어플리케이션 실행 

드디어 실행할 준비가 되었습니다. 

Run/Debug Configurations를 아래와 같이 만들고 실행 해 보십시오. 

좀 더 자세한 Run/Debug Configuration을 만드는 방법은 이전 글의 'Import Application > Run/Debug Configuration 만들기'를 참고 하세요. 

https://happycloud-lee.tistory.com/278  

아래와 같이 'Service' 탭에서 어플리케이션을 구동 합니다. 

참고) 어플리케이션 구동 안될 때
혹시 어플리케이션 구동이 실패하면 DBeaver에서 자동 생성된 테이블을 모두 삭제 하고 다시 해 보십시오.  


 

- Table 생성 확인

정상 실행 후에 DBeaver에서 Table을 확인해 보면 아래와 같이 여러 테이블이 자동으로 만들어진 걸 볼 수 있습니다. 

'elephant'라는 테이블은 JPA가 Entity 'Elephant'를 참조하여 만든 것이고, 나머지는 JPA가 Axon framework을 위해 만든 테이블들입니다. 

 

- Axon Server 확인 

Axon Server 웹콘솔로 접근합니다. 

http://localhost:18024 

'Overview' 메뉴를 클릭하면 Axon Server의 참여 어플리케이션으로 등록된 것을 볼 수 있습니다. 

 

- Swagger Page 확인

application.properties에서 지정한 포트인 '19080'으로 swagger 페이지를 엽니다. 

http://localhost:19080/swagger-ui/index.html 

API 리스트들이 잘 나타날 것이고 테스트 해 보면 동작 하는걸 확인할 수 있습니다. 


지금까지 전체 프로세스 중 첫번째인 '코끼리 생성, 넣기, 꺼내기 API' 작성을 해 보았습니다. 

다음 편에서 계속 이어 가도록 하겠습니다.  

 


마이크로서비스 패턴 쉽게 개발하기 목차

  1. 마이크로서비스 패턴 이해: Saga, Event Sourcing, API Composition, CQRS 이해
  2. 실습환경 준비
  3. 주문 서비스 테스트 
  4. Axon Framework 이해 
    1. 코끼리 냉장고에 넣기 프로젝트 시작
    2. CommandGateway와 Event Sourcing
    3. EventGateway
    4. QueryGateay
    5. Snapshot
    6. State Stored Aggregate
    7. Event Replay로 조회DB 데이터 복구

아래 주제들은 '마이크로서비스패턴 쉽게 개발하기'라는 제 책에서 만나실 수 있습니다. 

  • 멀티 모듈 프로젝트 작성
  • 신규 주문 정상처리 프로세스 구현 
  • 배송 상태 변경 및 재고 증감 처리
  • 신규 주문 보상처리 프로세스 구현
  • 주문 수정 정상처리 프로세스 구현
  • 주문 수정 보상처리 프로세스 구현
  • 주문 삭제 정상처리 프로세스 구현
  • 주문 삭제 보상처리 프로세스 구현
  • API Composition 패턴과 CQRS 패턴

책 한번 내겠다는 평소의 꿈을 실현하기 위해 이번에 전자책을 내게 되었습니다. 

제 꿈을 응원하신다는 마음으로 전체 내용을 공유하지 않는것을 양해해 주시고 

구매까지 해 주시면 더욱 감사하겠습니다. 

https://happycloud-lee.tistory.com/notice/291

 

[마이크로서비스패턴 쉽게 개발하기] 전자책 출간

안녕하세요? 온달입니다. 그동안 몇 번 시도하다 포기했던 책내기에 드디어 성공 했습니다. 책 제목은 '마이크로서비스패턴 쉽게 개발하기'입니다. 2024년 2월 19일 부터 교보문고, Yes24, 알라딘 등

happycloud-lee.tistory.com

 

댓글

클라우드 네이티브 애플리케이션 부트캠프 알림