스프링 부트 단일 모듈 코드에 멀티 모듈을 적용하여 프로젝트 구조 개선하기

jonghyun.log·2023년 5월 12일
18

spring

목록 보기
7/7
post-thumbnail

이번 글에서 부터 코드에 직접 적용해가면서 리팩토링 해가는 과정을 정리하도록 하겠습니다.

앞서 제시한 문제점들을 보다 효율적으로 적용하기 위한 방법이 어떤것이 있을까 고민하던중
멀티 모듈 의 존재에 대해 알게 되었습니다.

멀티 모듈이란 특정 코드를 하나의 단위로 묶어서 독립된 개체로 사용하는 것으로
또한, 각 모듈은 독립적으로 빌드할 수 있다는 특징이 있습니다.

이 글에서는 기존 프로젝트의 도메인 , 외부 api와 통신하는 코드, 영속성 계층의 코드 등 특정 단위의 코드를 묶어서 모듈화 하겠습니다.

멀티 모듈에 대해 더 알고 싶은 분들은 여기로
실전! 멀티 모듈 프로젝트 구조와 설계 | 인프콘 2022
멀티 모듈 적용법을 참고한 블로그
향로님의 멀티 모듈 정리글
멀티 모듈의 개념을 참고한 블로그

모듈화를 해야하는 이유

우선 기존 프로젝트 구조를 대대적으로 뜯어고치기 위해 기존 소스코드가 어떤식으로 되어 있는지 보겠습니다.

위 소스코드도 크게 보면 하나의 모듈 로 이루어져 있습니다. 전체 프로젝트 코드의 맨 위 패키지에 해당하는 backend 패키지가 루트 모듈 로써 모든 코드를 가지고 있는 형태입니다.

그러면 이번에는 소스코드를 좀 더 펼쳐서

제 나름대로 코드 구조를 정리한다고 각 기능별로 디렉토리를 나눠 놓고

각 기능 패키지 안에 컨트롤러 서비스 레포지토리와 엔티티들을 넣어놓는 구조를 가지고 있습니다.
이전 글에서 언급한 영속성 계층의 도메인과 비즈니스 레이어의 도메인이 섞여 있던 문제
가 생길 수 밖에 없는 구조입니다.

Vote 라는 기능을 담당하는 패키지안에 도메인 패키지를 만들고 그 안에서 엔티티를 관리하다 보니
개발자 입장에서 해당 클래스를 가지고 서비스 클래스와 레포지토리 클래스안에서 사용하도록 혼동하기 쉬운 구조인 것이죠.

즉, 개발하는 입장에서 혼동하지 못하게끔 각 계층을 물리적으로 구분짓는 프로젝트 구조가 필요합니다.

프로젝트 모듈 구조

저의 모듈화의 목표는 기존에 뒤섞여 있는 계층형의 코드를 분리하여 관리를 용이하게 하기 위함입니다.

따라서 기존의 코드에서

  1. 컨트롤러단을 위한 모듈 - Presentaion layer
  2. 서비스(비즈니스 도메인)을 위한 모듈 - Business Layer
  3. 레포지토리(영속성 계층)을 위한 모듈 - Persistence Layer
  4. 외부 코드와 통신하기 위한 코드 모듈

이렇게 네가지의 모듈을 만들어서 분리하는 작업을 하겠습니다.

계층형 레이어에 대한 더 자세히 알고 싶으신 분은 여기로
계층형 레이어 정리글

모듈간 의존성 설명

앞으로 만들 모듈간의 의존성은 위의 시퀀스 다이어그램과 같습니다. (화살표는 의존관계 표시)

각 멀티 모듈을 모두 묶어서 빌드를 하기 위해서는 빌드를 하는 모듈에서 모든 모듈의 존재를 알고 있어야 합니다.
즉, 빌드를 시작하는 모듈에서 모든 모듈의 의존성을 가지고 있어야 하는 것이죠.

이를 위해 presentation layer에 대응하는 chooz-api 모듈에서 모든 의존성을 가지고 있게 만들고 빌드 진행과 전체 프로젝트 실행 파일을 가지고 있게끔 만들겠습니다.

멀티 모듈 시작하기

멀티 모듈을 위해서는 크게 두가지의 과정을 거쳐야 합니다.

  1. 전체 프로젝트 루트 디렉토리에서 공통 빌드 세팅
  2. 생성한 모듈 안에서 빌드 세팅

1. 전체 프로젝트 루트 디렉토리에서 공통 빌드 세팅

1-1. build.gradle로 프로젝트 공통 세팅 설정

시작하기에 앞서 우선 기존 build.gradle 을 수정해줘야 합니다.

위 사진 기준 root 패키지의 build.gradle 입니다.

//build.gradle 파일
plugins {
	id 'org.springframework.boot' version '2.7.4'
	id 'io.spring.dependency-management' version '1.0.14.RELEASE'
	id 'java'
}

