본문 바로가기

programing

멀티 모듈-3

이번 포스팅에서도 멀티 모듈에 대해서 다뤄 보도록 하겠다.

이전에 포스팅한 내용과 중복되는 부분이 있더라도 다른 분의 견해를 다시 소개하는 것이니 중복되는 부분은 복습하는 차원에서 보고 새로운 설계법이 있다면 참고하게 되면 좋을 것 같다. 이 포스팅을 마지막으로 멀티 모듈에 대한 개념을 잡을 것으로 마지막 단락에서 공부한 것을 토대로 나의 멀티 모듈 설계에 대한 견해를 작성할 생각이다. 물론 필자가 틀릴 수 있다. 이전 포스팅에서 말한 결정되지 않는 사항을 최대화해서 대략적인 틀을 잡아 큰 폭으로 서비스를 나누고 세부적인 모듈은 다음 포스팅에 필자 프로젝트를 소개하면서 나눠보도록 하겠다.

 

https://www.youtube.com/watch?v=nH382BcycHc&t=1s

https://techblog.woowahan.com/2637/

 

멀티모듈 설계 이야기 with Spring, Gradle | 우아한형제들 기술블로그

{{item.name}} 멀티 모듈 설계 이야기 안녕하세요. 배달의민족 프론트 서버를 개발하고 있는 권용근입니다. 멀티 모듈의 개념을 처음알게 되었을 때부터 현재까지 겪었던 문제점들과 그것을 어떻게

techblog.woowahan.com

 

해당 포스팅은 권용근님의 영상과 포스팅을 기반으로 작성되었음을 알립니다.

 

 

하나의 서비스에서 API, ADMIN, WEB 등 관리되는 구조를 모놀리틱 아키텍처라 부른다.

 

모놀리틱 아키텍처는 크게 단일 모듈 멀티 프로젝트와 멀티 모듈 단일 프로젝트로 나뉜다.

 

이때 떠오르는 건 그렇다면 MSA는 어떤 구조를 가지게 되는걸까? 라는 생각이 들었다. 

 

 

이처럼 서비스가 내부적으로 받는 도메인 단위를 상세히 분리하고 그 안에 정리되는 직군(서비스)들을 구상한 방식을 MSA라 한다.

 

멀티 모듈과 MSA가 함께 언급되는 이유에 대해 알아보자.

멀티 모듈의 역할, 의존성 분리를 통해 시스템의 분리와 통합을 유연하게 만들어주는 좋은 아키텍처를 만들 수 있다.

멀티 모듈을 활용해 역할과 의존성을 잘 분리할수록  모놀리틱과 MSA간 전환이 용이하며, 용이해야만 좋은 아키텍처라고 볼 수 있다.  

이로 인해 멀티 모듈과 MSA는 함께 언급된다.

 

멀티 모듈에 대해 알아보자.

단일 모듈 멀티 프로젝트

 

각각의 프로젝트 단위로 만약 IDE를 3개를 사용한다면 3개를 띄워둔 상태로 개발하는 것이다.

각 Member 클래스가 중복되고 있다. 이때 각 프로젝트도 어떤 서비스를 위한 것이기 때문에 각 Member의 스펙은 같아야 한다.

이렇듯 서로 독립된 프로젝트 단위를 가지고 있을 때 가장 큰 문제점은 중심 Domain이 가져야 할 구조와 규칙 등을 동일하게 보장해 주는 메커니즘이 없다는 것이다.

각 프로제그는 같은 Member를 사용하기 때문에 Copy&Paste로 개발하게 되고 이는 귀찮으면서 실수가 발생할 수 있는 일이다. 만일 Member를 잘못 구성한 상태에서 복사와 붙여 넣기를 했을 때 각 프로젝트마다 수정해야 한다. 이는 효율성이 정말 떨어진다.

 

이때 멀티 모듈 프로젝트는 기존 단일 프로젝트를 프로젝트 안의 모듈로써 갖게하는 구조를 제공한다.

 

위와 같이 하나의 시스템에서 중심 도메인을 모듈로 분리하여 위와 같은 보장 메커니즘을 제공받을 수 있다.

