본문 바로가기

도서/스프링으로하는 마이크로서비스 구축

공조 마이크로서비스

이전에 작성한 포스팅에서는 마이크로서비스의 장단점과 디자인패턴에 대해서 정리했습니다. 이번 포스팅에서는 해당 쳅터의 핵심 내용들에 대해 정리해보려고 합니다.  모든 소스코드는 오픈되어 있어서 누구나 참고할 수 있도록 되어있습니다!

 


골격 마이크로서비스

 

기본 적인 틀은 위 이미지와 같으며, 각 프로젝트의 역할을 정리하면 다음과 같습니다. 

  • api 
    • API 정의 - Java Interface를 활용해서 RESTFul API를 설명합니다. 
    • Model 정의 - 각 요청과 응답에 매핑되는 클래스를 정의합니다.
  • util 
    • 마이크로서비스가 공유할 헬퍼 클래스를 정의합니다. 
  • microservices 
    • 각 마이크로서비스의 집합으로 두가지 케이스로 분류됩니다. 
      • 복합 서비스 - 발신 요청을 처리하는 통합 컴포넌트를 정의합니다. 
        • product-composite-service :7000(port)
      • 핵심 서비스 - 복합 서비스가 요청할 대상인 컴포넌트를 정의합니다. 
        • product-service :7001(port)
        • review-service :7002(port)
        • recommendation-service :7003(port)

Gradle에서 여러 개의 관련 프로젝트를 하나로 빌드하는 방법

root 경로에 setting.gradle 파일을 생성하고 다음과 같이 함께 빌드할 프로젝트를 명시합니다. 

include ':api'
include ':util'
include ':microservices:product-service'
include ':microservices:review-service'
include ':microservices:recommendation-service'
include ':microservices:product-composite-service'

 

각 프로젝트의 build.gradle에서 build와 관련된 내용을 작성해 두면 root level에서 gradle build를 했을 때 모든 프로젝트가 같이 빌드됩니다. 이때 api, util 프로젝트는 microservce내 모든 프로젝트에서 사용할 라이브러리 프로젝트로써 역할합니다. 때문에 microservcei 프로젝트 내에서 사용할 수 있어야 하는데, 각 프로젝트 내 build.gradle에서 다음과 같이 설정 정보를 수정합니다. 

dependencies {
	implementation project(':api')
	implementation project(':util')
    ...
}

마이크로서비스에서 버전 의존성을 갖도록 하는 것의 중요성

교재에서 설계한 골격 마이크로서비스 프로젝트는 빌드와 배포를 단순하게 하기 위해서 api, util을 멀티프로젝트 빌드에 포함시켰습니다. 하지만 DevOps 관점에서는 마이크로서비스 아키텍처에서 각 마이크로서비스가 자체적으로 버전을 선택할 수 있도록 버전 의존성을 갖게 하는 것이 바람직합니다. 

  • 각 마이크로서비스가 자체적으로 버전을 선택할 수 있으면, 독립적으로 마이크로서비스를 배포할 수 있으며, 이는 다른 마이크로서비스와 상관없이 특정 마이크로서비스의 업데이트 또는 롤백이 가능해집니다. 
  • 마이크로서비스간의 의존성이. 각마이크로서비스 자체에서 관리되면, 서비스의 의존성이 서로 격리되어 한 마이크로서비스의 변경이 다른 서비스에 영향을 최소화할 수 있습니다. 
  • 또한, 해당 마이크로서비스의 기술 스택을 자유롭게 선택할 수 있고, 이는 새로운 기술을 도입하거나 기존 기술을 업그레이드할 때 유연성성을 제공합니다.

각 마이크로서비스가 각각의 api, util 프로젝트에 대해 버전 의존성을 갖는 방법은 다음과 같습니다. 이 방법은 각 프로젝트의 build.gradle이 설정된 메이븐 저장소에서 배포된 dependecy를 직접 참조하는 방법으로 api, util의 버전을 선택해서 프로젝트에 종속시킬 수 있습니다. 

dependencies {
	implementation 'com.example:api-project:1.2.31'
	implementation 'com.example:util-project:1.3.12'
    ...
}

 

