멀티 모듈 개발기(1) (feat.grale)

개발을 하다 보면 멀티모듈이라는 말을 들어보았었다. 단순히 모듈을 여러 개 사용하는 거라고 생각했다. 하지만 사용할 때마다 빌드가 되지 않는 상황이 발생하였다. 결과적으로는 "되긴 되게" 만들어 놓고 넘어갔다. 즉, 해결은 했지만 이해하지는 못한 상태였다. 멀티모듈을 사용하게 되면 중복제거를 하면 좋다는 정도만 알았고, 반대로 멀티 모듈을 도입하면서 발생하는 빌드 복잡도 증가, 의존성 관리 비용, 실행 구조의 제약과 같은 손해(트레이드오프)에 대해서는 전혀 고려하지 않았던거 같다. 지금 돌이켜보면, 문제의 원인은 멀티 모듈 자체가 아니라
구조를 이해하지 않은 상태에서 구조를 도입하려 했다는 점에 있었다.

멀티모듈을 왜 사용하는가?

멀티모듈을 왜 사용할까요? 사용하지 않으면 다음과 같이 처리 할 수 있습니다. 

일단 제가 오해하고 있었던 부분부터 고칠 필요가 있을거 같습니다.
멀티 모듈은 중복 제거를 위해서가 아니라, 변경의 영향 범위를 통제하고, 빌드/의존성/책임을 구조적으로 분리하기 위해 사용한다고 합니다. 멀티 모듈의 본질은 변경이 일어날 때, 어디까지 영향을 미칠 것인가를 구조로 고정하는 것이라고 합니다.
즉, 어떤 코드가, 어떤 코드에, 의존할 수 있고, 의존하면 안 되는지를 Gradle 수준에서 강제하는 수단이라고 합니다.

저는 다음과 같이 모듈을 분리를 하였습니다.

Root
  apps
    - api
  modules
    - jpa
    - redis
  supports
    - logging

모듈을 분리 할때 위와 같이 분리하지 않아도 된다고 생각합니다. 다만 왜 위 처럼 분리를 왜 했는지 알아야 된다고 생각합니다.
이걸 그림으로 그리면 다음처럼 그릴 수 있습니다.

이를 개념적으로 정리해보면, 멀티 모듈은 Apps, Modules, Supports와 같은 영역 간 책임을 Gradle 의존성 단위에서 제어하여 잘못된 접근을 컴파일 타임에 차단하는 구조라고 볼 수 있습니다.

그런 관점에서 보면, 현재 제 구조는 엄밀한 의미의 멀티 모듈이라고 보기는 어렵습니다.
다만 책임을 기준으로 구조를 나누고, 이후 Gradle 단위로 이를 강제하기 위한 멀티 모듈로 가기 위한 발판 단계로 이해하는 것이 적절하다고 생각합니다.

그렇다면 Gradle은 어떻게 의존성 단위를 제어할 수 있을까?

의존성 방향을 만들기전에 root에 등록을 시켜줘야 합니다. 

include(
        ':apps:????_api',
        ':supports:logging',
        ':modules:jpa'
)

이렇게 등록을 시켜주지 않으면 root 모듈에서 하위 모듈들을 제어 할 수 없습니다. 
그리고 사용하려면 

implementation project(":modules:jpa")

 

요렇게 사용해서 다른 모듈을 사용할 수 있습니다. 하지만 내부 의존성(api) 까지는 자동으로 전파되지 않습니다.

implementation ????

그 이유는 implementation는 내부 구현에 쓰는 의존성 이기 때문입니다.

그렇다면, 어떻게 해야 사용 할 수 있을까요? 바로 

// modules:jpa
dependencies {
    api("org.springframework.boot:spring-boot-starter-data-jpa")
}

요렇게 하면 상위 모듈에서도 사용이 가능합니다. 사실 api를 사용하려면 플러그인 하나 추가해야 합니다.

plugins {
    id 'java-library'
}

요 플러그인을 등록해야 그레이들에서 api라는 키워드 사용이 가능합니다.