IDE도 한개 만 사용하고 시스템적으로 일관성 보장과 빠른 개발 사이클을 확보할 수 있다.

단순하게는 멀티 모듈은 프로젝트 공통으로 사용하는 코드를 모아서 같이 쓸 수 있게 하는구나.라고 생각할 수 있다.

맞는 말이지만 단순하게 생각할 때 문제점과 마주하게 된다.

 

위와 같이 공통되는 로직을 분리해 Common이라는 모듈을 구성해 보자.

extenrnal, batch, internal 모두 중복이 발생하거나 부분적으로 발생해도 Common에 포함시킨다고 했을 때 코드를 모듈화 하는 것은 중복을 최소화하는 것이라는 생각으로 집합시키게 되면

 

Common은 점점 커지면서 내부적으로 비즈니스가 흐르게 된다.

핵심이나 공통 코드들이 전부 Common에 들어가게 되므로 extenrnal, batch, internal는 간략하고 중복 없이 본래 가지고 있는 기능을 하게 되겠지만 문제점이 발생하게 된다. 각 모듈에서 작성하게 될 코드들이 점점 Common에 의존하게 되고 분리한 모듈의 기능도 점점 Common에 들어가게 될 것 이다.

 

스파게티 코드

모든 프로젝트들은 주기적인 청소가 반드시 필요한데 기능의 F/O든 자발적인 리팩토링이든 코드를 재구성하게 될 때 공통 모듈이 이를 방해하게 된다. 어떤 오래된 기능을 정리하게 될 때 영향 범위는 시스템 전체에 미치게 된다.

이처럼 공통 모듈 속에서 얽힌 의존을 확인하게 될 것이다.

class AService {
  private final ARepository aRepository;

  public A act(Long id) {
    ...
    return aRepository.findById(id);
  }
}

class BService {
  private final AService aService;

  public B act(Long id) {
    A a = aService.act(id);
    ...
    return mapToB(a);
  }
}

class CService {
  private final BService bService;

  public C act(Long id) {
    B b = bService.act(id);
    ...
    return mapToC(b);
  }
}

class DService {
  private final CService cService;

  //띠용!!
  public A act(Long id) {
    C c = cService.act(id);
    ...
    return mapToA(c);
  }
}

 

위와 같이 A -> B, B -> C, A -> C 결국 이런 식으로 얽힌 의존을 확인하게 된다

 

의존성 덩어리

Common 모듈에서는 애플리케이션들이 사용할 수 있는 의존성을 모두 가지게 된다.

 

애플리케이션들이 사용하는 의존성은 다를 수도 있다. 예시로 데이터베이스를 사용하지 않은 애플리케이션에서 공통 모듈을 사용하기 위해 데이터베이스와 커넥션을 맺게되고 그 외에 의존성도 필요하지 않는 기능으로 자원을 사용하게 될 수 있다. 스프링부트 설정 등에 의해 의도하지 않은 설정 값이 트리거로 발동 돼서 예기치 못한 에러를 발생시키는 상황이 발생한다.

 

공통 설정

고정적으로 공통으로 사용돼야 하는 호스트 정보 등은 경우에 따라 공통으로 봐야 할 수 있지만 그 외 스레드 풀, 커넥션 풀, 타임아웃 등의 설정도 가장 민감하고 중요한 애플리케이션 기준으로 몰아 들어간다. 발생하는 예시로 대표적으로 데이터베이스 커넥션이 있다. 모든 데이터베이스에는 가질 수 있는 최대 커넥션 개수가 정해져 있다. 데이터베이스를 사용하지 않는 애플리케이션에서 공통 모듈을 사용하기 위해 사용되는 커넥션으로 인해 실제 데이터베이스를 사용하는 애플리케이션과 해당 애플리케이션도 모두 문제가 생길 수 있게 된다.

이 외에 애플리케이션마다 다르게 설정되어야 할 설정이 한 곳에 몰리게 되면 예상하지 못한 사이드 이펙트가 발생할 수 있다.

 

