[쇼핑몰 프로젝트] 멀티모듈 도입기 -1편

대원·2024년 12월 23일

쇼핑몰

목록 보기
4/13
post-thumbnail

배경

현재 진행하는 토이프로젝트에서 멀티모듈을 적용해보기로 했다.

소프트웨어 개발에서 유지보수성과 확장성은 프로젝트에서 아주 중요한 요소라고 할 수 있는데, 현대적 시스템 개발에서는 이러한 요소를 효과적으로 지원하는 아키텍처 설계가 필수적이라고 할 수 있다.

다양한 기업의 JD를 살펴보면 많은 우대사항에 ‘클린 아키텍처’와 같은 내용이 있는 것을 알 수 있다. 바로 클린아키텍처를 적용하기 보다는 어떠한 아키텍처든 밑바탕이 되는 멀티모듈을 먼저 학습하고 이를 토이프로젝트에 적용해보고자 한다.


모듈이란?

  • 상호 관련성이 높은, 재사용 가능한 패키지 및 리소스(이미지, XML 파일 등)들을 한데 묶어 지칭
    Java 9 Modules 이해하기

모듈은 각 모듈끼리 독립적으로 개발, 빌드, 테스트, 배포가 가능하다.
이 말의 의미를 잘 곱씹어보자.

그렇다면 모듈이 여러개라면 어떻게 될까?

멀티모듈

하나의 애플리케이션에서 모듈별로 역할과 책임을 적절하게 할당시키고 분배한다면, 특정 모듈을 변경하거나 기능을 추가하거나 변화가 생긴다면 해당 모듈만 개발, 빌드, 테스트 후 배포가 가능하다는 뜻이다.

이 말은 곧 애플리케이션을 개발 및 유지 보수하는데 있어서 비용을 상당히 감소시키고 효율화를 꾀할 수 있다는 방증이다.

가령 하나의 모듈로만 관리하고 운영하는 프로젝트가 있다고 하자.
특정 코드에 변화가 생기거나 기능을 추가할 일이 생긴다면 모듈이 하나이기 때문에 연관되지 않은 도메인과 서비스들도 모두 빌드 후 배포를 해야한다.
(물론 모듈의 규모에 따라 특정 상황에선 빌드 비용이 증가할 수도 있긴할 수도 있다.)

도커와 같은 컨테이너 기술과 멀티모듈의 기술이 결합되어 사용시엔 확장성, 유지보수성, 효율성 측면에서 큰 이점을 줄 수 있으리라 생각된다. 여기서 더 확장되고 개선된다면 MSA환경을 구축할 수 있을 것이다.


본래의 모듈 및 패키지 구조

본래 현재 토이프로젝트의 아키텍처는 일반적은 레이어드 아키텍처의 구조를 따랐다.
즉, 하나의 모듈 내에서 여러 패키지별로 controller, service, repository와 같은 형식으로 구성했다.

(하단 그림 참조)


초기 설계한 멀티모듈의 구조

처음 멀티모듈 설계시에 인프콘, 테크 블로그에서 작성한 멀티모듈과 관련한 글 또는 영상 등을 참고했다.

당연한 이야기지만 현재 내 역량과 수준에서는 영상과 글 등에서 주장하는 개념들이 다소 모호하게 느껴지거나 개념적으로 잘 와닿지 않다고 느껴졌다. 그리고 연설자, 작성자분들께서 공유해주신 멀티모듈을 도입하면서 느끼거나 경험하셨던 것들을 직접적으로 토이프로젝트에 1대1매칭해서 대입해보기에는 규모도 다르다고 느껴졌다.

무엇보다 소프트웨어 개발의 대부분의 분야가 그러하듯이 절대적 정답지가 없는 상황이었기 때문에 주관적인 시선이 개입될 수 밖에 없다고 느껴졌다. 그 주관적 시선이라는게 우열을 나눈다는 것이라기 보다는 해당 개발팀의 컨벤션 및 문화 및 시선에 따라서 다양화될 수 있다고 생각했다.

따라서 일단은 간략하게나마 내가 필요한 부분을 임의적으로 나누고 일단은 먼저 멀티모듈을 도입해보기로 생각을 했다. 그렇기 때문에 모듈별로 뭐가 되었든 쪼개보고 각 컴포넌트간의 의존관계 주입이 적절히 되게끔만 해보자는게 1차 목표였다. 그 후에 리팩토링을 하면서 구조를 변경하는쪽으로 만들어보자는 생각을 하였다.

아래는 내가 처음에 설계한 멀티모듈의 구조이다.

의존성 순서 및 모듈의 구조는 아래와 같다.

위와 같이 web, core, core, external, infra 모듈로 분리하였다.

참고로 모든 모듈간의 의존성은 implementation 방식을 이용하여 모듈 내의 의존하는 라이브러리를 외부의 모듈이 알 수 없게끔 구성하였다.

참고) Gradle 모듈 관리

  • Implementation
    • 해당 의존성을 모듈 내부에서 사용할때 이용
    • 모듈간 결합도를 낮춰 캡슐화 가능성 증대
    • 컴파일패스가 작아지므로 빌드속도가 빠름
  • api
    • 해당 모듈을 외부 API로 노출할 때 사용
    • 해당 모듈을 의존하는 다른 모듈에서도 해당 API를 사용가능
    • 공용 API를 외부에 노출함으로써 재사용성이 높아짐
    • 컴파일패스가 커지므로 빌드성능 저하 가능성 높고 모듈간 의존성이 높아짐으로써 변경에 취약하다.

