포스트

Spring Boot - 멀티 모듈 구성 (1) Spring Boot Docs Guide 따라하기

환경

  • Spring Boot 3.2.3
  • java 17
  • Gradle 8.6
  • Spring Dependency Management 1.1.4

  • IntelliJ IDEA Ultimate


하나의 gradle build 환경에서 Spring Boot module을 여러개 구성하는 방법에 대해 정리 ✏️

2가지 방법을 정리하는데, 하나는 24.04.04 기준 Spring Documents에서 알려주는 방법이고
또 하나는 그동안 사용해왔던 방법이다.

이번 글은 Spring Documents에서 알려주는 방법을 정리한다!
(그동안 사용해왔던 방법은 ‘Spring Boot - 멀티 모듈 구성하기 (2)’에서 정리)

💭 두 방법에 대한 내 생각 Spring Documents에서 알려주는 방법은 dependency, plugin 등 필요한 설정들을 서브 모듈에서 모두 작성하는 방법이고, 그동안 사용해왔던 방법은 루트에서 서브 모듈들의 공통 설정(dependency, plugin 등등)을 작성하는 방법이다.

연구해본 결과 Spring Documents에서 알려주는 방법을 사용하면 루트 위치에 build.gradle을 작성하지 않고 서브 모듈에서 build.gradle을 별도로 작성하는 것이고 그동안 사용해왔던 방법은 루트 모듈에서 서브 모듈들의 공통 설정을 작성 한 후 각각의 모듈에서 별도로 필요한 의존성은 각 모듈의 build.gradle에서 작성하거나 의존성과 모듈의 버전 관리만 각 모듈의 build.gradle에서 관리하는 방법이었다.

그리고 Spring Documents에서 알려주는 방법이 훨씬 간단하고 빨랐으며 관리가 쉬웠다.

정보의 습득이 부족한탓인지 2가지 방식이 왜 다른 실행 결과를 발생시켰는지 알아내기 어려워서 2가지를 함께 정리!

언제라도 알게 된다면 이 글을 수정할 생각이다!


0. 멀티 모듈 공부 계획

image 멀티 모듈 프로젝트 구조

multi-module-project라는 이름의 gradle project를 만들고, gradle project의 sub module로 module-applcationmodule-library를 생성한다.

module-applcation 모듈은 spring boot application이고 module-library 모듈은 spring boot library처럼 사용한다.

module-library에는 spring data jpa의 entity와 repository를 정의 하고 module-application에서는 module-library의 entity와 repository를 가져와 사용하도록 구현 할 계획이며 실행 확인을 위해 사용자 이름본인확인문구를 POST 메서드로 전달 받은 후 h2 데이터베이스에 저장해보는 실습을 진행한다.


1. 프로젝트 및 모듈 생성

1. gradle project

1. gradle project 생성

image


2. build.gradle 삭제

루트 위치의 build.gradle은 삭제해준다.

image


2. 모듈 ‘module-library’

1. 모듈 ‘module-library’ 생성

root 프로젝트인 gradle project 이름을 한번 클릭해주고
네비게이션 창에서 마우스 우클릭 - 새로만들기 - 모듈 을 선택하여 모듈 생성 창을 만든다.
image


다음 사항을 확인 한 후 생성한다.

  • 모듈 이름 : module-library
  • 생성 위치 : gradle project (multi-module-project)
  • 상위 : gradle project 이름 확인 (multi-module-project)
  • 언어 : java
  • 시스템 빌드 : gradle

image


2. 루트 위치의 settings.gradle 확인

settings.gradle 파일을 열어 include 'module-library'코드가 추가되었는지 확인한다.
만약 추가되어있지 않다면 추가해주도록 한다.


3. 모듈 ‘module-library’의 build.gradle 작성

Spring initializr 사이트를 참고해서 build.gradle 코드를 복사해온다.
이 사이트 를 이용해서 build.gradle 스크립트를 생성할 때, 모듈에서 사용할 lombok, spring data jpa dependency를 추가하여 가져오도록 한다!

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
31
32
33
plugins {  
    id 'java'  
    id 'org.springframework.boot' version '3.2.4'  
    id 'io.spring.dependency-management' version '1.1.4'  
}  
  