이런 문제점이 발생하는 이유는 멀티 모듈 프로젝트는 독립적으로 실행가능한 애플리케이션을 1개 이상 가질 수 있기 때문이라 생각하고 멀티 모듈 프로젝트는 하나의 시스템을 단위로 만들어진다. 여기서 시스템은 독립적으로 실행 가능한 애플리케이션을 서비스라 하고 1개 이상의 서비스와 공유 인프라가 모여 하나의 시스템을 구성한다.

멀티 모듈 프로젝트는 독립적으로 실행가능한 애플리케이션 모듈을 1개 이상 가지고 있으며 사용하는 인프라 자원 역시 1개 이상을 가지고 있다. 독립적으로 실행 가능한 애플리케이션들은 당연히 서로 다른 책임과 역할을 가지기 때문에 스파게티 코드, 의존성 덩어리, 설정 등의 문제를 피하기 위해 하위의 모듈들에 대한 의존성과 사용성에 대한 개방, 폐쇄를 철저히 해야 한다.

 

멀티 모듈 구성하기

이후 아래에 나올 용어와 관점은 기본적으로 바라보는 시점인 레이어 아키텍처의 시점으로 바라볼 때 혼동이 올 수 있으니 주의하자.

 

 

모듈의 개념으로는 독립적으로 운영될 수 있는 의미를 가지는 구성요소 단위이다.

 

다른 멀티 모듈 프로젝트에 대한 레퍼런스를 찾아도 어떤 구조로 사용하고 어떤 기능을 위해 다른 기능과의 연관, 개인적인 주관도 들어있기 때문에 레퍼런스를 확인하기에는 어려움이 많다. 하지만 모듈 자체의 특징에 대해서는 공통된 특징을 찾아낼 수 있다.

- 모듈은 독립적인 의미를 갖는다.

- 모듈은 어떠한 추상화 정도에 대한 계층을 가지고 있다.

- 계층 간 의존 관계에 대한 규칙이 있다.

이러한 특징을 기반으로 설계 시 시스템에서 가져야 할 계층 구조를 정의했다.

 

  • 독립 모듈 계층
  • 도메인 모듈 계층
  • 내부 모듈 계층
  • 공통 모듈 계층
  • 애플리케이션 모듈 계층

이렇게 정의된 계층 구조를 갖음으로 모듈이 어디까지 책임과 역할을 가질 수 있는지를 명확히 할 수 있고 의존 관계를 최소화해 최적화할 수 있게 된다. 

 

위 이미지의 모듈을 나누는 법은 권용근 개발자님이 사용하신 방법이다.

이제 각 모듈별로 가지는 특징, 구성 원칙을 둘러보자.

 

독립 모듈 계층

시스템과 무관하게 어디에서나 사용 가능한 라이브러리 성격의 모듈을 이 계층에 배치해야 한다. 프로젝트 안에서 성격이 가장 먼 모듈이 이 계층에 위치하게 된다. 시스템, 도메인의 비즈니스와 별개로 자체 제작한 라이브러리 같은 모듈이 이 계층에 존재하게 되고 특별한 경우를 제외하고 이 계층에 모듈을 생성하면 안 된다.

원칙으로는 자체로써 독립적인 역할을 갖게 해야 한다.

 

 

위는 권용근 개발자님 모듈 설계 예시이다.

 

공통 모듈 계층(System core)

 

하나의 프로젝트에서 모든 모듈에 사용될 수 있는 코드는 생길 수밖에 없고 이 모듈은 제약이 필수이다.

Type, Util 등을 정의한다. 가능하면 사용하지 않는다.

시스템 속 모든 모듈이 의존할 수 있어야 할 만큼 적은 의존성을 제공해야 하기 때문에 프로젝트 내 어떤 모듈도 의존하지 않아야 한다.

bootJar 
jar 

dependencies {
}

이 말은 순수 Java Class만 정의할 수 있는 것으로 시스템적으로 많이 사용되는 Type의 정보나 DTO 등 유틸 클래스만 배치하고 최대한 사용하지 않는다를 지향해야 한다.

 

도메인 모듈 계층(System domain)

 

시스템의 중심 도메인을 다루는 모듈을 이 계층에 배치하고 내부 모듈 계층과 비슷한 성격의 계층이지만 저장소와 밀접한 중심 도메인을 다루는 계층은 더 견고하고 특별하게 격리되고 관리되어야 하기 때문에 반드시 분리되어야 한다.

