멀티 모듈 프로젝트 전환기

크리링·6일 전
0

실무 트러블 슈팅

목록 보기
10/10
post-thumbnail

재고 관리 서비스사내에서 쓰는 관리자 페이지 두 프로젝트는 공유하는 코드가 많다.
다른 프로젝트로 관리하고 있었는데 멀티 모듈 프로젝트로 합쳐서 하나의 프로젝트로의 관리로
코드, 배포 면으로 많은 불편함이 개선됐다.

긍정적인 동료들의 반응






문제 상황

  1. 관리자, 사용자, 프랜차이즈 각각 서버 존재
    공유하는 domain, repository, service, utility의 양이 많음
    매번 업데이트 될때마다 각 프로젝트 별로 복붙하고 똑같이 유지해줘야함
    -> 너무 번거롭고, 실수를 통한 오류 가능성 높음

  2. 하나의 도메인 변화 -> 각각의 프로젝트(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')	// 모듈 의존성 등록 (공통 코드 사용 가능)
    }
}

rootbuild.gradle 생성 및 코드 추가

해결되지 않는 빨간줄

settings.gradle 수정
(miri-server 프로젝트가'module-common, module-miri, module-miri_admin 프로젝트를 하위 프로젝트로 관리하겠다는 의미입니다.)

module-common의 파일들은 매핑이 되지만
spring-data-jpalombok 같은 라이브러리 어노테이션이 동작 안함



Gradle 오류

IntelliJ 오른쪽에 gradle이 없음
./gradlew clean build

터미널로 명령어 실행

실패

확인해보니 wrapper 폴더가 없음
gradle wrapper 

명령어 실행

Could not find org.springframework.boot:spring-boot-gradle-plugin:3.3.1.RELEASE.

3.3.1.RELEASE는 존재하지 않는 버전명
-> 3.3.1 변경

gradle wrapper 명령어 재실행 실패

miri-server/build.gradle 확인

apply plugin: ‘org.springframework.boot’ 로 변경

miri-server/build.gradle

실패

하위 폴더 인 module-miri, module-miri_admin build.gradle

id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'

제거 후 실행

버전 중복 제거

성공

gradle-wrapper.jar 생성 확인



./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-commonjar 생성 비활성화 + subProjectsJAR 생성 활성화 필요

# 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() 메서드가 들어있는 클래스 @SpringBootApplicationGradle이 찾지 못해서 발생한 에러

추가하고 싶지만 @SpringBootApplication을 찾지 못함

subProjectsbuild.gradle에 각각 Main class 경로 추가 후 명령어 실행



성공!

하지만 자바 파일을 인식하지 못함



자바 파일 인식 못함

폴더 `main>java` 생성 후 폴더/파일 위치 변경

많은 빨간불을 어노테이션 주입 + 실행

그리고 실행했을때 메인 메소드의 Hello, World! 출력 확인

메인 함수 명 변경(subProjects에서의 main 클래스 경로 또한 수정), application.yml 추가 후 실행

실행 성공!



패키지 묶이지 않은 오류

처음 실행 완료 후 스웨거 문서 접속시 SecurityConfigformLogindisable 했는데 formLogin이 나왔다.
원인을 찾아보다가 애플리케이션 시작시 print 문을 추가했는데 실행되지 않았다.

Swagger 접속시 formLogin 확인

SecurityConfig의 formLogin을 disable

각각의 파일들이 가까운 상위 폴더의 package로 지정돼있어 묶이지 않는 상태이다.

Application.java

SecurityConfig.java

java 하위에 폴더 생성 후 폴더/파일 모두 이동 + build.gradle에서 경로, 문자열로 변경

java 하위 폴더 경로 생성 후 이동

build.gradle에서 경로, 문자열로 변경



Could not autowire common module file

실행시 Could not autowire 문제 발생

원인

  • Spring Boot는 @SpringBootApplication 기준으로 자기 모듈 내부만 자동 스캔
  • module-common의 common.repo.user.UserRepository는
module-miri의 @SpringBootApplication 기준으로 보면 외부 패키지
  • 그래서 @Component, @Repository, @Service가 있어도 스캔 대상이 아님
# 패키지 컴포넌트 스캔
@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.

관련해서 찾아보니

  • @SpringBootApplication(scanBasePackages = {…}) 는 일반 컴포넌트만 스캔
  • @EnableJpaRepositories : JpaRepository 인터페이스 등록용 어노테이션
  • @EntityScan : JPA 엔티티 클래스 인식용

Spring이 책임 구분을 명확히 하기 위해 의도적으로 나눴다고 한다.

@SpringBootApplication(scanBasePackages = {"miri", "common"})
# repo 스캔 추가
@EnableJpaRepositories(basePackages = "common.repo")
# entity 스캔 추가
@EntityScan(basePackages = "common.domain")
public class MiriApplication {
repo, entity 스캔 추가 후 실행

Security 등록 문구 확인

Swagger 실행 및 접속까지 확인



TEST 추가

테스트 폴더도 package 명 맞춰주고, 어노테이션과 의존성 관련 빨간줄 수정 후
하위 프로젝트 모듈 각각 테스트 실행

사용자 테스트 성공

관리자 테스트 성공






소감

쉽지는 않았지만 생각만큼 어렵지도 않았다. (GPT가 잘 도와준 덕이 큰 듯)
그리고 기존에는 start.spring.io 를 이용해서 초기 프로젝트를 세팅했는데
생각보다 초기 세팅에서 많은 부분을 의존하고 있었다는 생각이 들고, 좋은 경험이었다.



0개의 댓글