allprojects {
	group = 'com.chooz'
	version = '0.0.1-SNAPSHOT'
	sourceCompatibility = '11'
    
    repositories {
		mavenCentral()
	}

}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

subprojects { // 각 모듈에 적용할 공통 설정
	apply plugin: 'java'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'

	dependencies { // 롬복은 공통적으로 사용하는 의존성이니 여기서 끌어다 사용
		compileOnly 'org.projectlombok:lombok'
		annotationProcessor 'org.projectlombok:lombok'
	}

	tasks.named('bootJar') { //빌드할 때 bootjar 파일로 하겠다는 의미
		enabled = false  
	}

	tasks.named('jar') { //빌드할 때 jar 파일로 하겠다는 의미
		enabled = true
	}

	tasks.named('test') {
		useJUnitPlatform()
	}
}

위 코드에서 주의깊에 봐야할 곳은 subprojects 부분입니다.
이 속성안에 있는 코드들은 프로젝트 각 모듈들에 공통으로 적용하겠다는 의미입니다.

기존에 있던 dependencies 부분은 다른곳에 임시로 잘 저장해놓고 날려줍니다.
(추후에 각 모듈에서 필요한 의존성만 끌어다 사용할 예정입니다.)

그 이후 루트 모듈에서 새 모듈을 생성해주도록 합시다.

위 사진은 컨트롤러 단을 위한 모듈로 api의 명세를 나타내는 부분이기 때문에 저희 프로젝트의 이름과 합쳐 chooz-api라고 명명하였습니다.

같은 방법으로 영속성 계층을 위한 모듈외부 코드와 통신하기 위한 코드 모듈
만들어주도록 합니다.

프로젝트의 각 모듈을 생성한 모습입니다.

영속성 계층 모듈외부 통신 모듈 은 각각 storage , client 로 만들었습니다.

2. 각 모듈 gradle 빌드 세팅하기

  1. 루트 디렉토리의 settings.gradle 안에 추가할 모듈 include 추가
  2. 생성한 모듈의 build.gradle 작성
    • 모듈 내에서 사용되는 라이브러리 의존성 추가
    • 다른 모듈의 의존성이 필요하면 다른 모듈의 의존성도 추가
  3. gradle 리프레쉬 클릭해서 생성한 모듈 빌드에 추가

각 모듈을 추가할 때 마다 위의 과정을 거쳐주면 됩니다.

a. 프레젠테이션 레이어 모듈 - chooz-api

프레젠테이션 레이어(presentation layer)는 기존의 컨트롤러단에 대응하는 계층으로
외부와 상호작용하는 역할을 담당합니다.

a-1. settings.gradle 에서 각 모듈 빌드파일 include 하기

//root 디렉토리 settings.gradle 파일
rootProject.name = 'chooz'

include 'chooz-api'

setting.gradle에서 각각 모듈의 정보를 include를 통해 포함할 모듈의 정보를 알려주어야 합니다.

a-2. 생성한 모듈의 build.gradle 작성

빨간색으로 표시한 파일들은 빌드시 자동으로 생성되는 파일들 입니다.
저희는 나머지 파일 src , build.gradle만 만들어주면 되는것이죠.

//chooz-api 내부 build.gradle
tasks.named('bootJar'){ // bootJar 세팅을 켜기
    enabled = true
}

