마이크로 서비스의 분리 - TheOpenCloudEngine/uEngine-cloud GitHub Wiki
기존의 주문과 고객서비스는 하나의 팀에서 관리를 해왔으나, 조직이 성장하여 고객관리시스템을 주문서비스와 분리하여 다른 개발팀에서 운영하기로 결정되었다.
따라서, 다음과 같은 설계 이슈가 존재한다:
- 주문을 요청한 주문자 정보(Customer)와 주문정보(Order)가 HATEOAS relation 으로 연결되어야 함.
- 주문이 벌어지는 마이크로 서비스는 주문 서비스이므로, 주문서비스에서 고객 서비스를 그 순간에 호출하여 고객의 포인트에 합산되어야 한다. (이때 데이터 정합성 / 트랜잭션 이슈가 있을 수도 있다)
- 새 App 생성: OCE 에서 새로운 App을 생성하고, App의 이름은 "customer-service" 로 생성한다 (물론, 도메인 명이 겹치는 경우, 앞에 prefix 를 주어 분리해준다)
- 기존 order-service 에 포함되었던 Customer.java 와 CustomerRepository.java 를 가진 클래스를 customer-service 로 옮겨 온다.
- 이때 Customer.java 클래스는 주문서비스와 참조가 벌어지는 Bounded Context Class 가 된다. 그러니까, 주문서비스에 Relation을 형성하려면, 주문서비스와 공통으로 참조할 수 있는 영역에 Customer entity 가 존재해야 한다. 따라서 두 프로젝트가 같이 참조 가능한 공통 참조 프로젝트가 존재해야 한다.
- customer-service 의 클래스를 order-service 가 참조할 수 있도록 하기 위하여 jar 라이브러리 저장소가 필요하다. 자바 프로젝트에서는 maven nexus server 를 라이브러리 저장소로 이용한다. maven nexus server 는 OCE 에 기본 서비스로 탑재되어있으므로, 탑재된 주소로 연결 할 수 있도록 customer-serviced의 pom.xml 에 distribution 절을 다음과 같이 설정해준다:
<distributionManagement>
<repository>
<id>releases</id>
<url>http://nexus.pas-mini.io/repository/maven-releases</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>http://nexus.pas-mini.io/repository/maven-snapshots</url>
</snapshotRepository>
</distributionManagement>
그리고 nexus repo 로 업로드가 가능하려면 계정 설정이 필요하다. /user-home/.m2/settings.xml 을 편집하여 계정 정보를 제공한다:
<settings>
<servers>
<server>
<id>releases</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>
</servers>
</settings>
- admin/admin123은 nexus 기본 어드민 계정이다. 추후에는 사용자를 추가하여 사용할 것.
- customer-service 의 deploy
mvn deploy
- deploy 된 customer-service 를 order-service 에서 참조한다:
<dependencies>
<dependency>
<groupId>org.uengine</groupId>
<artifactId>customer-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
...
<repositories>
<repository>
<id>ossrh</id>
<url>http://nexus.pas-mini.io/repository/maven-snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
기존에 고객서비스의 Customer.java 와 주문서비스의 Order.java 는 JPA 릴레이션을 통하여 조합되었다. 조합된 서비스는 REST 서비스 인터페이스 상에서는 서로 URI 를 통하여 연결되었다 (HATEOAS). 하지만, 기존 서비스는 REST 인터페이스 상으로 분리될 수 있는 준비는 되었으나, 하나의 뭉쳐진 서비스 노드 상에서 제공되었다. 여기서는 이 두 서비스를 각각의 서비스로 분리할 예정이며, 이를 위하여 기존 JPA 릴레이션 대신, customerId 값으로만, 실제 연결을 할 것이며, Metaworks4 가 제공하는 @RestAssociation 애노테이션을 통하여 두 엔티티 클래스를 연결해야 한다 (아직 JPA를 통하여 분리된 서비스간에 통합은 기본 Spring Data Rest 에서 제공되지 않는다!)
- Order.java 기존에 JoinColumn 으로 선언하였다 foreign key 컬럼인 customerId 를 Order.java 내에 직접 선언해준다. Customer 의 primary key 는 Long 타입이므로 Long customerId 로 선언해준다:
Long customerId;
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
다음과 같은 URI relation 을 통한 http 호출로써 customerId 가 자동으로 바인딩 되게 해주기 위해서는 @RestAssociation 애노테이션을 사용한다.
http localhost:8080/orders customer="http://localhost:8080/customers/고객ID" item="http://localhost:8080/items/TV" qty=5
Order.java 의 기존 Customer 필드 선언을 @RestAssociation 을 사용하도록 전환한다. 이때 path 값은 고객서비스 uri 패턴을 입력해주고, joinColumn 으로 선언한 field 명을 joinColumn 에 지정해준다:
@Transient
@RestAssociation(serviceId = "customer-service", path="/customers/{customerId}", joinColumn = "customerId")
Customer customer;
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
그리고, Customer 클래스를 연결할때 연결된 Customer 는 order-service 의 데이터베이스 내에는 만들지 않을 것이므로 @Transient 로 선언한다.
도메인 클래스들에 대한 설정은 위에서 설명한 대로다. 이러한 REST 기반 연계 기능들을 실제로 적용하려면, 다음의 두가지 설정이 필요하다.
- OrderRepository 의 유형을 Metaworks의 MultitenantRepository를 사용하도록 전환한다:
- OrderRepository.java
public interface OrderRepository extends MultitenantRepository<Order, Long> {
}
- MultitenantRepository 가 실제로 동작할 수 있도록 Application.java 의 설정에 다음을 추가해준다:
@EnableJpaRepositories(repositoryBaseClass = MultitenantRepositoryImpl.class, basePackageClasses = {Order.class, Customer.class})
public class Application extends Metaworks4BaseApplication {....}
앞서 주문서비스와 마찬가지로 Spring-boot:run 명령이나 IDE 설정으로 서비스를 기동한다. 이때는 기존 주문서비스와 다른 포트로 기동시킨다: (서비스를 OCE 엔진에 곧바로 올린 경우라면, 그냥 해당 route 로 접속한다)
mvn spring-boot:run -Dserver.port=9001
그런 후, 고객을 등록한다:
http localhost:9001/customers firstName="jinyoung" lastName="jang" point=0
해당 고객으로 주문을 한다:
$ http localhost:8080/orders customer="http://localhost:9001/customers/4" qty=5
{
"_links": {
"customer": {
"href": "http://192.168.0.47:9001/customers/4"
},
"item": {
"href": "http://localhost:8080/orders/1/item"
},
"order": {
"href": "http://localhost:8080/orders/1"
},
"self": {
"href": "http://localhost:8080/orders/1"
}
},
"customer": {
"firstName": null,
"lastName": null
},
"customerId": 4,
"qty": 5
}
결과를 살펴보면, customer 에 대한 link 정보가 분리된 마이크로서비스의 리소스 주소인 "http://192.168.0.47:9001/customers/4" 로 입력된 것을 확인할 수 있다. 여기서 192.168.0.47 주소는 EUREKA 서버를 통하여 customer-service 가 등록된 내부주소를 리턴했기 때문에 해당 주소로 표시된 것을 확인할 수 있다.