#4. 멀티 모듈 적용하기 (2)

달래·2024년 2월 15일
0

개인프로젝트

목록 보기
5/6

앞선 글에서는 멀티모듈이란 무엇인지 이론적인 부분을 중점적으로 살펴보았습니다.
이젠 실전으로 적용해볼 때입니다!

현 시리즈의 1번에서 적용했던 모듈 구성은 이해가 부족한 것 같아 좀 더 공부한 뒤 수정해보았습니다.

1. 🛠️프로젝트 스펙


spring boot 2.7
gradle
jpa
myabtis, redis

2. ⛓️모듈 구성


api-diary
application
auth-jwt
core-mysql
core-redis

크게 위와 같이 모듈을 나누어보았는데요, 기본적인 구성은 레이어드 아키텍처 기반입니다. 일반적인 개발자에게 익숙한 형태죠.

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

레이어드 아키텍처


api-diary

api-diary
┕ common
┕ config
┕ exception
┕ diary, comment ....
	┕ request
    ┕ response
    ┕ mapper (serviceDtoMapper)
    ┕ controller

presentation 레이어를 담당하고있습니다. 즉, 외부와 통신하기 위한 요청과 응답을 담당하고 있습니다. controller와 request, response 등이 해당됩니다.

common

  • 공통으로 쓰이는 paging request를 만들어 페이징에서 필요한 request에서 상속하여 사용합니다.

config

  • 요청과 응답에 대한 API 문서화를 자동으로 해주는 Swagger를 사용하고 있으므로 SwaggerConfig가 포함됩니다.

exception

  • 요청을 받고 로직을 거친 뒤 에러를 어떻게 반환할 지를 다루고 있습니다.
  • 이 모듈에서는 Controller까지 넘어온 exception을 어떻게 처리할 것인지를 다루는 ExceptionAdvice가 해당됩니다.서로 다룬 모듈에서 쓰이는 에러를 처리하는 방법에 대해서는 다른 포스팅에서 따로 다뤄보도록 하겠습니다.

application

application
┕ common
┕ exception
┕ diary, comment...
	┕ serviceDto
    ┕ service

controller에서 넘어온 요청을 처리하는, 서비스의 비즈니스 로직을 담당하는 모듈입니다.

common

  • paging request를 받아서 domain계층에서 쓰일 Pageable로 변환하는 pagingDto가 있습니다.

exception

  • 서비스 비즈니스 계층에서 사용할 exception들이 정의되어 있습니다.

auth-jwt

auth-jwt
┕ application
	┕ serviceDto
    ┕ service
┕ exception
┕ security
	┕ jwt
    	┕ authfilter
        ┕ tokenprovider
        ...
    ┕ securityconfig

이 auth 모듈에서는 presentation 레이어를 다루지 않습니다. api모듈에서 요청을 다 받고 반환까지 다루기 때문이죠.
그래서 auth와 관련된 로직만을 다룰 수 있도록 분리하였습니다.

security

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

common

common
┕ domain
	┕ MemberRole
┕ exception
	┕ exception response

앞선 포스트에서 말했듯이, common은 모든 모듈이 의존하고있는 모듈이므로 최대한 가볍고, 다른 모듈 등에 최대한 의존성이 적고 비즈니스 로직이 존재하지 않아야 합니다.

저의 경우 공통 exception Responseenum 엔티티가 포함되어있습니다.

  • enum 엔티티를 왜 domain 모듈이 아닌 공통모듈에 담는지 꽤 고민했는데, 실제로 사용해보니 api request를 받을 때 요청할 값을 지정해놓으니 깔끔하더라구요.
    말 그대로 api에서나 domain 모듈에서나 특별한 의존성 필요 없이 공통적으로 쓰일 수 있습니다.

core-mysql, core-redis

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작업을 하지 않아도 되어 편리하기때문에 사용하고 있습니다.


3. ⚙️모듈 gralde 빌드


루트 프로젝트

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'

api-diary

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모듈을 의존하지 않음으로써 도메인 계층에 어떤 로직이 있는지 전혀 알지 못하고 사용하지도 못합니다.

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 모듈에서는 서비스의 비즈니스만을 다룹니다.

서비스 비즈니스에 필요한 도메인과 관련된 core계층만을 의존하게 됩니다.
api계층을 의존하지 않아 요청/응답과 전혀 상관없이 로직이 구성됩니다.

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

    // 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을 이용하므로 이에 대한 의존성도 추가하였습니다.

core-redis

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

bootJar {
    enabled = false
}

jar {
    enabled = true
}

redis와 관련된 로직을 작성할 때 spring data redis를 사용하므로 이에 대한 의존성을 갖고 있습니다.


여기까지 개인 프로젝트에서 멀티 모듈을 구성하고, 직접 build하면서 구성해보았습니다.
사실 여기까지 이해하고 적용하는데 굉장히 많은 시간이 걸렸습니다.
큰 프로젝트에서는 모듈이 더욱 세분화되고, 관련된 설정들도 더욱 자세하겠죠?
아직 부족한 점도 많고 공부할 것도 많아서 어렵지만 전반적인 아키텍처에 대한 이해도가 높아졌다는 느낌이 들어서 뿌듯합니다 ㅎㅎv
아직 용어와 개념이 많이 헷갈리지만..

profile
아좌잣~!

0개의 댓글