tasks.named('jar'){ // jar 설정은 끄기
    enabled = false
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

bootJar 와 jar의 차이
bootJar : dependecies와 클래스 파일을 모두 묶어서 빌드
jar : 클래스만 묶어서 빌드

다른 dependencies와 같이 빌드하기 위해 bootJar 설정을 켜줬습니다.

a-3. gradle 리프레쉬 클릭해서 생성한 모듈 빌드에 추가

그리고 다 세팅했다면 인텔리제이 우측의 gradle 탭을 누르고 탭 메뉴의 + 버튼 옆의 빌드 리프레쉬 버튼을 눌러줍니다.

위 과정을 거쳐야 chooz-api 모듈안으로 dependencies를 불러와서 java class 작성이 가능합니다.

package com.chooz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ChoozApplication {

    public static void main(String[] args) {
        SpringApplication.run(ChoozApplication.class, args);
    }
}

위에서 언급한대로 chooz-api 에서 모든 모듈의 의존성을 가지고 있게 만들 예정이기 때문에
spring 시작 main 메서드가 들어있는 클래스는 chooz-api 모듈안에 만들었습니다.

이상없이 잘 실행되는 모습 ㅎㅎ

b. 비즈니스 레이어 모듈 - domain

비즈니스 레이어(Business Layer)는 기존 서비스 코드에 대응하는 계층으로 비즈니스 로직과 관련된 일련의 작업을 합니다.

기존 프로젝트의 서비스 클래스들과 로직을 묶어서 도메인 모듈을 만들겠습니다.

b-1. 루트 디렉토리의 settings.gradle 안에 추가할 모듈 include 추가

 //root 디렉토리 settings.gradle 파일
rootProject.name = 'chooz'

include 'chooz-api'
include 'domain'

b-2. domain 모듈의 build.gradle 작성

// domain 모듈내 build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

위에서 모듈 의존성에서 설명한대로 chooz-api 모듈에서 domain 모듈의 의존성이 필요하므로 chooz-api 모듈에서 의존성을 추가해주도록 하겠습니다.

b-3. gradle 리프레쉬 클릭해서 domain 모듈 빌드에 추가

domain 모듈도 성공적으로 gradle 인식에 성공했습니다.

c. 영속성 레이어 모듈 - db-jpa

영속성 계층(Persistence Layer)은 데이터베이스 연결(JDBC)을 다루기 위한 모듈로써
지금은 jpa를 사용하기는 하지만 추후에 다른 툴로의 변경 가능성을 열어두기 위해
storage 패키지 안에 모듈을 생성하겠습니다.

c-1. 루트 디렉토리의 settings.gradle 안에 추가할 모듈 include 추가

 //root 디렉토리 settings.gradle 파일
rootProject.name = 'chooz'

include 'chooz-api'
include 'domain'
include 'storage:db-jpa'

c-2. db-jpa 모듈의 build.gradle 작성

// db-jpa 모듈의 `build.gradle`
dependencies {
    implementation project(':domain') // 의존성 역전
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    runtimeOnly 'com.h2database:h2'
}

다른 모듈과 과정이 거의 똑같으나 특이하게도 db-jpa 모듈에서 domain 모듈을 의존하고 있습니다.

기존의 컨트롤러 -> 서비스 -> 레포지토리 구조와는 다른 의존성을 가지고 있지요.
이것에 관해서는 추후에 다른 포스팅으로 정리하도록 하겠습니다.

c-3. gradle 리프레쉬 클릭해서 db-jpa 모듈 빌드에 추가

d. 외부 코드와 통신하기 위한 코드 모듈 - client:kakao,naver

외부 api와 통신하기 위한 모듈로 소셜 로그인 모듈이 정의되어 있습니다.

d-1. 루트 디렉토리의 settings.gradle 안에 추가할 모듈 include 추가

//root 디렉토리 settings.gradle 파일
rootProject.name = 'chooz'

include 'chooz-api'
include 'domain'
include 'storage:db-jpa'
include 'client:kakao'
include 'clinet:naver'

d-2. kakao,naver 모듈의 build.gradle 작성

각 모듈안에 build.gradle 을 생성하고 다음과 같이 필요한 의존성을 작성해줍시다.

d-3. gradle 리프레쉬 클릭해서 client:kakao,naver 모듈 빌드에 추가

마지막 모듈들도 성공적으로 gradle에 추가했습니다.

추가) 마지막으로 chooz-api는 모든 모듈에 대해 의존성이 있으므로 의존성 추가하기

tasks.named('bootJar') {
    enabled = true
}

tasks.named('jar') {
    enabled = false
}


dependencies {
    implementation project(':domain')
    implementation project(':storage:db-jpa')
    implementation project(':client:kakao')
    implementation project(':client:naver')
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

chooz-api 모듈에서 다른 모듈에 대한 의존성을 추가해줍니다.

멀티 모듈의 장점 - 코드 단위에서 import 컨트롤 가능

기존에 단일 모듈에서는 접근지정자만 맞다면 모든 패키지에서 특정 코드를 import로 불러올수가 있었습니다.

하지만, 멀티 모듈에서 의존성이 연결되어있지 않는 부분에서는 import가 불가능합니다.

이런 패키지 구조에 chooz-api 모듈은 모든 의존성을 가지고 있고 client:kakao 모듈은 chooz-api의 의존성이 없습니다.

chooz-api 에서 client:kakao 모듈 안의 test 클래스 import 가능!

client:kakao 모듈에서는 chooz-apiChoozApplication 클래스 import 불가능!

배운점

기존 단일 모듈 프로젝트에서는 임의로 import 가 가능하여 협업할 때 팀원이나 혹은 미래의 자신이 임의로 import 하는것을 막을 수 없어서 처음 설계한것과는 다르게 패키지와 코드 구조가 망가질 가능성이 다분하였는데 이를 멀티 모듈을 통해 사전에 방지한다는 점에서 매우 강력한 것 같습니다.

이번에 세팅하면서 기존에 부족했던 gradle과 빌드 지식이 보충되어서 좋았습니다.
앞으로는 멀티 모듈 구조를 기반으로 기존 프로젝트 코드를 가져와서 하나씩 바꿔가는 과정을 다뤄보겠습니다.

1개의 댓글

comment-user-thumbnail
2023년 12월 12일

따라하기 쉽게 잘 작성해주셔서 많이 참고했습니다! :)

답글 달기