Backend 개발자/아키텍처

마이크로 서비스 레이어드 아키텍처(MSA) 3편 멀티 모듈(Multi Module)

by 앵과장 2022. 2. 22. 10:35
반응형

안녕하세요
앵과장입니다.

이글을 읽기전에 소스코드부터 필요하신분들을 위해서 github 링크 걸어둡니다.
https://github.com/lswteen/product


MSA로 가기위해서 많은 부분들을 점진적으로 만들어보고 삽질한 내용을 기록중인 상태입니다.

프로젝트는 항상 이상적인 방법으로 구현하기란 쉽지 않습니다.
왜냐!!!!
Legacy가 존재 하기 때문입니다.

상황에 맞는
마이크로서비스


마이크로 서비스는 생각보다 많은 리소스 비용이 발생합니다.
도메인 별로 분리한다는건...

인스턴스 생성
네트워크 I/O
도메인 비지니스 소스파편화
Devops CI/CD 구성
도메인 DNS 생성
도메인별 리소스 개발 운영비용

여기서 Legacy 서비스를 MSA로 전환한다고 가정한다면..

Legacy 분석및 설계
도메인으로 분리하는 과정
기존 데이터 마이그레이션
여러 요소들이 존재합니다.

한마디로 한번에 MSA로 갈수 있는 상태가 아니라는 겁니다

점진적인 방향에서
멀티모듈


우선 기존에 Legacy도메인 서비스를 점진적으로 확장할수 있는
단계적 구성으로 멀티모듈을 사용해보도록 하겠습니다.

On premise환경에 Monolithic 으로 구성된 구조는 소스가
스파게티 처럼 뒤섞여 있을 확률이 보통 80프로 이상 입니다.

여기에 오래동안 서비스를 운영했다면 분석까지 힘들어지는 상태가 되기도 합니다.

Legacy분석중 힘들었던 Best 8

1.Table 컬럼에 데이터가 비지니스로직이 들어갈때
2.네이밍이 축약되어있을때
3.소스 엄청길때
4.If else에 연속일때
5.불필요한 주석이 너무많이 존재할때
6.안쓰는 변수 메소드가 많을때
7.물어봤는데 아무도 모를때
8.동일한 방어코딩이 여기저기 들어가 있을때

그외에도 많지만 이정도로 정리해봅니다. 어느순간 레거시가 가장 힘들었나요?!

멀티모듈 왜?
사용해야되나요!!


멀티모듈을 본격적으로 사용했던시기는 2018년 인것 같습니다.

개발자들은 항상 리펙토링을 진행하며 접근제한과 용도에 맞게 구성하려고 합니다.
(리펙토링 안하던 시기도 있었지만 최근에는 많은 개발자들이 성장을 위해서 변화하는 추세입니다.)

하지만 언어처럼 Syntax로 제약을 할수가 없기 때문에 의도와 상관없이 운영하면서 맨처음 생각했던 구조에 틀을 벗어나는 코드가 만들어지기도 합니다.

Domain은 분리했는데..
Internal, External, Batch, Common 는 어떻게 분리하면될까?


예를 들어 상품 도메인을 분리하고 위에 언급한 내용들을 개발한다고 가정해보도록 하겠습니다.
상품을 서비스하기 위해서는 용도가 필요한 Application이 사용됩니다.

Internal Api : 내부 도메인들과 통신할 Private VPC 영역에서 사용할 Api Application
External Api : 외부 도메인들과 통신할 Public VPC영역에서 사용할 Api Application
Batch : 특정 시간에 동작해서 일괄처리할 성격을 가진 스케쥴링 필요한 Job

문제점!!


이런 구조를 가지고 있을 때 가장 큰 문제점은 시스템의 중심 Domain 이 가져야할 구조와 규칙 등을 동일하게 보장해주는 메커니즘이 없다는 것 입니다.

개발자는 동일한 Domain 을 가지고 있는 위 3가지 어플리케이션을 열심히 Ctrl C + Ctrl V 하며 개발을 하게 됩니다. 중복이 발생하며 동일한 코드를 변경할때마다 한땀한땀 변경해야하는 이슈가 발생합니다.

멀티모듈은 이럴때 필요합니다!!

멀티 모듈 프로젝트는 기존의 단일 프로젝트를 프로젝트 안의 모듈로서 갖을 수 있을 수 있는 구조를 제공합니다.

실패한 멀티 모듈 프로젝트

그림에서 보는것 처럼 어플리케이션들을 가지는 시스템을 멀티 모듈로 바꾸어보았습니다.

Product System

  • internal api
  • external
  • batch