group = 'kim.zhyun'  
version = '0.0.1'  
  
java {  
    sourceCompatibility = '17'  
}  

repositories {
    mavenCentral()
}

configurations {  
    compileOnly {  
        extendsFrom annotationProcessor  
    }  
}  
  
dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'  
    compileOnly 'org.projectlombok:lombok'  
    annotationProcessor 'org.projectlombok:lombok'  
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  
}  
  
tasks.named('test') {  
    useJUnitPlatform()  
}


그리고 이 모듈은 spring boot library로 사용될 것이기 때문에 spring boot 실행 파일인 bootJar가 필요 없으므로 spring boot 플러그인을 비활성화 해주어야 한다. spring boot 플러그인은 비활성화 하지만, 의존성 관리 기능은 유지되어야 하기 때문에 아래 코드를 참고하여 코드를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// plugins 블럭 수정
plugins { 
	id 'org.springframework.boot' version '3.2.2' apply false 
	id 'io.spring.dependency-management' version '1.1.4' 
	// ... other plugins 
}

// dependencyManagement 블럭 추가
dependencyManagement {  
    imports {  
        mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES  
    }  
}

// spring boot 플러그인 비활성화시 java compiler 작업의 -parameters 옵션이 비활성화 되는데,
// 이 옵션은 매개변수 이름을 참조하는 표현식을 사용할 때 중요하게 사용되므로 활성화해준다.
tasks.withType(JavaCompile).configureEach {  
    options.compilerArgs.add("-parameters")  
}


완성 코드

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
31
32
33
34
35
36
37
38
39
40
41
42
43
plugins {  
    id 'java'  
    id 'org.springframework.boot' version '3.2.4' apply false  
    id 'io.spring.dependency-management' version '1.1.4'  
}  
  
group = 'kim.zhyun'  
version = '0.0.1'  
  
java {  
    sourceCompatibility = '17'  
}  

repositories {
    mavenCentral()
}
  
configurations {  
    compileOnly {  
        extendsFrom annotationProcessor  
    }  
}  
  
dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'  
    compileOnly 'org.projectlombok:lombok'  
    annotationProcessor 'org.projectlombok:lombok'  
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  
}  
  
tasks.named('test') {  
    useJUnitPlatform()  
}  
  
dependencyManagement {  
    imports {  
        mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES  
    }  
}  
  
tasks.withType(JavaCompile).configureEach {  
    options.compilerArgs.add("-parameters")  
}

작성 후 gradle 새로고침을 진행하여 이상이 없는지 확인한다!

image


3. 모듈 ‘module-application’ 생성

다음 사항을 확인 한 후 생성한다.

  • 모듈 이름 : module-application
  • 생성 위치 : gradle project (multi-module-project)
  • 상위 : gradle project 이름 확인 (multi-module-project)
  • 언어 : java
  • 시스템 빌드 : gradle

image


1. 루트 위치의 settings.gradle 확인

settings.gradle 파일을 열어 include 'module-application' 코드가 추가되었는지 확인한다.
만약 추가되어있지 않다면 추가해주도록 한다.


2. build.gradle 작성

module-library와 마찬가지로 Spring initializr 사이트를 참고해서 build.gradle 코드를 복사해온다.
이 사이트를 이용해서 build.gradle 스크립트를 생성할 때, 모듈에서 사용할 lombok, spring web, spring data jpa, h2 db dependency를 추가하여 가져오도록 한다!

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
31
32
33
34
35
36
37
38
39
plugins {  
    id 'java'  
    id 'org.springframework.boot' version '3.2.4'  
    id 'io.spring.dependency-management' version '1.1.4'  
}  
  
group = 'kim.zhyun'  
version = '0.0.1-SNAPSHOT'  
  
java {  
    sourceCompatibility = '17'  
}  

