재고 관리 서비스
와사내에서 쓰는 관리자 페이지
두 프로젝트는 공유하는 코드가 많다.
다른 프로젝트로 관리하고 있었는데 멀티 모듈 프로젝트로 합쳐서 하나의 프로젝트로의 관리로
코드, 배포 면으로 많은 불편함이 개선됐다.
관리자
, 사용자
, 프랜차이즈
각각 서버 존재
공유하는 domain, repository, service, utility의 양이 많음
매번 업데이트 될때마다 각 프로젝트 별로 복붙하고 똑같이 유지해줘야함
-> 너무 번거롭고, 실수를 통한 오류 가능성 높음
하나의 도메인 변화 -> 각각의 프로젝트(2개)에 반영 -> 프로젝트의 branch 별 배포
-> 하나의 도메인 변경이 2개의 프로젝트에서 각 브랜치별 배포 필요
이 과정에서 코드 동기화 시간이 늘어나고, 배포하는 시간도 길어지며, 배포에서의 오류도 많이 났다.
결국 공통의 코드 관리와 그에 따른 배포 시에 충돌이 자주 생길 위험 때문이다.
좀 간편하게 공통의 코드를 관리할 수 없을까?
를 고민하다가 멀티 모듈 프로젝트를 알게 됐고 불편하게 느끼는 점을 개선할 수 있어보인다.
공부 및 참고한 내용
멀티 모듈 프로젝트(Multi-module project)는 하나의 큰 프로젝트를 여러 개의 서브 프로젝트(모듈)로 나누어 기능별로 분리한 구조
my-app/
│
├── module-api/ # 외부 API 요청/응답 처리 (Controller, DTO 등)
├── module-core/ # 핵심 비즈니스 로직 (Service, Domain)
├── module-common/ # 공통 유틸, 공통 예외, 설정 등
├── module-infra/ # DB, 외부 API 연동, JPA Repository 등
├── build.gradle
└── settings.gradle
모듈 각각 생성
common
, prod
, admin
세가지로 구성
각각 하위 폴더와 파일로 src
폴더와 Main.java
, build.gradle
구성
domain, repository 폴더 -> common 모듈,
service 폴더 -> service 모듈
// Gradle 자체 빌드에 필요한 플러그인 classpath 지정
buildscript {
ext { // 루트에서 공통으로 사용할 변수 선언
springBootVersion = '3.3.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath "io.spring.gradle:dependency-management-plugin:1.1.5.RELEASE"
}
}
// 모든 하위 모듈에 공통 플러그인과 설정 적용
subprojects {
group 'com.multi-miri'
version '1.0'
apply plugin: 'java'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
}
project(':module-miri') {
dependencies {
compile project(':module-common') // 모듈 의존성 등록 (공통 코드 사용 가능)
}
}
project(':module-miri_admin') {
dependencies {
compile project(':module-common') // 모듈 의존성 등록 (공통 코드 사용 가능)
}
}
root
의 build.gradle 생성 및 코드 추가
settings.gradle 수정
(miri-server
프로젝트가'module-common
, module-miri
, module-miri_admin
프로젝트를 하위 프로젝트로 관리하겠다는 의미입니다.)
module-common
의 파일들은 매핑이 되지만
spring-data-jpa
나 lombok
같은 라이브러리 어노테이션이 동작 안함
./gradlew clean build
터미널로 명령어 실행
gradle wrapper
명령어 실행
Could not find org.springframework.boot:spring-boot-gradle-plugin:3.3.1.RELEASE.
3.3.1.RELEASE는 존재하지 않는 버전명
-> 3.3.1 변경
miri-server/build.gradle
확인
apply plugin: ‘org.springframework.boot’
로 변경
하위 폴더 인 module-miri
, module-miri_admin
build.gradle에
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
제거 후 실행
./gradlew clean build
build 재실행
Execution failed for task ':module-common:bootJar'.
> Error while evaluating property 'mainClass' of task ':module-common:bootJar'.
> Failed to calculate the value of task ':module-common:bootJar' property 'mainClass'.
> Main class name has not been configured and it could not be resolved from classpath
오류 내용을 요약하자면 module-common
은 실행 가능 모듈이 아니라 bootJar
를 만들려 하면 Main class
를 찾을 수 없어 실패
module-common
에 jar 생성 비활성화 + subProjects
에 JAR 생성 활성화 필요
# module-common의 build.gradle
plugins {
id 'java'
}
group = 'miri'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
// Spring Boot 실행 JAR 생성을 비활성화 (main class 없음)
bootJar {
enabled = false
}
// 일반 JAR은 활성화 (라이브러리로 쓰기 위해)
jar {
enabled = true
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'spring-boot-starter-*' // 공통 기능들
annotationProcessor 'querydsl-apt:5.0.0:jakarta' // QueryDSL 관련
}
tasks.named('test') {
useJUnitPlatform()
}
// profile 별 application.yml 디렉터리 적용
ext.profile = (!project.hasProperty('profile') || !profile) ? 'local' : profile
def querydslDir = "build/generated"
sourceSets {
main {
resources {
srcDirs "src/main/resources", "src/main/resources-env/${profile}"
}
java{
// querydslDir 에서 생성된 Q클래스
srcDirs += querydslDir
}
}
}
module-common의 build.gradle에 jar 옵션 추가
# 하위 프로젝트의 gradle
plugins {
id 'java'
}
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
...
}
tasks.named('test') {
useJUnitPlatform()
}
ext.profile = (!project.hasProperty('profile') || !profile) ? 'local' : profile
sourceSets {
main {
resources {
srcDirs "src/main/resources", "src/main/resources-env/${profile}"
}
}
}
module-miri의 build.gradle 추가
다시 ./gradlew clean build
실행
* What went wrong:
Execution failed for task ':module-miri:bootJar'.
> Error while evaluating property 'mainClass' of task ':module-miri:bootJar'.
> Failed to calculate the value of task ':module-miri:bootJar' property 'mainClass'.
> Main class name has not been configured and it could not be resolved from classpath
오류 내용을 요약하자면 module-miri
는 실행 가능한 Spring Boot 앱
이지만 main()
메서드가 들어있는 클래스 @SpringBootApplication을 Gradle
이 찾지 못해서 발생한 에러
subProjects
의 build.gradle
에 각각 Main class
경로 추가 후 명령어 실행
성공!
많은 빨간불을 어노테이션 주입 + 실행
메인 함수 명 변경(subProjects
에서의 main 클래스 경로 또한 수정), application.yml
추가 후 실행
처음 실행 완료 후 스웨거 문서 접속시 SecurityConfig
의 formLogin
을 disable 했는데 formLogin
이 나왔다.
원인을 찾아보다가 애플리케이션 시작시 print 문을 추가했는데 실행되지 않았다.
각각의 파일들이 가까운 상위 폴더의 package로 지정돼있어 묶이지 않는 상태이다.
java 하위에 폴더 생성 후 폴더/파일 모두 이동 + build.gradle에서 경로, 문자열로 변경
원인
# 패키지 컴포넌트 스캔
@SpringBootApplication(scanBasePackages = {"miri", "common"})
public class Application{
}
Could not autowire
문제는 해결됐지만
Description: Parameter 1 of constructor in miri.jwt.JwtServiceImpl required a bean of type 'common.repo.user.UserRepository' that could not be found. Action: Consider defining a bean of type 'common.repo.user.UserRepository' in your configuration.
관련해서 찾아보니
Spring이 책임 구분을 명확히 하기 위해 의도적으로 나눴다고 한다.
@SpringBootApplication(scanBasePackages = {"miri", "common"})
# repo 스캔 추가
@EnableJpaRepositories(basePackages = "common.repo")
# entity 스캔 추가
@EntityScan(basePackages = "common.domain")
public class MiriApplication {
repo, entity 스캔 추가 후 실행
테스트 폴더도 package 명 맞춰주고, 어노테이션과 의존성 관련 빨간줄 수정 후
하위 프로젝트 모듈 각각 테스트 실행
쉽지는 않았지만 생각만큼 어렵지도 않았다. (GPT가 잘 도와준 덕이 큰 듯)
그리고 기존에는start.spring.io
를 이용해서 초기 프로젝트를 세팅했는데
생각보다 초기 세팅에서 많은 부분을 의존하고 있었다는 생각이 들고, 좋은 경험이었다.