공통으로 사용하는 코드들을 모았습니다. 모듈을 이름을 뭘로 하지?
common 이 좋겠다! 그리고 common 의 저주가 시작되었습니다. 아래와 같은 구조가 완성되었습니다.

정책이나 오픈라이브러리에 대한 부분들은 Common에서 변경하면 됩니다.라고 생각했는데.....

공통(Common) 모듈 문제점

공통 모듈에 대부분의 핵심 또는 공통 코드들이 다 들어가게 되었습니다. 그리고 많은 문제점들이 생기기 시작했습니다.

A 어플리케이션 에서 기능을 추가합니다. 이 때 코드는 어디에 작성되게 될까요? 개발자는 고민을 하게 되고, 코드는 어떠한 선택에 의해 빈번하든 아니든 공통 모듈에 점점 추가 가 됩니다. B 어플리케이션 에서도 마찬가지 일 것 입니다.

이제 C 어플리케이션 에서 기능을 추가합니다. 공통 모듈에 작성된 유용해보이는 코드들이 있습니다. 사용하게 됩니다. 사실 그 기능은 A 어플리케이션 을 위해 작성한 코드입니다.

위 과정이 반복이 되다보면 어느세 공통 모듈은 걷잡을 수 없이 커져있을 것 입니다. 그리고 어플리케이션에서 하는 일이 점점 줄어들고 공통 모듈에서 점점 더 많은 일을 하게 됩니다.

Common에 너무 많은 의존성 증가

공통 모듈은 최초 구성에서 개발진행되며 여러가지 많은 비지니스에 필요한 Dependency Opensource 라이브러리들을 포함하게 됩니다.

External API에는 필요하지만 Internal API에는 사용하지 않는내용이 들어가고 Batch에는 사용되지만 External APi, Internal API에는 사용되지 않는 내용들 말입니다.

dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' implementation "org.apache.commons:commons-lang3:3.11" implementation 'javax.validation:validation-api:2.0.1.Final' compile 'com.fasterxml.jackson.core:jackson-databind:2.11.4' implementation 'junit:junit:4.13.1' // MapStruct implementation 'org.mapstruct:mapstruct:1.4.2.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' testAnnotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final" // lombok-mapstruct-biding annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0" developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-web' // implementation 'org.springframework.boot:spring-boot-starter-data-jpa' compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2' compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
공통 설정

Infrastruture config에 해당하는 부분까지 공통영역으로 들어가면 어플리케이션에서 민감하게 처리하는 Thread Pool, Connection Pool, Timeout에 대한 이슈가 발생할수 있습니다.

문제점을 해결하기 위한 회고(retrospect)

멀티모듈 프로젝트는 독립적으로 실행가능한 어플리케이션 모듈이 1개이상, 인프라스트럭처 자원 1개이상 입니다.
독립적으로 실행 가능한 어플리케이션은 각자의 책임과 역활이 있기 때문에
복잡성, 의존성, 설정 등의 문제를 피하기 위해서는 서로 필요한 만큼만 접근할수 있는 개방, 폐쇄를 최대한 지켜야 합니다.

개방, 폐쇄 를 최대한 지키는 멀티 모듈 구성하기

멀티모듈에 대한 계층에 대한 설명에 대한 이해를 돕고자 아래 관점에 대한 이해가 필요합니다.
각 레이어 계층을 왼쪽사진보다는 오른쪽 사진을 기준으로 접근하는것이 해당내용을 이해하는데 도움이 됩니다.

 

  • 프리젠테이션 계층 (Presentation) : 말그대로 표현하는형태로 Client 계층이라고 예를 들어보도록 하겠습니다.
  • 서비스 계층 (Service) : Run 실행되는 Main Class 가 존재하는 Applicatoin계층이며 Backend 에서는 BFF (Backend for Frontend) 또는 Domain API Application 을 표현합니다.
  • 도메인 계층(Domain) : RDBMS, In Memory, Nosql, Queue 등 다양한 Infrastructure에 해당하는 서로 용도에 맞게 분리된 entity, Repogitory, service 단순한 구조에 지속적인 원자성 무결성 데이터를 처리하는 영속성(Persistent) 도메인 비지니스를 처리합니다.
  • 인프라스트럭츠 계층(Infrastructure) : independently available 독립적인 프로세스이며 RDBMS, In Memory, Nosql, Queue 에 속하며 Outside the System 이라고 표현합니다. Application에서 사용하기 위해서 Config설정이 필수적으로 필요합니다.
멀티모듈을 구성하는 계층 특징

 

  • 독립 모듈 계층 (Independently available)
  • 도메인 모듈 계층 (System Domain)
  • 내부 모듈 계층 (in System Available)
  • 공통 모듈 계층 (System Core)
  • 어플리케이션 모듈 계층 (Applicatoin)

