현재 진행하는 토이프로젝트에서 멀티모듈을 적용해보기로 했다.
소프트웨어 개발에서 유지보수성과 확장성은 프로젝트에서 아주 중요한 요소라고 할 수 있는데, 현대적 시스템 개발에서는 이러한 요소를 효과적으로 지원하는 아키텍처 설계가 필수적이라고 할 수 있다.
다양한 기업의 JD를 살펴보면 많은 우대사항에 ‘클린 아키텍처’와 같은 내용이 있는 것을 알 수 있다. 바로 클린아키텍처를 적용하기 보다는 어떠한 아키텍처든 밑바탕이 되는 멀티모듈을 먼저 학습하고 이를 토이프로젝트에 적용해보고자 한다.
모듈은 각 모듈끼리 독립적으로 개발, 빌드, 테스트, 배포가 가능하다.
이 말의 의미를 잘 곱씹어보자.
그렇다면 모듈이 여러개라면 어떻게 될까?
하나의 애플리케이션에서 모듈별로 역할과 책임을 적절하게 할당시키고 분배한다면, 특정 모듈을 변경하거나 기능을 추가하거나 변화가 생긴다면 해당 모듈만 개발, 빌드, 테스트 후 배포가 가능하다는 뜻이다.
이 말은 곧 애플리케이션을 개발 및 유지 보수하는데 있어서 비용을 상당히 감소시키고 효율화를 꾀할 수 있다는 방증이다.
가령 하나의 모듈로만 관리하고 운영하는 프로젝트가 있다고 하자.
특정 코드에 변화가 생기거나 기능을 추가할 일이 생긴다면 모듈이 하나이기 때문에 연관되지 않은 도메인과 서비스들도 모두 빌드 후 배포를 해야한다.
(물론 모듈의 규모에 따라 특정 상황에선 빌드 비용이 증가할 수도 있긴할 수도 있다.)
도커와 같은 컨테이너 기술과 멀티모듈의 기술이 결합되어 사용시엔 확장성, 유지보수성, 효율성 측면에서 큰 이점을 줄 수 있으리라 생각된다. 여기서 더 확장되고 개선된다면 MSA환경을 구축할 수 있을 것이다.
본래 현재 토이프로젝트의 아키텍처는 일반적은 레이어드 아키텍처의 구조를 따랐다.
즉, 하나의 모듈 내에서 여러 패키지별로 controller, service, repository와 같은 형식으로 구성했다.
(하단 그림 참조)

처음 멀티모듈 설계시에 인프콘, 테크 블로그에서 작성한 멀티모듈과 관련한 글 또는 영상 등을 참고했다.
당연한 이야기지만 현재 내 역량과 수준에서는 영상과 글 등에서 주장하는 개념들이 다소 모호하게 느껴지거나 개념적으로 잘 와닿지 않다고 느껴졌다. 그리고 연설자, 작성자분들께서 공유해주신 멀티모듈을 도입하면서 느끼거나 경험하셨던 것들을 직접적으로 토이프로젝트에 1대1매칭해서 대입해보기에는 규모도 다르다고 느껴졌다.
무엇보다 소프트웨어 개발의 대부분의 분야가 그러하듯이 절대적 정답지가 없는 상황이었기 때문에 주관적인 시선이 개입될 수 밖에 없다고 느껴졌다. 그 주관적 시선이라는게 우열을 나눈다는 것이라기 보다는 해당 개발팀의 컨벤션 및 문화 및 시선에 따라서 다양화될 수 있다고 생각했다.
따라서 일단은 간략하게나마 내가 필요한 부분을 임의적으로 나누고 일단은 먼저 멀티모듈을 도입해보기로 생각을 했다. 그렇기 때문에 모듈별로 뭐가 되었든 쪼개보고 각 컴포넌트간의 의존관계 주입이 적절히 되게끔만 해보자는게 1차 목표였다. 그 후에 리팩토링을 하면서 구조를 변경하는쪽으로 만들어보자는 생각을 하였다.
아래는 내가 처음에 설계한 멀티모듈의 구조이다.
의존성 순서 및 모듈의 구조는 아래와 같다.