갖는 원칙으로는 

서비스 비즈니스를 모른다.

하나의 모듈은 최대 하나의 인프라 스트럭처에 대한 책임만 갖는다.

도메인 모듈을 조합한 더 큰 단위의 도메인 모듈이 있을 수 있다.

 

이 계층의 모듈은 오로지 도메인에 집중해야 한다. 어떠한 도메인이든 그 도메인이 가져야 할 서비스와 무관한 도메인의 비즈니스가 있다. 도메인 중심 설계에 대한 내용으로 견고한 도메인에서부터 프로젝트를 만들어 올라가면 자연스럽게 이런 구조가 나올 수 있기도 하다.

하나의 모듈은 최대 하나의 인프라 스트럭처를 갖는 것은 의존성의 전파를 방지하기 위해서이다. 인프라 스트럭처 단위로 최대한 작은 단위부터 작성되어야 한다. 

 

단일 인프라 스트럭처 사용 모듈 - 가장 흔한 케이스로 RDBMS가 중심 도메인을 품고 있는 프로젝트는 이런 도메인 모듈이 하나만 만들어진다. 도메인 모듈은 아래와 같은 책임을 갖는다.

 

도메인 - 자바 클래스로 표현된 도메인 클래스가 이곳에 위치한다. JPA의 기준으로는 테이블과 매핑되는 클래스들이다. 도메인 모듈 내부에서는 이곳에 위치한 도메인들을 사용해 대화하게 된다. 

인프라 스트럭처 - 데이터 접근을 담당하는 DAO나 JPA 리포지토리를 포함한다. 데이터베이스와 상호작용을 담당한다.

도메인은 인프라 스트럭터를 몰라야 제대로 된 도메인을 구성할 수 있다.

 

다중 인프라스트럭처 사용 모듈 - 도메인 계층에서 여러 인프라 스트럭처를 사용해야 할 때 꼬이는 의존 관계가 많아진다.

예시로 RDNMS를 사용하는 A라는 도메인 모듈이 있을 때 시스템의 요구사항으로 A의 도메인에 삽입이 발생할 때 B라는 도메인으로 가공해 임시 저장하는 요구가 생겼을 때 RDBMS를 사용하는 도메인 모듈에 레디스 의존성을 추가하면 의존성이 점점 무거워질 것이다.

A 도메인에 대해 조회성을 위해서만 도메인 모듈을 사용해야 하는 상황에서도 레디스에 대한 설정과 의존이 필요하게 되므로 다양한 인프라 스트럭처를 사용할 때 점점 의존성은 꼬이고 무거워질 것이다. 그래서 하나의 모듈은 하나의 인프라스트럭처만 책임지도록 모듈을 구성하는 것이 추천된다. 만약 두 개의 인프라 스트럭처를 사용 시에 관계가 생길 때 두 모듈을 품는 모듈을 작성하거나 두 인프라 스트럭처 모듈을 품는 애플리케이션에서 처리해야 한다.

 

이렇게 구성시 하나의 모듈이 두 인프라 스트럭처를 책임지는 것이 아니냐는 물음이 생길 수 있지만 두 인프라 스트럭처의 모듈을 품는 모듈에게는 인프라스트럭처의 대한 책임이 주어지지 않는다.

 

내부 모듈 계층(in System available)

저장소, 도메인 이외 시스템에 필요한 모듈은 이 계층에 속하게 된다. 독립 모듈 계층은 시스템에도 전혀 관여되지 않았다면 이 계층은 시스템과 연관이 있는 모듈을 말한다.

원칙은 애플리케이션, 도메인 비즈니스를 모른다.

이 계층은 시스템 전체적인 기능을 서포트하기 위한 기능 모듈이 만들어질 수 있다. 독립 사용이 가능한 모듈이 위치해야 하므로 도메인 계층을 의존하지 않는다.

 

애플리케이션 모듈 계층

독립적으로 실행 가능한 애플리케이션 모듈 계층이다.

애플리케이션 모듈은 하위 설계했던 모듈들을 조립해 서비스 비즈니스를 완성시킨다.