아래 이미지들과 위에서 언급한 계층에 대해서 단계적으로 설명하며 모듈 단위에서 필요한 단계를 알아보도록 하겠습니다.

독립 모듈 계층 (Independently available)

시스템과 무관하게 어디에서나 사용가능한 라이브러리 성격의 모듈 계층이라고 보시면됩니다.
Cache, Rdbms, redis, sqs 등이 포함됩니다.

정책(Policy) :

  • 자체적으로 독립적인 역활을 수행합니다.

 

도메인 모듈 계층 (System Domain)

시스템의 중요한 중심 도메인을 다루는 모듈 계층입니다.

독립적계층(Independently available), 공통 모듈 계층을 포함합니다.
도메인에 필요한 로직에 집중해야하며 도메인 중심 설계에 대한 내용으로 견고한 도메인에서부터 프로젝트를 만들어 올라가면 자연스럽게 이러한 구조가 나올수 있기도 합니다.

하나의 모듈은 최대 하나의 인프라스트럭처를 갖는 것은 의존성의 전파를 방지하기 위함입니다.
사용하지 않는 인프라스트럭처에 대한 설정까지 해줘야 했던 실패한 멀티 모듈에서 경험으로 인프라스트럭처 단위로 최대한 작은 단위부터 작성되는것이 중요합니다.

정책(Policy) :

  • 서비스 비지니스를 알필요가 없습니다.
  • 하나의 모듈은 최대 하나의 인프라스트럭처에 대한 책임만 가져야 합니다.
  • 도메인 모듈을 조합한 더 큰 단위의 도메인 모듈이 만들어 질수 있습니다.

 

단일 인프라스트럭처 사용 모듈

가장 많이 사용할수 있는 RDBMS 중심 도메인을 품고 있는 프로젝트는 아래와 같은 형태의 모듈이 만들어 집니다.
멀티모듈 생성시 네이밍 규칙은 {xxx}-domain 으로 생성하였습니다.

도메인 모듈이 가질수 있는 책임에 대한 내용입니다.


Domain Entity : DAO(Data Access Object) JAVA Class로 표현된 도메인 정보들이 표시되며 JPA, Mybatis 기준으로 한다면 테이블과 매핑이 되는 Class입니다.

Domain Repository : 도메인의 조회, 저장, 수정, 삭제 역활을 합니다. 도메인 모듈에서 가장 보호 받아야하며 견고한 모듈이며 다른 도메인들과 Aggregation 되어 함께 노출이 되어야하거나 하나의 도메인 모듈이상에 개발이 필요하다면 Application 레이어에서 작업이 되어야 합니다.

Domain Service : 도메인 비지니스 (Persistent) 를 책임 지는 역활입니다.
비지니스가 단순하다면 해당계층은 삭제할수도 있습니다.
해당 계층에서는 트랜잭션에 단위를 정의하며 요청온 데이터 검증 또는 이벤트를 발생하는 일을 수행합니다.

다중 인프라스트럭처 사용 모듈

도메인 계층에서 여러 인프라스트럭처를 사용해야할때 스파게티가 되는 의존관계를 많이 볼수 있습니다.

예를 들어 RDBMS에서 사용하는 A라는 도메인 모듈이 존재합니다.
기획의 정책 요구사항으로 A 도메인에 저장하기 전에 B 라는 도메인으로 가공해서 임시 저장시켜야하는 기능이 필요하다고 가정해 보도록 하겠습니다.
또는 회원가입을 해야할때 6자리 임시문자를 발송하고 Redis에 저장한뒤 특정 시간이 지나면 종료하고 회원가입전 인증했는지에 대한 여러가지 중간 프로세스가 존재한다고 예시해보면...

이때 RDBMS를 사용하는 도메인 모듈에 Redis 의존성을 추가해야한다면 의존성 으로 복잡해지는 코드를 구현할 가능성이 높아집니다.
그래서 하나의 모듈은 하나의 인프라스트럭처만 책임지도록 설계 하는것을 추천하며 두개 인프라스트럭처를 사용해야하는 관계가 발생한다면 두개의 모듈을 모두 사용할수 있는 Domain 레이어를 만들거나 어플리케이션에서 두개의 모듈을 처리할수 있도록 구성하는것을 권고 드립니다.

왼쪽보다는 오른쪽에 있는 아키텍처 처럼 각 도메인에 하나의 인프라스트럭처만 책임을 주고 필요하다면 두개의 도메인을 전달할수있는 계층을 두고 어플리케이션 개발하는것을 권고합니다.

