포스트

Spring Boot - 멀티 모듈 구성 (2) 다른 방법

환경

  • 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 - 멀티 모듈 구성 (1)‘에서 정리)

💭 두 방법에 대한 내 생각 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 수정

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
buildscript {  
    repositories {  
        mavenCentral()  
    }  
  
    dependencies {  
        classpath "org.springframework.boot:spring-boot-gradle-plugin:3.2.3"  
        classpath "io.spring.gradle:dependency-management-plugin:1.1.4"  
    }  
}  

// 서브 모듈에서 공통으로 사용하는 코드 작성
subprojects {  
    apply plugin: 'java'  
    apply plugin: 'org.springframework.boot'  
    apply plugin: 'io.spring.dependency-management'  
  
    group 'kim.zhyun'  
  
    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'  
    }  
  
    test {  
        useJUnitPlatform()  
    }  
}  
  
/*  
아래 코드를 작성하면, 각 모듈에 build.gradle을 작성하지 않아도 된다.  
project(':module-application') {  
    version = '1.0.0'  
    
    bootJar { enabled = true }    
    jar { enabled = false }  
    
    dependencies {        
	    implementation project(":module-library")        
	    implementation 'org.springframework.boot:spring-boot-starter-web'        
	    runtimeOnly 'com.h2database:h2'    
    }
}  
  
project(':module-library') {  
    version = '1.0.0'  
    
    bootJar { enabled = false }    
    jar { enabled = true }  
    
    dependencies {        
	    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'    
    }
}  
*/


3. 모듈 ‘module-library’

1. 모듈 ‘module-library’ 생성

image


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

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

image


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

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


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

필요한 dependency인 jpalombok은 공통으로 사용되기 때문에 루트 위치의 /build.gradle에서 선언하였으므로 module-library의 build.gradle에서는 작성할 필요가 없다.

1
2
3
4
5
6
7
8
version = '1.0.0'  
  
dependencies {  
    
}  
  
bootJar { enabled = false }  
jar { enabled = true }


3. 모듈 ‘module-application’ 생성

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

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

image


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

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


2. build.gradle 작성

Spring initializr 사이트를 참고해서 module-application모듈에서만 사용할 spring web, h2 db dependency를 추가하여 가져오도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
version = '1.0.0'  
  
dependencies {    
    implementation project(":module-library")
    
    implementation 'org.springframework.boot:spring-boot-starter-web'  
    runtimeOnly 'com.h2database:h2'  
    
}  
  
bootJar { enabled = true }  
jar { enabled = false }


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


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


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

image


2. 실행 확인을 위한 구현

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

1. module-library ) entity, repository 구현

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

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 image image

Application 작성

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
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-02\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 구현

/request/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;  
}

/service/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.dto.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("인증 실패!!");  
        }  
    }  
}

/controller/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 image 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. https://velog.io/@soyeon207/스프링-부트-멀티-모듈-프로젝트-만들기
  2. 패스트캠퍼스 예상국 강사님 강의
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.