각 모듈의 패키지 구성

  • web 모듈
    • controller, filter, config, argument 등 관리
    • security, spring-web, websocket 등 의존관계
├── 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    
  • core 모듈
    • domain 객체, repository, service 등 관리
    • spring, jpa, redis 등 의존관계
├── 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  
  • common 모듈
    • errorcode, apiexception 등 관리
    • spring 의존관계
  .
├── 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  
  • external모듈
    • 외부 API 호출과 같은 것들을 모아놓은 모듈
  .
├── 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 모듈
    • redis 등 cache 목적 외부 DB(?) 등 관리
  └── infra
    └── config
        └── redis
            └── RedisConfig.java

상기의 방식으로 분리하고 수많은 컴파일 에러와 의존성을 해결하는 꽤나 많은 시간이 들었다.
테스트는 어떻게 분리할 것이며.. 수 없이 많이 깨지는 의존관계와 컴파일에러 앞에서 많은 시간을 할애했다.

당연히 확정적으로 결정된 내용은 아니고 계속해서 수정이 필요할 듯 하지만, 멀티모듈을 짧게나마 도입하면서 내가 경험하고 고민한 내용은 다음과 같다.

도입 과정에서의 문제점과 고민

그러나 상단의 방식은 내 스스로 생각해도 많은 무리가 있다고 느껴졌다.

내가 파악한 문제점과 고민했던 내용은 다음과 같다.

1. 모듈의 이름이 불명확하다.

  • ’core’라는 이름이 정확히 무엇을 수행하는지 헷갈린다.
  • 회사 그리고 속한 팀에 따라서 네이밍 컨벤션은 다를 수 있지만 core라는 이름이 조금은 모호한 것처럼 느껴졌다.

2. 의존관계 다이어트가 필요하다.

  • 나름 분리한다고 분리했지만, common모듈에서 Spring에 관한 의존성이 생겼다.
  • 그 이유는 common 모듈 내에서 ErrorCode가 HttpStatus를 의존하는데 해당 클래스가 Spring 라이브러리에 의존적이라는 점이다.
  • common 모듈이 POJO로만 구성되어야 한다는 바뀌어서는 안된다.

3. infra와 external의 의미가 불명확하다.

  • 외부 API 호출이 필요한 feign client를 이용하는 toss_api 등을 위치시켰다.
  • 그러나 개념상 infra와 차이가 꽤 모호한 것처럼 느껴진다.
  • 따라서 infra, external을 하나로 단일화시키는게 적절한 것처럼 느껴진다.
  • 또한 infra에 Redis와 같은 NoSQL DB 설정 내용을 구성하였는데 RDB도 그럼 따로 빼야하는 것 아닌가 하는 고민이 들었다.

4. Entry-Point 추가 시에 문제점

  • 현재는 Web이 유일한 API의 입구이다.
  • 그러나 Batch 또는 Kafka 와 같은 새로운 Entry point가 추가될 경우 해당 모듈이 의존하게되는 대상에서 불필요하게 의존성이 생성될 문제가 있다.
  • 예를 들어 Batch등과 같은 모듈에서는 비즈니스 로직에 의존관계가 있지, QueryDsl, JPA와 같은 DB종속적인 특정 라이브러리에 의존적이어서는 안되는데 현재 모듈 구조에서는 이를 해결할 수 없다.

5. 계층에 대한 고민

  • 하나의 모듈로 프로젝트를 관리할때는 전통적인 레이어드 아키텍처의 구조를 따랐다.
  • 따라서 해당 패턴에 맞게끔 구성하였는데, web과 core(도메인 포함 로직)을 분리하는 순간부터 Entry-point부분과 비즈니스 로직을 포함한 계층 분리를 어떻게 하는 것이 좋을지 또 어떻게 분리하는 것이 좋을지에 관련한 고민을 했다.

6. 모듈 분리로 인해 컴포넌트 스캔을 하지 못한다.

  • 단일 모듈 이용시에는 경험하지 못했던 Bean을 찾지 못하는 상황이 발생했다.
  • 단일 모듈 사용시에는 @SpringBootApplication이 작성된 이하의 패키지를 모두 Entity Scan을 해서 Bean으로 자동 등록을 해주지만, 모듈을 분리하는 시점에서는 명시적으로 스캔 대상 등을 작성해주지 않으면 Bean 등록과 관련된 에러가 지속해서 발생하게된다.

7. 테스트 코드 계속 실패하는 문제

  • Controller Layer는 @WebMvcTest를 사용하고, Service Layer는 @SpringBootTest를 사용했고, Repository Layer는 @DataJpaTest를 사용했다.
  • 현재 해결하지 못한 문제이다.

마무리

멀티모듈 도입 자체가 처음인지라 모듈별 의존성 관리 문제, 네이밍 문제, 컴파일 에러 해결, 단일 모듈 시에는 경험하지 못한 컴포넌트 스캔 문제, 테스트 시 의존성 관련 문제 등.. 많은 문제를 겪었다.

실무에서는 많은 곳에서 당연히 사용하는 개념인데 모든 것이 첫 걸음마인지라 많이 헤매개 되었다.

앞으로의 계획은 다음과 같다.

  1. 모듈 이름 및 역할 명확화
  2. 의존성 정리 및 Spring 의존 제거
  3. Infra와 External 통합 여부 검토
  4. Entry-Point 다변화 대비 설계 개선

전술한 문제점들을 해결하며 이어서 글을 작성해보겠다.

profile
고민하고 공부하는 사람

0개의 댓글