이 계층에 모듈에는 app이라는 네이밍으로 모듈 내 실행이 가능한 애플리케이션이라는 것을 명확히 할 수 있다.

애플리케이션 모듈은 독립 모듈 계층, 도메인 모듈 계층, 내부 모듈 계층, 공통 모듈 계층을 사용성에 따라 의존성을 추가해 사용하게 된다.

 

이렇게 구성하게 된다면 계층은 아래와 같은 의존 관계 흐름을 가지게 된다.

 

 

멀티 모듈 구성 효과

기존 하나의 모듈에서 다양한 애플리케이션과 레이어 의존성을 책임지고 있었지만 계층화된 모듈 레이어로 분리됨으로 아래와 같이 역할과 책임의 선을 명확하게 하고 개발의 생산성을 향상할 수 있다.

 

각 모듈이 갖는 책임과 역할이 명확하여 리팩토링, 기능의 변경의 영향 범위를 파악하기 용이하다.

경계가 명확해짐으로써 기능의 제공 정도를 예측 가능하고 스파게티 코드 발생 가능성이 줄어들었다.

역할과 책임에 대한 애매함이 없어짐으로써 어떤 모듈에서 어느 정도까지 개발되어야 할지 명확해진다.

 

최소 의존성

각 모듈은 각자의 최소 의존성만 갖게 할 수 있다.

이때 의존성의 관리가 중요한가 생각하면 자바에서 성능으로 본다면 큰 성능 차이는 아니지만 좋은 개발환경이라는 측면과 스프링 부트를 사용하는 측면에서 손해가 생길 수 있다.

첫 번째로는 여지의 개방으로 개발 생상성을 떨어트린다. 무엇이든 끌어다 쓸 수 있게 된다는 측면에서 초기 개발은 빨라지지만 스파게티 코드가 될 것이다.

두 번째로 스프링 부트를 사용하고 있기 때문에 예상 못하거나 불필요한 설정이 동작하게 될 수 있다. 클래스의 존재만으로 작동되는 설정들이 많다. 이로 인해 예상치 못한 설정이 로드될 수 있으며 혹은 잘못된 멀티 모듈에서 보았던 사용하지 않는 자우너에 대한 설정을 해줘야 하는 일도 발생할 수 있다.

 

의존 관계

 

스프링 부트는 몇 가지 규칙에 의해 configuration property를 로드하도록 되어있다.

 

 대부분 모듈에 속해있는 properties 파일을 작동시키기 위해 application-$이 로드되는 방식을 많이들 이용하고 있다. 모듈에는 application-xxx.yml과 같은 네이밍을 사용하여 변수를 세팅하고 애플리케이션 모듈의 application.yml 에서

 

spring.profiles.include=xxx

 

위와 같이 xxx profile을 추가해 파일을 로드하는 방식이다.

 

모듈 Component Scan

 

모듈에 있는 Bean(configuration, Service, Repository 등)을 로드할 수 있는 방법으로

첫 번째로 spring.factories를 통한 로딩 방식이다. 스프링 프레임워크는 모듈에 있는 META-INF/spring.factories 파일을 통해 특정 사이클에서 로드될 클래스를 설정해 줄 수 있다.

두 번째로 base package의 확장이다. 스프링 부트는 아래와 같이 스캔되는 패키지를 확장할 수 있다.