위와 같이 web, core, core, external, infra 모듈로 분리하였다.
참고로 모든 모듈간의 의존성은 implementation 방식을 이용하여 모듈 내의 의존하는 라이브러리를 외부의 모듈이 알 수 없게끔 구성하였다.
참고) Gradle 모듈 관리
├── WebApplication.java
├── argument
│ ├── Login.java
│ └── LoginArgumentResolver.java
├── config
│ ├── ThreadPoolConfig.java
│ ├── security
│ │ └── SecurityWebConfig.java
│ ├── web
│ │ └── WebConfig.java
│ └── websocket
│ └── WebSocketConfig.java
├── filter
│ ├── CustomContentCachingRequestWrapper.java
│ ├── ReqResLoggingFilter.java
│ └── session
│ ├── CustomLoginFilter.java
│ └── SessionConst.java
├── handler
│ └── WebSocketChatHandler.java
└── presentation
├── cart
│ └── CartController.java
├── category
│ └── CategoryController.java
├── chat
│ └── ChatController.java
├── home
│ └── HomeController.java
├── message
│ └── MessageController.java
├── order
│ └── OrderController.java
├── payment
│ └── PaymentController.java
├── product
│ └── ProductController.java
└── user
└── AuthController.java
├── CoreApplication.java
├── common
│ ├── ApiControllerAdvice.java
│ ├── BaseEntity.java
│ ├── LocalFileStore.java
│ └── config
│ ├── JpaConfig.java
│ └── SecurityCoreConfig.java
└── domain
├── auth
│ ├── dto
│ └── service
├── cart
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── category
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── chat
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── image
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── message
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── order
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── payment
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
├── product
│ ├── dto
│ ├── entity
│ ├── repository
│ └── service
└── user
├── dto
├── entity
├── repository
└── service
.
├── ApiResponse.java
└── exception
├── ApiException.java
├── ErrorCode.java
├── ExternalApiError.java
├── ExternalApiException.java
└── domain
├── AuthErrorCode.java
├── CartErrorCode.java
├── CategoryErrorCode.java
├── ChatErrorCode.java
├── ImageErrorCode.java
├── OrderErrorCode.java
├── PaymentErrorCode.java
├── ProductErrorCode.java
├── S3UploaderErrorCode.java
└── UserErrorCode.java
.
├── build
│ ├── libs
│ │ └── external-0.0.1-SNAPSHOT-plain.jar
│ └── tmp
│ └── jar
│ └── MANIFEST.MF
├── build.gradle
├── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ ├── java
│ └── resources
└── toss_payment
├── build
│ ├── classes
│ │ └── java
│ │ └── main
│ │ └── shoppingmall
│ │ └── tosspayment
│ │ └── feign
│ │ ├── PaymentAuthInterceptor.class
│ │ ├── PaymentClient.class
│ │ ├── PaymentConfiguration.class
│ │ ├── PaymentErrorDecoder.class
│ │ ├── PaymentProperties.class
│ │ ├── TossPaymentMethod.class
│ │ ├── TossPaymentStatus.class
│ │ └── dto
│ ├── generated
│ │ └── sources
│ │ ├── annotationProcessor
│ │ │ └── java
│ │ │ └── main
│ │ └── headers
│ │ └── java
│ │ └── main
│ ├── libs
│ │ └── toss_payment-0.0.1-SNAPSHOT-plain.jar
│ ├── resources
│ │ └── main
│ │ └── application.yaml
│ └── tmp
│ ├── compileJava
│ │ ├── compileTransaction
│ │ │ ├── backup-dir
│ │ │ └── stash-dir
│ │ └── previous-compilation-data.bin
│ └── jar
│ └── MANIFEST.MF
├── build.gradle
└── src
├── main
│ ├── java
│ │ └── shoppingmall
│ │ └── tosspayment
│ │ └── feign
│ │ ├── PaymentAuthInterceptor.java
│ │ ├── PaymentClient.java
│ │ ├── PaymentConfiguration.java
│ │ ├── PaymentErrorDecoder.java
│ │ ├── PaymentProperties.java
│ │ ├── TossPaymentMethod.java
│ │ ├── TossPaymentStatus.java
│ │ └── dto
│ │ ├── TossPaymentConfirmRequest.java
│ │ └── TossPaymentConfirmResponse.java
│ └── resources
│ └── application.yaml
└── test
├── java
└── resources
└── infra
└── config
└── redis
└── RedisConfig.java
상기의 방식으로 분리하고 수많은 컴파일 에러와 의존성을 해결하는 꽤나 많은 시간이 들었다.
테스트는 어떻게 분리할 것이며.. 수 없이 많이 깨지는 의존관계와 컴파일에러 앞에서 많은 시간을 할애했다.
당연히 확정적으로 결정된 내용은 아니고 계속해서 수정이 필요할 듯 하지만, 멀티모듈을 짧게나마 도입하면서 내가 경험하고 고민한 내용은 다음과 같다.
그러나 상단의 방식은 내 스스로 생각해도 많은 무리가 있다고 느껴졌다.
내가 파악한 문제점과 고민했던 내용은 다음과 같다.
멀티모듈 도입 자체가 처음인지라 모듈별 의존성 관리 문제, 네이밍 문제, 컴파일 에러 해결, 단일 모듈 시에는 경험하지 못한 컴포넌트 스캔 문제, 테스트 시 의존성 관련 문제 등.. 많은 문제를 겪었다.
실무에서는 많은 곳에서 당연히 사용하는 개념인데 모든 것이 첫 걸음마인지라 많이 헤매개 되었다.
앞으로의 계획은 다음과 같다.
전술한 문제점들을 해결하며 이어서 글을 작성해보겠다.