공통 모듈 계층 (System Core)

공통 모듈이라고 크게 다양한부분에 대한 복잡성이나 의존성을 올리는 레이어는 아니라는 점 참고하시기 바랍니다.

정책(Policy) :

  • 필요할때만 사용하며 가능하다면 사용하지 않는것을 권고드립니다.
  • Type, Util등 비지니스로직을 포함된 내용을 담지 않아야합니다.
  • 되도록 이면 Gradle에도 의존성이 되는 모듈은 최소화 하는것을 원칙으로 합니다.

순수한 Java Class 정의 많이 사용되는 Static한 내용 Type 또는 많이 사용되는 utils로 구성해야합니다.

어플리케이션 모듈 계층 (Applicatoin)

독립적으로 실행 가능한 어플리케이션 모듈 계층입니다.
IP:PORT가 존재하며 OS에서 유니크한 프로세스로 동작합니다. 필요에 따라 Domain DNS가 생성될수 있습니다.

어플리케이션 모듈은 하위 설계 했던 모듈을 블록처럼 조립하여 서비스 비지니스를 완성합니다.

{domain}-app
{domain}-app-internal-api
{domain}-app-external-api
{domain}-app-worker
{domain}-app-batch

다양한 Application 'app' 이라는 네이밍 규칙으로 실행가능하다는 어플리케이션이라는 부분을 직관적으로 어필할 필요성이 있습니다.

내부 모듈 계층 (in System Available)

External API로 외부 서비스 도메인에 대한 요청과 응답을 할수 있는 사용성 제공 이나
Web Filter를 이용한 보안, 로깅, 웹에 대한 필수적인 공통 설정을 만드는용도로 활용됩니다.
특정 이벤트에 대한 처리를 담당하며 SQS로 이벤트를 전송하거나 로그를 남기는 특정 행위를 만들때 사용할수 있습니다.

멀티 모듈 구성에 효과
명확한 추상화적 경계

계층에 따라 분리된 모듈로 인해 추상화 레벨에 맞출수 있습니다.

모듈 Component Scan

Spring에서는 Component Scan이 동작해야합니다.
Bean으로 등록될 준비를 마친 클래스들을 스캔하여 빈으로 등록하는것을 말합니다.

보통 @Controller, @Service, @Component, @Repository 어노테이션들을 붙인 클래스들을 빈으로 등록될 준비가 되어 있다고 보면됩니다.

예전 Spring만 사용할때는 @ComponentScan을 정의하였지만
Springboot 에 @SpringBootApplication 을 선언하면 기존에 불필요한 선언들을 줄일수 있습니다.

물론 Springboot Document에 나와있는 가이드 처럼 했다는 가정에 말입니다.!!
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io


잠이 안올때 들으면 잠이 5분만에 온다는 백기선님의 스프링 유투브를 보셔도됩니다.
1-40개의 영상이 대략 40-60분 분량으로 되어있습니다.

Spring을 좀더 알아보고싶다면 추천드립니다.!!
https://www.youtube.com/watch?v=CnmTCMRTbxo&list=PLfI752FpVCS8tDT1QEYwcXmkKDz-_6nm3

 

@SpringBootApplication 을 클릭하면 위처럼 필요한 어노테이션들이 선언되어있습니다.
멀티모듈에서 필요로 하는 Bean클래스를 로드하는 방법으로

Application 클래스 패키지 상위에 작성하고
추가적인 base package 스캔 설정 없이 진행하도록 구성 하는 방식 입니다.

이렇게하면 main class가 존재하는 패키지 구조 'kr.co.renzo' 하위 모든 Class를 스캔하게됩니다.

접근 개방 / 폐쇄

멀티모듈 사용하는 방법으로 gradle dependencies를 사용하였습니다.

gradle의 implementation, api 활용합니다.
implementation 는 하위 의존관계를 숨겨준다고 합니다. 즉 하위 의존에 대한 접근을 제한한다는것 입니다.

A Module
public class A

B Module
api project(':A')

C Module
implement project(':B')

public class C {
public void act(){
new A() //compile error
}
}

implementation 은 하위 의존을 숨겨주므로 우리가 보호받아야할 하위 모듈에 대한 접근 폐쇄가 가능해집니다.
어플리케이션 모듈계층에서는 implementation을 사용하고 그외 계층에서는 api방식으로 개발합니다.

왼쪽 : domain module
오른쪽 : application module



한때 상당히 심취해 있었던
우아한 형제들 기술블로그에 멀티모듈을 참고하여 직접 개발해보고 사용해본 내용을 기반으로 작성하였습니다.
https://techblog.woowahan.com/2637/