프로젝트 Project

멀티 모듈로 프로젝트 구성하기

달래dallae 2024. 8. 5. 13:20

앞선 글에서는 멀티모듈과 MSA란 무엇인지 이론적인 부분을 중점적으로 살펴보았는데요, 직접 멀티모듈을 적용해서 프로젝트 구조를 구성해보겠습니다.

 

1️⃣ 프로젝트 환경

  • Spring Boot 2.7
  • Gradle
  • Java 11
  • JPA
  • Rest API

 


2️⃣ 모듈 구성

레이어드 아키텍처 기반으로 멀티 모듈을 구성했습니다.

  • common
  • api-diary
  • application
  • auth
  • aws
  • core-mysql
  • core-redis

구성 시에 jpa 특성상 domain과 infra를 완전히 나누는게 큰 의미가 없다고 생각되어서, domain-infra를 묶어 하나의 모듈로 생성하였습니다.

 

1.  api-diary

presentation 레이어 모듈로서, 전체 다이어리 서비스의 API 요청과 응답을 처리하는 역할입니다.

  • common
    • API 컨트롤러 내에서 공통으로 사용하는 paging request가 있습니다.
  • config
    • 자동 API 문서화를 지원하는 Swag
    • ger를 사용하고 있기 때문에 관련 설정을 해당 모듈에 넣어주었습니다.
  • diary
    • api 요청과 응답을 처리하는 컨트롤러가 포함됩니다. diary외에도 member, friend, comment 등이 도메인별로 나뉘어져있습니다.
  • exception
    • API 요청 후 Application → Domain 로직에서 생긴 에러를 Controller로 넘겨서 응답하는 ApiExceptionAdvice가 있습니다. 서로 다룬 모듈에서 쓰이는 에러 처리는 다른 포스팅에서 다루었습니다.
  • Application main 클래스
    • @SpringBootApplication이 달린 main 클래스가 이 모듈에 존재합니다.

 

2. application

비즈니스 로직을 담당하는 모듈입니다.

  • common
    • paging request를 받아서 domain계층에서 쓰일 Pageable로 변환하는 pagingDto가 있습니다.
  • diary…
    • api 요청/응답에 대한 비즈니스 처리를 담당합니다.
  • exception
    • 비즈니스 계층에서 사용할 exception들이 정의되어 있습니다.

 

3. auth

이 auth 모듈에서는 presentation 레이어를 다루지 않습니다. api모듈에서 요청을 다 받고 반환까지 다루기 때문이죠.

그래서 auth와 관련된 로직만을 다룰 수 있도록 분리하였습니다.

  • security
    • jwt 관련 클래스들을 security에 묶은 것은, jwt 토큰의 작동 방식이 security에 의존적이라고 생각했기 때문입니다.
    • security filter chian을 기반으로 토큰을 주고받으며 이를 기반으로 authentication 과정을 마칩니다.

 

4. aws

외부 api를 사용하는 모듈은 따로 구성하였습니다.

저의 경우 aws에서는 s3만을 사용하고 있습니다.

 

5. core-mysql / core-redis

현재 프로젝트에서 사용하는 infra는 mysql와 redis로 2개입니다. 한 모듈에 하나의 책임을 갖게 하기 위해서 인프라별로 모듈을 분리했습니다.

두 모듈 다 jpa를 사용하고 있고, mysql의 경우 프로젝트의 루트로 사용하고 있습니다.

redis는 jwt refresh token을 담아두기 위한 저장소로 쓰이고 있습니다.

간단한 데이터만을 저장하기 때문에 db의 효율성을 위해서도 좋고, 데이터를 생성 할 때 지정한 만료시간이 지나면 자동으로 삭제되기 때문에 따로 batch작업을 하지 않아도 되어 편리하기때문에 사용하고 있습니다.

 

6. common

 


3️⃣ 의존성 관리

모듈이 여러개라는 뜻은, 여러개의 프로젝트로 나뉘었던 것을 한 프로젝트에 여러 모듈로 나누었다는 의미입니다.

이전에 여러 프로젝트에서 스파게티처럼 사용했던 의존성을 역할과 책임에 따라 분리하면, 각 모듈의 정체성을 한번에 파악할 수 있으며 유지보수가 쉬워집니다.

또한 gradle을 직접 빌드할 때, 의존성 관리를 잘 해야 각 모듈에 필요한 의존성이 포함되어 jar 빌드를 할 수 있습니다.

 

루트 프로젝트

build.gradle

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()
	}
}

공통으로 사용되는 의존성이나 설정을 기입해줍니다. (예: test, common)

 

setting.gradle

rootProject.name = 'mmd'
include 'common'
include 'api-diary'
include 'application'
include 'auth'
include 'aws'
include 'core-mysql'
include 'core-redis'

하위 모듈에 대한 설정을 입력합니다.

 

api-diary

dependencies {
    **implementation project(':application')
    implementation project(':auth')
    implementation project(':aws')**

    // 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
}
  • application과 auth, aws를 의존합니다.
  • security는 Controller에서 사용자 정보를 바인딩하기 위해 필요합니다. jpa는페이징을 위해 의존성을 추가했는데, 직접 request를 구현해서 사용하는 것이 더 바람직해보입니다.
  • api모듈은 domain모듈을 의존하지 않음으로써 도메인에 어떤 로직이 있는지 알 필요가 없게 됩니다.

 

application

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 모듈에서는 서비스의 비즈니스 로직을 다루기 때문에, 비즈니스에 필요한 도메인만을 의존합니다.

api계층을 의존하지 않아 요청/응답 방법에 상관없이 비즈니스 로직이 작동합니다.

aws

dependencies {
    // amazon s3
    implementation "org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE"

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

bootJar {
    enabled = false
}

jar {
    enabled = true
}

aws는 외부 api이므로, common 모듈만을 의존합니다.

 

core-mysql

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}"

    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
}

도메인 모듈 중 하나인 mysql 모듈입니다.

Mysql, jpa와 queryDSL에 대한 의존성을 갖고있습니다.

 

core-redis

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

bootJar {
    enabled = false
}

jar {
    enabled = true
}

redis 모듈에서는 spring data redis 라이브러리를 사용하므로 이에 대한 의존성을 추가해줍니다.

 

✔️ 마무리

이렇게 프로젝트의 구성과 설정은 모두 끝이 났습니다.

현재 모듈에서는 외부 api나 batch에 대한 고민이 부족한 점, 그리고 MSA로 발전하기 위해서는 도메인 기반으로 모듈을 구성하는것이 좋다는 점을 다시 고려해볼 수 있습니다.