여기서 java 플러그인 과 java-libray 플러그인의 차이를 학습할 수 있습니다.
java 플러그인은 애플리케이션 중심이며 의존성 전파 개념이 없습니다. 그렇기때문에 전부 implementation처럼 취급합니다.
java-library 플러그인은 라이브러리 중심입니다. 의존성을 api와 implemetation 두 계층으로 나누며 외부에 노출할경우 api를 사용해야 합니다.

그렇다면 어느상황에서 api를 사용해야 할까요? api는 상위 모듈이 반드시 알아야 하는 공개 계약(public contract)일 때만 사용하고
그 외에는 전부 implementation이 맞다고 합니다.

그러면 테스트는???

그렇담 운영쪽은 그렇다치는데 테스트는 어떻게 할 수 있을까요? 이것도 똑같을까요? Gradle에는 테스트 전용 클래스 패스도 존재한다고 합니다. 테스트 코드는 testImplementation, testRuntimeOnly이라고 합니다. 그렇다면 어떻게 사용할 수 있을까요? 

다음처럼 작성한다고 합니다.

testImplementation project(":supports:jackson")
testImplementation(testFixtures(project(":modules:jpa")))

특이하게 2가지 방법이 있어 모두 가져왔습니다. 그렇다면 2가지는 어떤 차이가 있을까요?

testImplementation는 운영 코드는 전혀 모르고 테스트 코드에서 사용이 가능하다는점입니다.
운영 코드에는 직접 노출 시키고 싶지 않을때 사용이 되어지며 테스트 편의성만을 위한 의존성일 때 사용이 되어진다고 합니다.

그렇다면 testFixture은 어떨까요? 이 모듈의 테스트를 돕기 위한 재사용 가능한 테스트 전용 코드라고 합니다. 
이는 테스트에서 공통 테스트 데이터를 사용, 테스트 설정, 헬퍼 클래스만 사용한다는 점에 있습니다. 
다음같은 장점이 있다고 합니다.

 

  •  테스트 전용 코드만 노출
  • 구현 캡슐화 유지
  •  테스트 재사용성 극대화
  • 구조 안정성 매우 높음

 

다만 공식적으로 testFixture을 제공하려면

id 'java-test-fixtures'

이 플러그인을 사용해야 합니다.

testFixtures를 통해 테스트 전용 코드를 공식적으로 제공하려면 java-test-fixtures 플러그인을 적용해야 합니다.
이 플러그인은 testFixtures 소스셋과 관련된 의존성 구성을 생성하여, 테스트 코드에서만 재사용 가능한 테스트 자산을
Gradle 차원에서 명확하게 관리할 수 있게 해줍니다.

마무리

멀티 모듈을 적용하면서, 모듈 간 책임을 분리하기 위한 구조로 멀티 모듈을 사용하는 것이라고 이해하게 되었습니다.
하지만 구조를 나누는 것과 실제 배포는 전혀 다른 문제였습니다.

멀티 모듈로 구성한 뒤 바로 배포를 시도했지만, 애플리케이션은 정상적으로 실행되는 것처럼 보였음에도 불구하고 아무 데이터도 조회되지 않는 상태로 동작했습니다. 처음에는 원인을 알 수 없어 로그와 설정을 하나씩 확인했고, 결국 싱글 모듈을 배포하던 방식 그대로 멀티 모듈을 배포하고 있었다는 사실을 알게 되었습니다.

AI의 도움으로 설정을 수정해 문제를 해결하긴 했지만, 왜 그런 변경이 필요한지에 대해서는 명확하게 이해하지 못한 상태였습니다.
현재로서는 단순히 "jar 파일 실행 경로를 잘못 잡았기 때문" 정도로만 인식하고 있지만, 이는 결과에 가까운 설명일 뿐 근본적인 원인은 아니라고 느꼈습니다.

다음 글에서는 멀티 모듈 환경에서 실제 배포 시 어떤 모듈이 실행 주체가 되어야 하는지, 그리고 빌드와 실행 기준을 어떻게 설정해야 하는지를 직접 학습하고 실습해보려 합니다.

댓글

Designed by JB FACTORY