티스토리 뷰
들어가기 전
현재 작성된 코드들은 GitHub 에 저장되어 있습니다.
이해가 안되는 부분이 있다면 번갈아 가며 확인해주시면 됩니다.
개발 환경
Spring Boot 3.4.5
Java 17
IntelliJ
MacOS
왜 멀티 모듈을 선택했나?
실무에서 진행하는 프로젝트는 멀티 모듈로 되어 있으며 개발을 진행 중입니다.
하지만 세팅해본 적은 없으며 그냥 있으니까 얼레벌레 개발만하고 "원래 이랬으니까~" 라는 생각으로 살아 왔었습니다.
하지만 이렇게 가다간 도태 될 것같아서 크기가 큰 프로젝트라면 언젠간 마주 할 것 같아서 이번 기회에 공부해보려고 합니다.
멀티 모듈은 아래와 같은 장점을 가지기 있으며 앞으로 협업을 진행하면 확실이 체감 할 것 같습니다.
거의 대부분의 회사가 멀티 모듈로 진행 중이였네..
- 도메인 로직 분리 : 핵심 로직(domain)을 외부 레이어(api, batch, infra 등)와 격리
- 재사용성 : 여러 서비스에서 공통 모듈(common, domain, external 등) 활용 가능
- 역할 분리 : 팀별 책임 경계를 명확히 할 수 있음
프로젝트 모듈 구조
저는 아래와 같은 모듈로 잡았습니다.
도움은 받았지만 저도 생각을 해보니 실무에서는 최소 이렇게 4개는 쓰겠구나라는 생각이 들었습니다.
multimodule-project
│
├── build.gradle (루트)
├── settings.gradle
│
├── domain # JPA Entity, Repository 등 핵심 비즈니스 로직
│ └── build.gradle
│
├── api # Web, Controller, Service 등 외부 노출 계층
│ └── build.gradle
│
├── common # Util Java 코드
│ └── build.gradle
│
├── external # 외부 시스템 연동
│ └── build.gradle
- Domain 모듈 : Domain(Entity), Enum, Repository, 핵심 비즈니스 로직(Service)
- Api 모듈 : Controller 및 service, Advice, Config, 필터, 인터셉터, 스케쥴러
- Common 모듈 : ErrorCode, Exception, 공통 응답, Util, Enum 등 모든 모듈에서 사용하는 순수 Java 코드
- External 모듈 : 외부 시스템 연동 (S3, AI, Redis 등)
멀티 모듈 프로젝트는 어떻게 만드는 건데?
- https://start.spring.io/ 에서 프로젝트를 의존성 없이 생성해서 IntelliJ로 Open합니다.
- ROOT에 있는 src 폴더를 직접 오른쪽 마우스를 눌러 삭제합니다.
- ROOT 프로젝트에서 오른쪽 마우스를 클릭한 후 new → module 을 선택하여 모듈 명을 정합니다.
Tip) 지금은 실행이 안됩니다 왜냐? main() 메서드가 없기 때문입니다.
Gradle 의존성 추가
Root → common → external → domain → api 순으로 Gradle을 작성하려고 합니다.
domain 모듈은 common 모듈을 의존합니다.
external 모듈은 common 모듈을 의존합니다.
api 모듈은 common, domain, external 모듈을 의존합니다.
Root의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5' apply false
id 'io.spring.dependency-management' version '1.1.7' apply false
}
group = 'com.aoxx'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
allprojects {
repositories {
mavenCentral()
}
}
// 모든 모듈 공통
subprojects {
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
dependencies {
// 모든 모듈에서 사용할 공통 의존성
implementation 'org.springframework.boot:spring-boot-starter' // Spring Boot 기본 스타터
compileOnly 'org.projectlombok:lombok' // Lombok
annotationProcessor 'org.projectlombok:lombok' // Lombok 어노테이션 프로세서
testImplementation 'org.springframework.boot:spring-boot-starter-test' // Spring Boot 테스트
}
tasks.named('test') {
useJUnitPlatform() // JUnit 5를 사용할 수 있도록 설정
}
}
Root의 settings.gradle
rootProject.name = 'multi-module'
include 'api'
include 'common'
include 'external'
include 'domain'
Common 모듈의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
}
bootJar { enabled = false }
jar { enabled = true }
dependencies {
}
test {
}
External 모듈의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
}
bootJar { enabled = false }
jar { enabled = true }
dependencies {
implementation project(':common') // common 모듈을 의존함
}
test {
}
domain 모듈의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
}
bootJar { enabled = false }
jar { enabled = true }
dependencies {
implementation project(':common') // common 모듈을 의존함
// Spring Data JPA 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'com.h2database:h2' // 테스트 시에만 필요
}
test {
}
Api 모듈의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
}
bootJar { enabled = true }
jar { enabled = false }
dependencies {
implementation project(':common') // common 모듈을 의존함
implementation project(':domain') // domain 모듈을 의존함
implementation project(':external') // external 모듈을 의존함
implementation 'org.springframework.boot:spring-boot-starter-web'
// Application 있는 곳에 h2 db 세팅
implementation 'com.h2database:h2'
}
test {
}
중간에 jar와 bootJar는 뭐야?
jar
- 일반적인 Java 라이브러리 형태로 패키징
- 실행용이 아니며 main()이 없음.
- 다른 모듈이 의존할 수 있는 라이브러리 역할을 함.
- 즉, Common, Domain, External 모듈은 라이브러리 역할을 하려고 합니다.
bootJar
- Spring Boot 애플리케이션 실행이 가능한 패키징 형태
- main() 메서드가 포함되어 있음.
- 종속된 모든 라이브러리를 포함한 독립 실행형 JAR를 생성함.
- 즉, Api 모듈이 Spring Boot 애플리케이션을 실행 책임을 가집니다.
위에 gradle에 한개만 다른 모듈이 있습니다. api 모듈입니다.
Api 모듈은 이 프로젝트를 실행시키는 책임을 가진 모듈로 두었기 때문에 bootJar = true 가 되어있습니다.
그래서 아래처럼 Api 모듈안에 Application 클래스를 만들어 @SpringBootApplication 을 입력하였습니다.
그리고 Bean Scan도 해당 위치부터가 아니고 원래 패키지로 잡기 위해 기본 패키지 경로로 옵션을 추가 하였습니다.
도메인 모듈 단위 테스트 전략
도메인 모듈의 비즈니스 로직의 단위 테스트만 진행할 것이기 때문에 도메인 모듈에서 테스트코드를 작성해보려고 합니다.
User Entity
@Table(name = "USER_INFO")
@Getter
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected User() {}
public User(String name) {
this.name = name;
}
}
Tip) H2 사용 시 user는 예약어입니다. user말고 다른 이름으로 변경하세요.
UserRepository
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// 이름으로 찾는 커스텀 메서드
Optional<User> findByName(String name);
}
UserRepositoryTest
- @DataJpaTest를 사용하면 @SpringBootTest와는 달리 빈을 다 등록하지 않고 JPA 환경에 필요한 것들만 가지고 테스트 하게 됩니다.
- @DataJpaTest는 @Component및 @ConfigurationProperties빈은 스캔되지 않기 때문입니다.
- 그래서 @DataJpaTest를 사용하였습니다.
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("사용자를 저장하고 이름으로 조회한다")
void saveAndFindByName() {
// given
User user = new User("홍길동");
// when
userRepository.save(user);
Optional<User> found = userRepository.findByName("홍길동");
// then
Assertions.assertThat(found).isPresent();
Assertions.assertThat(found.get().getName()).isEqualTo("홍길동");
}
@Test
@DisplayName("없는 이름으로 조회하면 Optional.empty()를 반환한다")
void findByNameNotExists() {
// when
Optional<User> result = userRepository.findByName("없는사람");
// then
Assertions.assertThat(result).isEmpty();
}
}
그런데 웬걸 왜 계속 빈을 못찾는다고 에러 메시지가 나옵니다.
생성해둔 기본 코드와 테스트 코드의 패키지 경로도 같은데!!!!!!! 왜!!!!


