I. 들어가며
최근 이직을 했는데, 이직한 곳의 프로젝트 구조가 종래의 단일 모듈 멀티 프로젝가 아닌 멀티 모듈 단일 프로젝트이다
낯선 방식인데, 어떤점에서 이 방식을 이용했는지 멀티모듈 아키텍처의 개념과 구조 그리고 실패사례 등을 살펴본다.
II. 멀티모듈 아키텍처란?
1. 모놀리식 아키텍처
하나의 서비스에서 API, Admin, Batch, WEB, DB등이 관리되는 구조이다.
이 경우 단일 모듈 멀티 프로젝트 VS 멀티 모듈 단일 프로젝트로 구분이 된다
2. 단일 모듈 멀티 프로젝트
이전 직장의 아키텍처 방식이다.
각각의 프로젝트 단위로 IDE를 각각 띄우면서 이용을 한다.
특히 모듈간 공통된 domain(ex: Member)이 중복이 된다. 이 경우 변경이 있으면 여러 모듈간 반복되는 복사 붙여넣기가 수반된다. 이는 곳 사람에게 굉장히 의존하는 형태임을 알 수있다.
공통된 domain을 해결하기 위해서 아래의 절차를 이용할 수 있다.
사설 Maven Repository 를 만들어 각각의 프로젝트에서 공유하고 있는 DTO, 도메인 클래스 분리 후 프로젝트화 시켜서 Nexus에 업로드 |
이렇게 하면 시스템으로 보장되는 일관성을 가질 수있다.
하지만 개발 cycle이 번거로워 지고 마찬가지로 IDE를 여러개 띄워야하는 문제점은 여전히 상존한다.
3. 멀티 모듈 단일 프로젝트
프로젝트는 하나이고, 그 안에 여러개의 모듈을 설치 가능한 방법이다. 그러므로 IDE도 하나만 사용하며, 시스템으로 보장되는 일관성과 빠른 개발이 가능하다.
(1) 멀티모듈 아키텍처의 개념
멀티모듈 아키텍처란 하나의 프로젝트를 여러 개의 독립적인 모듈로 분리하여 유지보수성과 확장성을 높이는 설계방식이다.
(2) 멀티모듈 아키텍처의 필요성
1) 코드 모듈화 및 유지보수성
도메인별 모듈을 분리하여 코드 복잡도 줄인다.
2) 배포 및 확장 용이
특정 모듈만 업데이트 가능해진다.
3) 재사용성 증가
공통 기능을 별도 모듈로 분리하여 여러 도메인에서 활용이 가능하다.
4) 서비스 간 결합도 감소
도메인 간 의존성을 최소화 하여 변경에 유연하게 대응 가능
개인적으로 알게된 점은 멀티모듈 아키텍처는 DDD의 도메인 기반으로 설계를하다보면 멀티모듈화 되고,
멀티 모듈화가 된다면 MSA로 전환을 쉽게할 수있는 기반이 마련되는것을 알게되었다. 다만 추가적인 일부 작업은 더 필요하다.
III. 멀티모듈 프로젝트 구조 및 주요 모듈
1. 최상위 프로젝트 구조
현재 맡게 된 서비스의 최상위 프로젝트 구조는 다음과 같다.
project
├── user # 핵심 비즈니스 로직 (API + Application + Business)
├── support # 글로벌 공통 로직 (로깅, 예외 처리 등)
├── infrastructure # 기술 스택 관련 모듈 (JPA, Redis, Firebase 등)
├── enumerate # Enum 및 Constants 모듈
각 모듈이 독립적인 역할을 수행하며, 필요한 경우 다른 모듈을 참조하여 기능을 확장한다.
(1) user 모듈
참고) 현재는 서비스의 레거시 프로젝트와 병행하여 운영중이여서 점진적으로 핵심 비즈니스 로직 관련 user와 같은 모듈이 점진적으로 추가 될 예정이다.
1) 역할
API, Application, Business 로직을 포함한 주요 모듈
프로젝트의 도메인 비즈니스 로직을 처리하는 중심 모듈이다.
2) 내부 구조
API → Facade → Service → Component → Repository
흐름의 구조가 적용되어있다.
비즈니스 로직을 세분화 하여 유지보수성을 높였다.
이 부분은 아래의 '모듈 경계와 의존성'에서 자세히 살펴보겠다.
(2) support 모듈 (공통 기능)
1) 역할
로깅, 예외처리 등의 기능 제공
여러 모듈에서 재사용 가능하도록 설계하였다.
(3) infrastructure 모듈 (기술 스택)
1) 역할
프로젝트에서 사용하는 기술 스택을 관리하는 모듈이다.
비즈니스 로직에서 떼어내어 강결합을 방지하여 유연하게 필요한 기술을 전환하는 이점이 존재한다.
2) 처리 방식
DB : MySQL (JPA, MyBatis)
캐싱 : Redis
파일 업로드 및 저장소 관리 : S3
비동기 메시징 처리 : SQS / Kafka
푸시 알림 서비스 : Firebase
이러한 목적별 기술스택을 채택하여 데이터베이스, 캐싱, 외부 API 연동 등을 처리한다.
(4) enumerate 모듈 (Enum & Constants)
1) 역할
프로젝트에서 사용하는 Enum과 상수(Constant) 값을 관리한다.
하드코딩을 방지하고, 도메인 모델을 명확하게 정의할 수있다.
2) 예
accounts beneficiary common coupon ekyc exchangerate notification partner system user
bank cloudstorage constants currencyexchange event fcm openbanking renewalevent transaction
현 서비스에서는 이러한 방식으로 도메인 모델을 나누어서 각각 정의하고 있다.
IV. 멀티모듈 아키텍처에서 살펴 볼 관점
1. 멀티 모듈에서의 모듈 경계 및 의존성 관리
// 단일 모듈 방식 (모든 기능이 한 곳에 집중)
@Controller
public class UserController {
@Autowired
private UserService userService;
@Autowired
private TransactionService transactionService; // 강결합 문제
}
// 멀티모듈 방식 (독립적인 모듈로 분리)
@Controller
public class UserController {
@Autowired
private UserFacade userFacade;
}
@Service
@Transactional
public class UserFacade {
private final UserService userService;
private final EmailService emailService;
public void createUser(User user) {
userService.createUser(user);
emailService.sendWelcomeEmail(user);
}
}
(1) 기존 단일 모듈
Controller → Service → Repository
형태로 모든 기능이 한 모듈 안에서 실행된다.
서비스가 같은 DB를 직접참조 하고, 서로 강결합을 가진다.
(2) 멀티 모듈
앞서 언급한 방식으로
API (Controller) → Facade → Service → Component → Repository
형태로 더 세분화한다.
즉 모듈간 직접 참조를 최소화하고, 독립성을 유지한다.
특히 Facade계층이 필요한 이유는 서비스 간의 복잡한 호출을 단순화하고 조정하기 위한 계층이다.
이 과정에서 컨트롤러에서 트랜잭션을 관리하지 않을 수있게 된다.
2. 횡단 관심사 처리 (AOP, 캐싱, 로깅)
(1) 기존 단일 모듈
서비스에서 직접 로깅, 예외 처리, 캐싱을 구현했다.
(2) 멀티 모듈
AOP와 공통 모듈(config)을 활용하여 공통 기능을 한 곳에서 관리한다.
1) 멀티모듈에 적용되는 횡단 관심사
로깅 : AOP로 처리
예외처리 : @ControllerAdvice
캐싱 : Redis + @Cacheable
비동기 처리 : @Async
V. 실패할 수 있는 멀티 모듈 프로젝트
학습과정에서 멀티모듈 프로젝트에 대한 실패 케이스를 살펴보기 위해 '우아한 멀티모듈 세미나' 내용을 살펴보았고 해당내용을 고민했다.
만약 멀티 모듈을 '공통되는 코드 제거'라는 단순한 접근을 한다면 어떻게 될까?
즉 공통되는 Common(core)라는 모듈을 만든다. 점점 Common이 커지게 되면 Common안에 비즈니스 로직이 흐르게 된다.
결국 다른 애플리케이션은 날씬하고 Common만 큰 프로젝트로 구성하게 된다.
즉 '코드를 모듈화 한다 == 중복을 최소화한다’ 라는 방식으로 common에 추가 코드를 몰아 넣은 결과물이다.
1. 스파게티 코드
에컨데 200 ~ 300개가 넘는 클래스가 서로를 의존하는 스파게티 코드라면은 common을 분해하고 싶어도 분해가 불가능하다.
즉 특정 기능이 사라져도 의존도가 높아서 클래스를 제거할 수가 없게된다.
2. 의존성 덩어리
Common 모듈에서는 애플리케이션들이 사용할 수 있는 의존성들을 모두 품게된다. 결국 사용하지 않는 것에 대해서도 의존을 하고 있어야한다.
- 위 스프링부트 설정 등에 의해서 의도하지 않은 설정 값이 트리거로 발동되어서 예기치 못한 에러를 발생시키는 상황이 발생하기도 한다.
예컨데
- common 모듈에 dynamodb 의존성을 추가해 둔 상태이다.
- batch 모듈은 webflux로 구현되어 있고 이 경우 webflux는 netty를 띄운다.
- netty db가 띄워진다고 생각했지만, common 모듈의 daynamodb 의존성에 의해 jetty가 떠있게 된다.
3. 공통 설정
위 이미지와 같이 애플리케이션을 구성할 때 각 모듈이 DB 커넥션을 필요로 하는 수가 다르다.
하지만 common모듈에 설정을 몰아둔 경우에는 모든 모듈이 이 설정에 기속하게 된다. 이렇게 된다면 DB커넥션은 더이상 제공해줄 수 없어서 장애가 발생한다.
4. 문제의 원인
모듈에 대한 정의가 모호한 것으로 볼 수있다.
이 경우 모듈화란 무엇인가? 무엇을 중심으로 정의를 내려야할까?
분량 문제로 다음에 계속...
- 참고자료
'배움 __IL > addtionalBackEnd' 카테고리의 다른 글
StreamAPI 의 실행순서와 병렬처리 (0) | 2025.02.07 |
---|---|
스프링과 싱글톤: 효율성과 안정성의 조화 (0) | 2025.01.12 |
N+1 문제와 Fetch 전략을 모르면 JPA를 잘못 쓰고 있을수도... (0) | 2025.01.04 |