이전 포스팅과 같이 멀티 모듈 설계에 대해서 진행해 보겠다.
이전 포스팅에서 말한 것 같지만 멀티 모듈을 설계할 때 이렇게 해야 한다.라는 구조가 있지 않지만 지켜져야 할 것이 있고 지켜져야 할 부분에 맞춰 관점을 두고 명확하게 분리하는 것이 좋은 멀티 모듈 설계라 생각한다.
https://www.youtube.com/watch?v=uvG-amw2u2s&t=357s
해당 포스팅은 위 영상을 보고 정리한 포스팅입니다. 자세한 설명이 필요할 것 같으시다면 확인해 주세요.
멀티 모듈이 필요할 때는 작업 수정 및 히스토리의 파악이 어려울 때, 서비스 분리가 어려울 때(애플리케이션을 만들거나 검수하거나 사용하는 사람이 사용하는 각 서비스)
주의해야 할 점
SRP를 위반하기 쉽다. 이 SRP는 하나의 일만을 수행해야 한다.라고 이해하고 사용하기 십상인데 SRP는 하나의 액터만 책임져야 하고 변경의 이유가 하나여야 한다. 를 의미한다.
Common의 변경이 어려움. Common을 수정해야 한다는 것은 Common을 의존하는 모든 프로젝트에 영향이 끼친다는 것이다.
배포 독립성과 개발 독립성이 확보되어야 한다.
배포 독립성 - 변경된 특정 컴포넌트만 다시 배포하면 됨
개발 독립성 - 각 모듈을 독립적으로 개발할 수 있음
이것이 지켜지지 않는다면 전체적인 빌드와 배포가 필요하므로 배포 독립성과 개발 독립성이 확보돼야 한다.
멀티 모듈 설계하기
그렇다면 어떤 모듈부터 구성하는 것이 좋을까?라는 생각을 가지게 되는데 위 영상에서는 가장 단순한 것, 공통된 모듈부터 구성하기로 했으며 그중에서도 시스템에 독립적인 공통 코드를 분리하기로 했다.
코드를 분리하는 데 있어서 차례대로 설명하도록 하겠다.
특정 도메인에 종속되지 않는 공통 모듈
서로 다른 수준의 공통 코드 분리하기
- 핵심 비즈니스 로직: 시스템의 유/무와 관계없이 존재하는 비즈니스 로직
- 유스케이스: 시스템이 있어야 유효한 비즈니스 로직
도메인과 인프라 영역은 다른 수준을 가지고 다른 변경의 속도를 가진다.
이때 수준은 입력과 출력까지의 거리이다.
입력은 HTTP나 웹소켓 등이 있고
출력은 캐시나 데이터베이스 등이 있다.
고수준이란 입력과 출력으로부터 먼 것이고 비즈니스 요구사항에 의해 수시로 변경된다. 이는 도메인 영역이 해당된다.
저수준은 입력과 출력으로부터 가까운 것으로 새로운 환경이나 보안 취약점 등에 의해 특정 시점에 변경된다.
이를 통해
이렇게 모듈을 구성할 수 있다.
다음으로는 기능별로 나누는 것인데 기능별로 필요한 의존성이나 코드 변경의 주기가 다르기 때문이다.
ex) api 제공을 위한 의존성으로 RestDocs, Swagger 등, 관리자용 기능을 위한 의존성으로 Thymeleaf 등, 배치 작업을 위한 의존성으로 Spring Batch 등 각 기능에 따라 의존성이나 변경에 주기가 다르기에 기능별로 모듈을 나눠야 한다.
마지막으로 서로 다른 액터를 기반으로 모듈을 나눠야 했다.
이는 SRP를 위반하지 않기 위함으로 SRP는 이전에도 설명한 적이 있지만 변경의 이유가 하나여야 하고 하나의 액터만 책임을 져야 한다.
예시로 애플리케이션을 만드는 사람을 위한 모듈 애플리케이션을 사용하는 사람을 위한 모듈로 나눌 수 있다.
이는 큰 틀에 대한 모듈을 나눈 것으로 더 세부적인 모듈 설계가 필요하다.
이때 고려사항으로는
유스케이스는 공통 모듈에 담지 않기. 왜냐하면 유스케이스는 시스템을 포함하기 때문에 비즈니스 로직을 사용할 때 사이드 이펙트가 생기게 된다. 분리하게 될 때 중복인 코드가 보인다면 우발적 중복(Accidental Duplication)을 의심해야 한다. 우발적 중복은 중복처럼 보이지만 중복이 아닌 코드를 말한다. 그럼에도 진짜 중복인 코드가 생긴다면 중복을 허용하거나 공통 API를 만들어서 사용한다. 이때 코드 공유를 하지 않는 것이 좋다. 이로 인해 유스케이스는 공통 모듈에 담는 것은 피해야 한다.
공통 모듈에 존재하는 인프라 구성을 제한하기. 예시로 Configuration이나 ComponentScan 같이 자동으로 설정을 등록해 주는 부분에 있어 이렇게 서비스 시 인프라 구성을 제어할 수 없게 된다. 어떤 모듈은 A의 기능이 필요 없을 수 있는데 자동 구성으로 생성하게 될 때 필요 없는 A의 기능을 사용하게 될 수밖에 없고 A의 자원을 사용하게 되므로 동적인 구성 요소 선택으로 인프라 구성에 대한 제어권을 가져야 한다. 이를 확인해 보자.
상위 구성 인터페이스를 두고 인터페이스를 구현해서 기능에 맞는 구성을 해서 Enum으로 사용하게 하는 것이다.
명시할 애노테이션으로 어떤 구성 클래스를 사용할 것인지 애노테이션에 적시하고 동적으로 적용할 구성 클래스를 선택하는 ImportSelector를 구현해서 사용한다. 스프링에 애플리케이션이 로딩되며 동적으로 적용해 주게 된다.
이로써 아래와 같은 설정으로 사용할 수 있게 된다.
다음은 공통 모듈을 제어해야 한다.
공통 모듈을 엄격히 제어하기 위해서는 4가지가 있다.
컴파일 제한: 언어(package-private 가시성 등) 차원에서 제공하는 가시성 문법을 이용해 컴파일 제한을 거는 것
위에서 사용한 AppStoreConfigImportSelector 같은 경우 인프라 구성 외에는 다른 곳에서 사용할 일이 없기에 패키지의 접근 제어자를 private으로 만들었다. 기본적인 패키지는 private으로 만들고 필요한 경우에만 public으로 열어주는 방식으로 제한하는 게 좋다.
빌드 도구를 이용하는 방법도 있다.
위와 같이 implemetation과 api과 같은 문법을 제공한다. 이 두 개의 차이는 전이 의존성에서 차이가 있는데 이때 전이 의존성이란 위 이미지 기준으로 C가 B를 의존하고 B가 A를 의존할 때 C가 A를 의존하는 것을 전이 의존성이라 부른다. 만약 이 경우 api를 사용하게 되면 전이 의존성에 해당하는 클래스가 컴파일 경로에 노출된다는 점이 특징이다. 이미지의 경우 A를 B가 api로 사용하기 때문에 C에서도 A를 참조할 수 있게 된다.
아래처럼 모듈 B의 의존성을 implemetation으로 바꾼다면 컴파일 에러가 발생할 것이다.
이렇게 본다면 api를 사용하는 것이 좋지 않을까?라는 생각이 들 수 있는데 하지만 좋은 시스템은 제약이 있는 시스템이다. 컴파일 에러의 발생으로 불필요한 컴파일 의존성을 사전에 차단할 수 있기 때문에 api를 제거하는 것으로 제약을 두는 것이 좋고 gradle 공식 문서에서 api를 사용하는 경우에 불필요한 재컴파일에 의해 빌드 시간이 늘어날 수 있다고 적시되어 있기 때문에 api를 제거하고 implemetation을 사용하는 것이 좋다. 만약 위 이미지에서 C가 A를 참조해야 하는 경우가 생긴다면 implemetation으로 A를 받게 하는 편이 좋다.
설계 시에 애매한 부분이 생기기 마련이다.
예시로 apis의 액터를 기반으로 한 서브 모듈들을 별도의 독립적인 서비스로 분리할지
글로벌 유틸스의 경우 도메인을 가리지 않기 때문에 러이브러리화를 해야 할지
공통 모듈을 세분화해야 할지?
이 부분에 대해서 결정 사항을 최대한 미루고 다음의 조언을 기초로 설계한다.
A good architectuer allows you to defer critical decisions
(좋은 아키텍트는 결정되지 않은 사항들을 최대화한다.)
- Robert C. Martin -
apis의 액터를 기반으로 한 서브 모듈들을 별도의 독립적인 서비스로 분리는 선택이 아닌 생존의 기로에서 분리한다. 이렇게 구성하면 서비스 하나가 장애가 나도 다른 부분은 차단할 수 있지만 운영 측과 클라우드 환경에서 비용을 지불하고 운영하는 측면에서는 관리해야 할 서비스가 증가하므로 이 부분에 타협과 절충안이 필요하다.
글로벌 유틸스의 경우 도메인을 가리지 않기 때문에 러이브러리화를 해야 할지에 대한 부분은 다른 프로젝트에 사용되는 등 필요한 경우에 라이브러리화 할 수 있게 하도록 할 수 있다. 즉, 필요한 경우 필수가 될 수 있지만 사용되는 규모에 따라서 달라진다.
공통 모듈을 세분화해야 할지는 우선 패키지로 진행하고 지나치게 복잡해질 경우 세분화한다.
이로써 결정되지 않은 사항을 최대화한다는 것은 결정 사항들을 미뤄 놓는 것이 된다.
결정 사항을 미루는 것은 도움이 많이 된다. 애플리케이션 개발에서도 일맥 상통한다. 지나치게 필요하진 않는 건지 혹시 꼭 필요한 것인데 빠트리진 않았는지, 어찌 보면 모호하게 가져간다는 느낌도 있지만 1회성으로 결정되는 것이 아닌 확장되면서 수시로 확인하고 검토해야 하는 부분이다.
여기까지 위 영상을 토대로 멀티 모듈 설계에 대해 알아보았다. 물론 영상의 내용은 이미 서비스가 되고 있는 규모가 큰 서비스를 기준으로 설명하신 거라 나에게 지금 맞지 않을지 모르지만 내 프로젝트도 멀티 모듈로 나눠 구성하기 위해서 멀티 모듈로 구성할 것이지만 MSA로 분리해서 구성할지에 대한 고민도 든다. 아직 멀티 모듈에 대해 공부가 부족하다 느낀다 영상 몇 개 정리하고 글 본다고 정리되는 것은 아니며 내 프로젝트에 녹여낼 수 있을 때야말로 이해한 것이라고 생각한다. 그전에 다음 포스팅으로 한 번만 더 멀티 모듈에 대해 정리하고 이전 포스팅을 둘러보며 멀티 모듈 구성에 대해 필자의 견해를 쭉 설명한 후 천천히 구성하는 것을 포스팅하도록 하겠다. 물론 틀린 부분도 실패하는 부분도 그대로 포스팅하고 이 부분은 어떤 게 틀렸는지에 대해서도 고찰하고 심도 깊게 다뤄볼 예정이다.
'programing' 카테고리의 다른 글
알고리즘 LIST (0) | 2024.06.24 |
---|---|
kafka connect에 대해 (0) | 2024.06.21 |
멀티 모듈-3 (1) | 2024.06.10 |
멀티 모듈에 대해 (1) | 2024.06.09 |
kafka KRaft (1) | 2024.06.08 |