@SpringBootApplication(scanBasePackages = "com.baemin.xxx.domain.shop")
public class XXXApplication {

 

이 방식이 스프링에서 권장하는 방식이다.

 

이때 권용근 개발자님은 애플리케이션 클래스를 한 패키지 상위에 작성하는 관례를 두었다.

프로젝트 안에서 모든 모듈은 공통적인 상위 공통 패키지를 갖고 애플리케이션 모듈에서는 상위 공통 패키지에 애플리케이션 클래스를 배치한다.

 

애플리케이션 모듈

 

 

하위 모듈

 

 

이렇게 구성했을 때 모듈 내 모든 Bean을 모듈 의존성 추가만으로 사용할 수 있다.

 

접근 개방/폐쇄

설계와 개발을 하다 보면 개방과 폐쇄에 대한 많은 고민을 하게 되는데 예로 A라는 코어가 있고 A는 보호받아야 하기 때문에 B라는 서비스를 만들게 되는 경우가 있다.

이때 사용할 수 있는 방법 3가지가 있다.

첫 번째는 접근제한자의 활용이다. 패키지나 상속 수준의 접근제한을 설정하며 A라는 코어를 보호할 수 있다. 그러나 A와 B를 모듈 차원에서 보았을 때 쉽지 않다. 모듈이란 것은 독립적인 의미를 가져야 하므로 이 둘 모듈의 내용을 제한할 수 없다.

두 번째는 관례이다. 함께 개발하는 사람들끼리 함께 지켜야 하는 관례를 만들어내 문서나 코드리뷰를 통해 새로운 구성원에게 전파되게끔 노력해야 하는 방법이다.

세 번째는 gradle의 implementation, api의 활용이다. 사용에 대한 부분은 이전 포스팅에서 잠깐 다룬 적이 있다. implementation을 사용하도록 하고 이는 하위 의존관계를 숨겨준다. 숨겨준다는 것은 하위 의존에 대한 접근을 제한한다는 것이다.

이에 대한 설명은 이전 포스팅에서 확인할 수 있다.

 

README

다양한 모듈을 갖게 될 때 Naming 만으로는 모듈의 의도를 파악하기 어려우므로 모든 모듈에 README를 작성하는 것이 바람직하다.

각 모듈에 대해 역할과 책임, 실행이나 사용 방법, 관례 등을 문서화해서 작성하면 원활한 개발 환경을 만들 수 있다.

 

 

정리

여기까지 멀티 모듈에 대해 알아보았다. 이제부터는 필자 프로젝트로 멀티 모듈을 구성하는 법을 진행할 것이다.

이전 포스팅들과 이번 포스팅까지 공부를 해본 결과 한 번에 세세하게 어떤 구성과 모듈을 구성해서 가져가자. 하는 것은 불가능한 일인 것 같다. 멀티 모듈을 구성하는 것도 처음이기에 한번에 정답을 도출해 낼 수는 없을 것이다. 각자의 견해도 다르기에 정리하면서 필자가 느낀 생각과 공통점은 계층을 분리하고 역할에 따른 모듈 분리로 각자가 가질 수 있는 책임을 확실히 분리해야 할 것 같다.

어떤 기능을 가진 모듈을 구성할 때 어떤 기능에 대한, 어떤 모듈에 대한 의존성을 갖는지 검사하고 최소한의 의존성을 가진 상태로 구성해야 할 것이고 변화에 따른 분리, 공통으로 사용하는 것에 대한 경량화가 주축이 되고 공통된 기능은 인터페이스나 추상화를 통한 리팩토링도 진행하게 될 것 같다.

아마 계속 변화하고 수정해야 할 수 있다. 틀린 부분도 많을 테니 감안하고 봐주길 바라고 이번 포스팅은 여기까지 하도록 하겠다.

다음 포스팅은 기초 틀을 구성하도록 하겠다. 우선 분리하기 위해서 필자가 프로젝트 이전에 유스케이스와 유스케이스 명세서가 있는데 이것을 기반으로 틀을 구성하고 실제 사용하는 기술이나 클래스를 사용해서 모듈을 대략적으로 나눠보고 각 클래스마다 책임과 역할을 분리하는 포스팅을 하게 될 것 같다. 

이전 멀티 모듈 구성을 해보자 했을 때 간단하지 않을까? 레이어 나누고 기능만 나누면 되겠지 패키지별로 떠서 분리하면 되겠지라는 막연한 생각을 가지고 있었지만 제대로 분리할 수 있도록 노력해 보고 문서화도 할 수 있도록 1인 개발이지만 관례도 만들어보고 하겠다.

'programing' 카테고리의 다른 글

알고리즘 LIST  (0) 2024.06.24
kafka connect에 대해  (0) 2024.06.21
멀티 모듈 - 2  (2) 2024.06.09
멀티 모듈에 대해  (1) 2024.06.09
kafka KRaft  (1) 2024.06.08