앞선 글에서는 멀티모듈이란 무엇인지 이론적인 부분을 중점적으로 살펴보았습니다.
이젠 실전으로 적용해볼 때입니다!
현 시리즈의 1번에서 적용했던 모듈 구성은 이해가 부족한 것 같아 좀 더 공부한 뒤 수정해보았습니다.
spring boot 2.7
gradle
jpa
myabtis, redis
api-diary
application
auth-jwt
core-mysql
core-redis
크게 위와 같이 모듈을 나누어보았는데요, 기본적인 구성은 레이어드 아키텍처 기반입니다. 일반적인 개발자에게 익숙한 형태죠.
jpa 특성상 domain과 infra를 완전히 나누는게 큰 의미가 없다고 생각되어서, core로 묶어서 하나의 모듈로 생성하였습니다.
레이어드 아키텍처
api-diary
┕ common
┕ config
┕ exception
┕ diary, comment ....
┕ request
┕ response
┕ mapper (serviceDtoMapper)
┕ controller
presentation 레이어를 담당하고있습니다. 즉, 외부와 통신하기 위한 요청과 응답을 담당하고 있습니다. controller와 request, response 등이 해당됩니다.
common
config
exception
application
┕ common
┕ exception
┕ diary, comment...
┕ serviceDto
┕ service
controller에서 넘어온 요청을 처리하는, 서비스의 비즈니스 로직을 담당하는 모듈입니다.
common
exception
auth-jwt
┕ application
┕ serviceDto
┕ service
┕ exception
┕ security
┕ jwt
┕ authfilter
┕ tokenprovider
...
┕ securityconfig
이 auth 모듈에서는 presentation 레이어를 다루지 않습니다. api모듈에서 요청을 다 받고 반환까지 다루기 때문이죠.
그래서 auth와 관련된 로직만을 다룰 수 있도록 분리하였습니다.
security
common
┕ domain
┕ MemberRole
┕ exception
┕ exception response
앞선 포스트에서 말했듯이, common은 모든 모듈이 의존하고있는 모듈이므로 최대한 가볍고, 다른 모듈 등에 최대한 의존성이 적고 비즈니스 로직이 존재하지 않아야 합니다.
저의 경우 공통 exception Response와 enum 엔티티가 포함되어있습니다.
core-mysql
┕ config : queryDSLConfig, S3Config
┕ entity
┕ exception
┕ repository
┕ util
┕ imageManager
┕ S3ImageUploader
┕ PasswordEncryptor
...
┕ vo
현재 프로젝트에서 사용하는 infra는 mysql와 redis로 2개입니다. 한 모듈에 하나의 책임을 갖게 하기 위해서 인프라별로 모듈을 분리했습니다.
두 모듈 다 jpa를 사용하고 있고, mysql의 경우 프로젝트의 루트로 사용하고 있습니다.
redis는 jwt refresh token을 담아두기 위한 저장소로 쓰이고 있습니다.
간단한 데이터만을 저장하기 때문에 db의 효율성을 위해서도 좋고, 데이터를 생성 할 때 지정한 만료시간이 지나면 자동으로 삭제되기 때문에 따로 batch작업을 하지 않아도 되어 편리하기때문에 사용하고 있습니다.
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.17'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
subprojects {
group = 'com.mmd'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
}
dependencies {
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
}
// common 모듈을 의존한다.
configure(subprojects.findAll {it.name != 'common'}) {
dependencies {
implementation project(':common')
// test
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
test {
useJUnitPlatform()
}
}
공통으로 사용되는 의존성(예를들어 lombok)은 루트 프로젝트의 gradle.build
에 명시했습니다.
그리고 settings.gradle
에 다음과 같이 하위 모듈에 대한 설정도 기입해줍니다.
rootProject.name = 'mmd'
include 'application'
include 'core-mysql'
include 'core-redis'
include 'auth-jwt'
include 'api-diary'
include 'common'
dependencies {
implementation project(':application')
implementation project(':auth-jwt')
// spring web
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// spring jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Swagger
implementation group: 'io.springfox', name: 'springfox-boot-starter', version: '3.0.0'
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '3.0.0'
}
bootJar { // 다른 dependency와 함께 build해야함
enabled = true
}
jar {
enabled = false
}
api모듈에서는 application과 auth를 의존합니다.
이 프로젝트는 web을 가정하고 만든 백엔드 프로젝트이므로 web의존성을 가지고 있는데요, 이러한 web 의존성은 모두 api모듈에 넣어줍니다.
api모듈은 domain모듈을 의존하지 않음으로써 도메인 계층에 어떤 로직이 있는지 전혀 알지 못하고 사용하지도 못합니다.
dependencies {
implementation project(':core-mysql')
implementation project(':core-redis')
// spring web
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly "jakarta.transaction:jakarta.transaction-api"
// spring jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure'
}
bootJar {
enabled = false
}
jar {
enabled = true
}
application 모듈에서는 서비스의 비즈니스만을 다룹니다.
서비스 비즈니스에 필요한 도메인과 관련된 core계층만을 의존하게 됩니다.
api계층을 의존하지 않아 요청/응답과 전혀 상관없이 로직이 구성됩니다.
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins{
id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}
dependencies {
// mysql
runtimeOnly 'com.mysql:mysql-connector-j'
// jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// querydsl
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
// amazon s3
implementation "org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE"
implementation 'org.springframework:spring-web'
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
bootJar {
enabled = false
}
jar {
enabled = true
}
core-mysql에서는 mysql과 관련된 의존성과, jpa와 queryDSL에 대한 의존성을 갖고있습니다.
그리고 이미지를 업로드하는 기능에서 aws s3을 이용하므로 이에 대한 의존성도 추가하였습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
bootJar {
enabled = false
}
jar {
enabled = true
}
redis와 관련된 로직을 작성할 때 spring data redis를 사용하므로 이에 대한 의존성을 갖고 있습니다.
여기까지 개인 프로젝트에서 멀티 모듈을 구성하고, 직접 build하면서 구성해보았습니다.
사실 여기까지 이해하고 적용하는데 굉장히 많은 시간이 걸렸습니다.
큰 프로젝트에서는 모듈이 더욱 세분화되고, 관련된 설정들도 더욱 자세하겠죠?
아직 부족한 점도 많고 공부할 것도 많아서 어렵지만 전반적인 아키텍처에 대한 이해도가 높아졌다는 느낌이 들어서 뿌듯합니다 ㅎㅎv
아직 용어와 개념이 많이 헷갈리지만..