이 방법이 가능하기 위해서는 다음과 같은 과정이 필요합니다. 

  • api, util 프로젝트의 버전별 코드 수정
  • build.gradle에서 version 명시 
version = '1.2.31'
  • gradle 빌드 
  • 지정한 maven repository에 Artifact 배포 

좀 더 나아가서 지정한 maven repository에 Artifactf를 배포한다는 것의 의미에 대해 알아봅시다. 먼저, Artifact라는 것은 소프트웨어 개발 및 빌드 과정에서 생성되는 산출물을 의미하며, 쉽게 말해 소스코드를 컴파일하고 빌드한 결과물을 의미합니. 이 케이스에서는 api, util 프로젝트를 jar 파일로 빌드하고 메이븐 레포에 배포하는 것을 의미합니다. 더 나아가 메이븐 레포지토리에 배포를 한다는 것은 자연스럽게 원격 서버에 배포한다고 예상하셨을 것 같습니다. 

 

메이븐 레포는 빌드된 Artifact 를 저장하고 관리하는 중앙 저장소로 api, util과 같은 프로젝트를 빌드해서 원격 레포에 배포하는 것이 가능합니다. 많은 조직에서는 Nexus Repository Manager와 같은 소프트웨어를 사용해서 자체 메이븐 레포지토리르 호스팅 합니다. 즉, 독립적인 메이븐 레보지토리 서버를 설정하고 관리함으로써 마이크로서비스 별로 api, util과 같은 프로젝트에 대한 버전 의존성을 갖도록 하는 것을 가능하게 합니다. 

 

예를 들어 api 프로젝트 내에서 소스코드를 일부 수정한 후 해당 프로젝트의 버전을 수정해 배포하는 과정은 다음과 같습니다.

이전에 설명했듯이 gradle 빌드 까지 끝냈다면 maven-publish 플러그인을 사용해서 사전에 배포설정을 진행하고 메이븐 레포지토리에 배포할 수 있습니다. 

plugins {
    id 'maven-publish'
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
    repositories {
        maven {
            name = 'myRepo'
            url = uri('file:///path/to/your/maven-repo')  // 로컬 리포지토리 경로 또는 원격 리포지토리 URL
        }
    }
}

 

마지막으로 gradle publish 명령어를 통해 메이븐 레포지토리에 배포합니다. 이렇게 하면 각 마이크로서비스 별로 버전을 선택하는 것이 가능해집니다. 


Java Interface를 사용해 RESTFul API를 설명하는 것의 효용성

  • 가독성이 좋습니다. 
  • 느슨한 결합이 가능합니다. 즉, API 구현체와 분리된 형태로 API를 정의하는 것이 가능합니다. 
  • 실제 구현체가 아닌 인터페이스를 통해 메서드를 호출함으로써 Mocking 테스트가 용이합니다. 
  • 다형성을 지원합니다. 
  • 문서화 자동화가 용이합니다. 즉, Swagger를 붙이 비용을 줄일 수 있습니다. 

저는 이 장점들 중에서도 테스트가 용이하다는 것이 매력적이었습니다. 아래 코드에서 ProductCompositeIntegration 클래스는 핵심 서비스로 발신 요청을 처리하는 통합 컴포넌트입니다. 자바 인터페이스를 사용해 API를 설명함으로써 테스트 코드에서 Mocking 처리하는 것이 쉬워지고, 인터페이스 메서드를 호출함으로써 테스트를 용이하게 만들었습니다. 

@MockBean
private ProductCompositeIntegration compositeIntegration;

@Before
public void setUp() {

    when(compositeIntegration.getProduct(PRODUCT_ID_OK)).
        thenReturn(new Product(PRODUCT_ID_OK, "name", 1, "mock-address"));
}

중요한 점

각 마이크로서비스가 담당하는 핵심 서비스로써의 역할은 비즈니스 로직에 의해 결정되는 것이라 이 챕터에서 중요한 점을 정리하면 아래와 같다고 생각합니다. 

  • Gradle을 사용해서 골격 마이크로서비스를 생성하는 방법
  • Java Interface를 사용해서 RESTFul API 설명을 작성하는 것의 중요성( with Mock 객체를 활용한 테스트의 용이성)
  • 마이크로서비스에서 버전 의존성을 갖는 것의 중요성
반응형