repositories {
    mavenCentral()
}
  
configurations {  
    compileOnly {  
       extendsFrom annotationProcessor  
    }  
}  
  
repositories {  
    mavenCentral()  
}  
  
dependencies {  
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'  
    implementation 'org.springframework.boot:spring-boot-starter-web'  
    compileOnly 'org.projectlombok:lombok'  
    runtimeOnly 'com.h2database:h2'  
    annotationProcessor 'org.projectlombok:lombok'  
    testImplementation 'org.springframework.boot:spring-boot-starter-test'  
}  
  
tasks.named('test') {  
    useJUnitPlatform()  
}


dependencies 블럭에 module-library 모듈의 의존성을 추가해준다.

1
2
3
4
5
dependencies {  
    ..
    implementation project(':module-library')
    ..
}


그리고 module-application은 gradle build시 spring boot 실행 파일인 bootJar만 생성되면 되기 때문에 jar 생성 옵션을 비활성화해준다.

1
jar { enabled = false }


gradle 새로고침을 진행하고 이상이 없는지 확인한다!

image


그리고 루트 gradle 프로젝트의 src 디렉터리는 사용하지 않기 때문에 삭제해준다.


서브 모듈 생성이 완료된 gradle 프로젝트 구조!

image


2. 실행 확인을 위한 구현

서브 모듈의 동작 확인을 위해
사용자 이름본인 확인 정보를 입력받아 h2 데이터베이스에 저장하는 코드를 구현한다.

1. module-library ) entity, repository 구현

모듈에 패키지 루트가 없는 상태이기 때문에 만들어준 다음 entity와 respository를 구현해준다.

image image image


MemberEntity 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package kim.zhyun.library.entity;  
  
import jakarta.persistence.*;  
import lombok.*;  
  
@Getter  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
@Entity  
@Table(name = "Member")  
public class MemberEntity {  

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
      
    private String username;  
    private String confirm;  
      
}


MemberRepository 구현

1
2
3
4
5
6
7
8
9
10
11
package kim.zhyun.library.repository;  
  
import kim.zhyun.library.entity.MemberEntity;  
import org.springframework.data.repository.Repository;  
  
import java.util.Optional;  
  
public interface MemberRepository extends Repository<MemberEntity, Long> {  
    MemberEntity save(MemberEntity entity);  
    Optional<MemberEntity> findByUsername(String username);  
}


2. module-application ) Main 메서드 작성

모듈의 루트 패키지와 main() 메서드가 포함된 Application을 생성한다.

image

Application 작성

image

1
2
3
4
5
6
7
8
9
10
11
package kim.zhyun.application;  
  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
  
@SpringBootApplication  
public class Application {  
    public static void main(String[] args) {  
        SpringApplication.run(Application.class, args);  
    }  
}


작성 후 main() 메서드를 실행하여 잘 동작하는지 확인해준다.
image


3. module-application ) application.yml 설정

h2 데이터베이스와 jpa 사용을 위한 application.yml 설정을 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:  
  h2:  
    console:  
      enabled: false  
  
  datasource:  
    url: jdbc:h2:${my.address}\h2\application;AUTO_SERVER=TRUE;mode=MySQL;  
  
  jpa:  
    hibernate:  
      ddl-auto: create  
    properties:  
      hibernate:  
        show_sql: true  
        format_sql: true  
        highlight_sql: true  
  
---  
  
my:  
  address: 'C:\Users\xh\Documents\multi-module-study\multi-module-project-01\module-application'


4. module-application ) JpaConfig 작성

spring data jpa는 같은 패키지의 repository와 entity를 기본으로 검색하여 사용하는데, 외부 모듈에서 jpa repository와 jpa entity를 사용한다면 패키지의 이름이 다른 경우가 보통이므로 jpa config를 작성하여 repository와 entity를 찾을 수 있도록 설정해준다.

/config/JpaConfig.java

1
2
3
4
5
6
7
8
9
10
package kim.zhyun.application.config;  
  
