👨👨👧👧 멀티 모듈?
멀티 모듈이란 개념은 한 프로젝트 안에 라이브러리처럼 사용 가능한 상태로 모듈들을 구성하고 그것들을 다른 모듈에서 가져다 쓸 수 있도록 만드는 것이라고 이해할 수 있습니다.
멀티 모듈을 구글에 검색하면 참고할 만한 문서들이 정말 많이 나옵니다.
멀티 모듈 프로젝트를 구성하는 방법 또한 친절하게 자세히 나와있어 쉽게 멀티 모듈 프로젝트를 구성할 수 있습니다.
멀티 모듈에 관한 참고 글
- 멀티모듈 설계 이야기 with Spring, Gradle
진행중이던 프로젝트를 멀티 모듈로 바꾸기를 원하는 분들도 많을 것입니다
하지만, gradle이나 멀티 모듈에 익숙하지 않은 분들이라면 고민하게 만드는 문제일 수 있죠.
이번 포스팅에서는 진행 중이던 프로젝트가 있다고 가정하고 해당 프로젝트를 멀티 모듈 프로젝트로 바꾸는 작업에 대해서 다뤄보겠습니다.
🖥 진행 중이던 프로젝트가 있어요
진행 중이던 프로젝트가 있다고 가정하기 위해 간단한 프로젝트 환경을 만들어 보았습니다.
작은 프로젝트이지만 누군가의 말대로 상상력을 발휘하여 domain 클래스도 많고 controller도 비해한 아주 큰 프로젝트라고 생각해주세요.
자세한 코드는 github에 있으니 참고 하시면 됩니다.
현재 진행중이던 프로젝트의 스펙은 다음과 같습니다.
lombok도 보이고 jpa도 보이네요. 역시나 이것뿐만 아니라 다른 여러가지 의존성이 많다고 상상해보시면 좋습니다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.3.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.blog'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
🖥 📲📱domain을 모듈로 분리하기
현 프로젝트에서 거대한 domain을 다른 모듈로 분리해 보겠습니다.
User 클래스는 다음과 같습니다.
User.java
@NoArgsConstructor
@Getter
@ToString
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String password;
@Builder
public User(Long id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
}
1. module 생성
module로 분리하기 위해서는 먼저 module을 만들어 주어야 하겠죠
root 프로젝트에서 module을 하나 만들어 주겠습니다.
다음과 같이 module-common이 생성되시면 성공입니다.
주목해야 할 부분은 settings.gradle 에 include 'module-common' 이 추가 되었다는 점입니다.
만약에 추가되지 않았다면 추가해주세요
2. module 로 코드 옮기기🚚
이제 module-common 으로 domain에 관련된 코드들을 옮기겠습니다.
프로젝트 현재 구조
build.gradle에서 jpa와 같은 domain에 관련된 코드들도 잊지 않고 옮겨줍니다.
root 프로젝트의 build.gradle
plugins {
id 'org.springframework.boot' version '2.3.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.blog'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
module-common의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}
group 'com.blog'
//module-common 에는 실행가능한 Application을 가지고 있지 않으니 아래 내용을 포함시켜 줍니다.
bootJar { enabled = false }
jar { enabled = true }
repositories {
mavenCentral()
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
하지만 지금 상태에서는 root 프로젝트에 속해 있는 코드들이 module-common에 있는 domain코드들을 사용하지 못합니다.
root프로젝트에서 module-common의 코드들을 사용하기 위해 root프로젝트의 build.gradle에 다음과 같이 추가해줍니다.
...
dependencies {
...
//코드 추가
compile project(':module-common')
}
...
그러면 에러가 사라지는 것을 확인할 수 있습니다.
3 Test 해보기🕹
그럼 이제 UserService를 테스트해봄으로써 root프로젝트에서 다른 모듈의 코드를 사용할 수 있는지 확인해보겠습니다.
UserService.java
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Long save(User user) {
return userRepository.save(user).getId();
}
}
UserServiceTest.java
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void save() {
User user = User.builder()
.name("홍길동")
.password("1234")
.build();
Long id = userService.save(user);
assertThat(id).isNotNull();
}
}
Test 결과는 성공이네요
Service를 모듈로 분리하기
이제 위의 과정을 참고하여 Service도 한번 분리해보도록 하죠.
ModuleApiApplication을 선언해 주어야 테스트가 잘 돌아갑니다.
위에서 해왔던 방법과 동일하게 적용하여 위 이미지처럼 module-api를 만들었는데, build.gradle이 다른 build.gradle과 중복되는 부분이 참 많은 것 같습니다.
module-api의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}
group 'com.blog'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
compile project(':module-common')
}
test {
useJUnitPlatform()
}
module-common의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}
group 'com.blog'
bootJar { enabled = false }
jar { enabled = true }
repositories {
mavenCentral()
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
}
이번엔 중복되는 코드를 줄여보도록 하겠습니다.
중복제거🔍
모듈들의 build.gradle에서 중복되는 부분을 제거하는 방법은 간단합니다.
root프로젝트에 있는 build.gradle에 subprojects{ }를 만들고 중복되는 코드를 넣어주면 됩니다.
root프로젝트의 build.gradle
...
//subprojects는 include된 프로젝트들을 관리합니다.
subprojects {
group 'com.blogcode'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
test {
useJUnitPlatform()
}
}
//module-api 프로젝트가 module-common의 의존하고 있음
project(':module-api') {
dependencies {
compile project(':module-common')
}
}
이렇게 하면 나머지 build.gradle은 많이 줄어들게 됩니다.
module-common의 build.gradle
bootJar { enabled = false }
jar { enabled = true }
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
}
module-api의 build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
어느덧 위에서 멀티모듈의 참고 글에서 안내하고 있는 코드와 비슷해지고 있네요.
이 글의 목적은 "진행중이던 프로젝트에서 모듈 분리를 어떻게 할 수 있을까"에 대한 글이기 때문에 어느 정도 목적을 이룬 것 같아 여기서 마무리하려고 합니다.
많이 부족한 글이지만 멀티모듈을 이해하는데 조금이나마 도움이 되었으면 좋겠습니다.
끗~