Unable to find a @SpringBootConfiguration by searching packages upwards from the test. You can use @ContextConfiguration, @SpringBootTest(classes=...) or other Spring Test supported mechanisms to explicitly declare the configuration classes to load. Classes annotated with @TestConfiguration are not considered.
이 에러는 @SpringBootApplication과 같은 Spring Context를 불러오는 포인트가 없어서 발생하는 에러입니다.
이해가 되지 않았다면 이해시켜보도록 하겠습니다.
보통 @SpringBootTest 또는 @DataJpaTest를 쓰면 스프링이 자동으로 시작 클래스(Main 메서드)를 기준으로 설정을 읽어 들이는데 Domain 모듈에는 그 시작시키는 클래스가 없어서 못찾고 있는 것입니다.

import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestConfiguration {
}
이 클래스를 만들고 세팅해 두었다면 이제 편안한 테스트 성공을 맛보시면 됩니다.
Api 모듈로 기동했을 때 다른 모듈 의존성 확인해보기
1) 터미널에서의존성 트리 확인
프로젝트 루트에서 아래 명령어를 작성해보시면 됩니다.
./gradlew :api:dependencies
Api 모듈은 Common, Domain, External 모듈을 의존하기 때문에 아래 처럼 있어야합니다.

2) jar 패키징 확인해보기
api 모듈을 빌드 하면 api.jar생성되는데 루트 경로에 아래와 같은 명령어를 작성해주시면 됩니다.
jar tf ./api/build/libs/api.jar
이거 하면 jar에 있는 많은 의존성이 올라오는데
Api 모듈은 Common, Domain, External 모듈을 의존하기 때문에 아래 처럼 있어야합니다.