import org.springframework.boot.autoconfigure.domain.EntityScan;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;  
  
@EnableJpaRepositories(basePackages = "kim.zhyun.library.repository")  
@EntityScan(basePackages = "kim.zhyun.library.entity")  
@Configuration  
public class JpaConfig { }


5. module-application ) service, controller 구현

MemberRequest.java

요청을 받을 RequestDto 작성

1
2
3
4
5
6
7
8
9
package kim.zhyun.application.request;  
  
import lombok.Getter;  
  
@Getter  
public class MemberRequest {  
    private String username;  
    private String confirm;  
}

MemberService.java

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
31
32
33
34
35
36
37
package kim.zhyun.application.service;  
  
import kim.zhyun.application.request.MemberRequest;  
import kim.zhyun.library.entity.MemberEntity;  
import kim.zhyun.library.repository.MemberRepository;  
import lombok.RequiredArgsConstructor;  
import org.springframework.stereotype.Service;  
  
import java.util.Optional;  
  
@RequiredArgsConstructor  
@Service  
public class MemberService {  
    private final MemberRepository memberRepository;  
      
    public MemberEntity create(MemberRequest request) {  
        MemberEntity entity = MemberEntity.builder()  
                .username(request.getUsername())  
                .confirm(request.getConfirm())  
                .build();  
          
        return memberRepository.save(entity);  
    }  
    
    public void confirm(MemberRequest request) {  
        Optional<MemberEntity> memberContainer = memberRepository.findByUsername(request.getUsername());  
          
        if (memberContainer.isEmpty()) {  
            throw new RuntimeException("등록되지 않은 사용자입니다.");  
        }  
        
        MemberEntity entity = memberContainer.get();  
        if (!entity.getConfirm().equals(request.getConfirm())) {  
            throw new RuntimeException("인증 실패!!");  
        }  
    }  
}

MemberApiController.java

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
package kim.zhyun.application.controller;  
  
import kim.zhyun.application.request.MemberRequest;  
import kim.zhyun.application.service.MemberService;  
import kim.zhyun.library.entity.MemberEntity;  
import lombok.RequiredArgsConstructor;  
import org.springframework.http.ResponseEntity;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.bind.annotation.RestController;  
  
@RequiredArgsConstructor  
@RestController  
@RequestMapping("/api/member")  
public class MemberApiController {  
    private final MemberService service;  
      
    @PostMapping  
    public MemberEntity create(@RequestBody MemberRequest request) {  
        return service.create(request);  
    }  
    
    @PostMapping("/confirm")  
    public ResponseEntity<Object> confirm(@RequestBody MemberRequest request) {  
        service.confirm(request);  
        return ResponseEntity.ok("Hi! %s 💐😆".formatted(request.getUsername()));  
    }  
}


6. module-application ) http 호출 확인

http 응답을 확인하기 위한 Postman과 talend api 앱이 있지만 나는 접근이 쉽고 관리하기 편한 http 요청 파일을 만들어서 확인하고 있다.
그래서 이번에도 http 요청 파일을 통해 확인 !
image

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
31
32
33
34
35
### member 등록
POST http://localhost:8080/api/member
Content-Type: application/json

{
  "username": "홍길동",
  "confirm": "동에 번쩍 서에 번쩍"
}

### member 확인 - 실패: 없는 사용자
POST http://localhost:8080/api/member/confirm
Content-Type: application/json

{
  "username": "홍길서",
  "confirm": "동에 번쩍 서에 번쩍"
}

### member 확인 - 실패: 확인 메세지 틀림
POST http://localhost:8080/api/member/confirm
Content-Type: application/json

{
  "username": "홍길동",
  "confirm": "남에 번쩍 북에 번쩍"
}

### member 확인 - 성공
POST http://localhost:8080/api/member/confirm
Content-Type: application/json

{
  "username": "홍길동",
  "confirm": "동에 번쩍 서에 번쩍"
}

image




참고한 사이트

  1. spring boot docs
  2. 패스트캠퍼스 예상국 강사님 강의
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.