프로젝트 전체 빌드
프로젝트 루트에서 빌드해보려고 합니다. 전체를 기동시키기 위해 !
./gradlew build --console=plain
- --console=plain 옵션은 빌드되는 것을 console에 출력하는 옵션입니다.

이제 잘 되는 것을 확인 하였고 개발을 진행하면 됩니다!!!!!!!
마무리
Spring Boot 멀티 모듈을 설계해보니 재사용성, 유지보수성에서 엄청난 이점을 가질 것이라고 예상됩니다.
거의 모든 회사에서 이러한 방법으로 사용하고 있는데 알아두면 너무 좋을 것 같습니다.
모듈 간 책임을 어떻게 오염을 안시키고 이끌고 갈지 고민이 되네요 모두 수고하셨습니다.

감사합니다.
'백엔드 > 🌸Spring' 카테고리의 다른 글
[Spring] API 서버에서 카카오 로그인 구현 (2/2) : 토큰 받고 사용자 정보 조회 (0) | 2025.06.15 |
---|---|
[Spring] API 서버에서 카카오 로그인 구현 (1/2) : 인가 코드 받기 (1) | 2025.05.19 |
[Spring] STOMP 설정과 사용하는 방법 쉽게 이해하기 (0) | 2025.04.07 |
[Spring] application.yml과 active profile 환경별 설정하기 (0) | 2024.11.22 |
[Spring] Spring Boot3에 Swagger 적용하기 (6) | 2024.10.17 |
- Total
- Today
- Yesterday
- 개발자
- JavaScript
- 개발블로그
- spring
- 개발
- 비동기
- 개발환경
- Cors
- 인증
- jvm
- 데이터 베이스
- 디자인패턴
- 프론트
- Fetch
- 프로세스
- AJAX
- Spring Security
- 템플릿
- 네트워크
- java
- 계단 오르기
- Front
- DBeaver
- 트랜잭션
- 소셜로그인
- 실시간 채팅
- 깃허브 액션
- 카카오 로그인
- aws
- 코딩테스트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |