[Spring] Gradle multi-module 프로젝트 세팅하기

2023. 2. 18. 18:44BackEnd


Gradle Multi Module로 프로젝트를 구성하는 이유

꽤나 잘 만들어진 오픈소스 프로젝트나 회사의 대부분 프로젝트에서는 멀티모듈로 구성하여 프로젝트를 운영합니다. 여러 이유가 있겠지만, 가장 큰 이유 1개만 꼽자면 모듈간 의존성을 줄이기 위함이라고 생각합니다. ( '멀티 모듈 = 모듈의 분산' -> 분산의 가장 큰 이점은 의존성 감소 ) 여러사람이 함께 참여하는 프로젝트에서는 작업 결과물의 반영, 그리고 배포 파이프라인의 분리 등의 이유로 멀티 모듈을 사용한다고 알고 있습니다.

 

Multi Module로 프로젝트 단점

멀티 모듈 구성은 단점도 있습니다. 관리 포인트가 늘어나는 것은 명백한 단점입니다. 흔히 core 혹은 common (같이 쓰는데도 있고)이라 불리는 공통 영역을 두고 사용하는데, 프로젝트가 장기화 되고, 서비스가 커질 수록 해당 모듈의 변경 등에 따라 다른 모듈에 영향을 주는 경우도 있습니다. 이를 회피하고자 core나 common 내에서도 특정 모듈에서만 사용하는 class를 만들어 core를 사용하는 목적인 재사용성을 줄이는 경우도 있습니다. 이런 맥락에서 이렇게 하지않는 추세(네이버 사례)도 있습니다.

 

 Multi Module로 프로젝트 만들기

결과적으로 어찌되었든, 많이 쓰는 형태이기 때문에 한번 Module을 만들어 보는 과정을 정리하고자 합니다. blog api (CRUD) 예시를 앞으로 만들어 보면서 관련 내용을 정리할 계획이기에 'blog-api' 라는 프로젝트 명으로 프로젝트를 만들어 보겠습니다. 최종 생성 결과는 아래와 같습니다.

.
├── HELP.md
├── blog-api
│   ├── build
│   ├── build.gradle
│   └── src
├── blog-core
│   ├── build
│   ├── build.gradle
│   └── src
├── build
│   ├── classes
│   ├── generated
│   ├── resources
│   └── tmp
├── build.gradle
├── gradle
│   └── wrapper
├── gradlew
├── gradlew.bat
└── settings.gradle

Multi Module 구성 내용

스프링(3.0.2), 자바(17)을 사용하였으며, 프로젝트는 spring initializr를 사용하여 생성했습니다. DB는 local에 설치한 postgresql을 사용하였습니다. IntelliJ를 사용하여 진행합니다. 포함된 gradle 의존성은 상단 프로젝트 github에서 확인하여 주세요.


Multi Module 구성 방법 ( 진행 순서대로 작성 )

프로젝트 생성까지는 완료되었고

 

1. 프로젝트 모듈 생성

  • core module 생성
  • api module 생성 ( 필요에 따라서 추가적으로 해도 됨 )
  • 기존 src 삭제
  • setting.gradle 확인 - 아래 처럼 추가한 모듈이 포함되어 있어야 합니다.
rootProject.name = 'blog'
include 'blog-core'
include 'blog-api'

 

2. gradle 설정

  • project 경로의 build.gradle 설정
  • core module의 build.gradle 설정
  • api module의 build.gradle 설정

먼저 project 경로의 build.gradle을 아래와 같이 수정합니다. subprojects 부분에서 포함된 내용이 각각의 하위 모듈에 반영되는 부분입니다. 저는 하위 모듈에서 사용할 DB 관련 의존성을 추가하였습니다.

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.2'
	id 'io.spring.dependency-management' version '1.1.0'
}

allprojects {
	group = 'com.blog'
	version = '0.0.1-SNAPSHOT'
}

sourceCompatibility = '17'

subprojects {
	apply plugin: 'java'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'

	repositories {
		mavenCentral()
	}

	dependencies {
		// spring
		testImplementation 'org.springframework.boot:spring-boot-starter-test'

		// lombok
		compileOnly 'org.projectlombok:lombok'
		annotationProcessor 'org.projectlombok:lombok'

		// DB
		runtimeOnly 'org.postgresql:postgresql'
		implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	}

	tasks.named('test') {
		useJUnitPlatform()
	}
}

 

두번째로 core 모듈의 build.gradle을 설정합니다. 여기는 다른 모듈에서 공통적으로 사용하는 모듈인데, 다른 모듈에서 해당 모듈을 포함하여 빌드하도록 합니다. 주로 entity와 repository를 작성하게 됩니다. 그래서 해당 모듈에 QueryDsl 사용을 위한 의존성을 추가하였습니다. ( 해당 버전이 별다른 plugin 설치 없이 가장 깔끔하게 쓸 수 있어 아래 버전으로 추가하였습니다. )

jar {
    enabled = true
}

bootJar {
    enabled = false
}

dependencies {

    // querydsl
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'

    implementation 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

}

 

세번째로 api 모듈의 build.gradle을 설정합니다. 중요한 것은 dependencies 부분에서 core 모듈을 반드시 Implementation하여야 core에서 정의한 내용을 사용할 수 있습니다. 그리고 해당 모듈에서 사용할 web 관련 내용을 추가하였습니다. core 모듈에서 entity와 repository를 다루고 있기에, 그것을 제외한 api 개발에 필요한 의존성들을 추가합니다. 

jar {
    enabled = false
}

dependencies {
    implementation project(path: ':blog-core')

    // spring
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // test
    testImplementation 'org.springframework.boot:spring-boot-test-autoconfigure'

    // config
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

    // web
    compileOnly 'jakarta.servlet:jakarta.servlet-api:6.0.0'
    implementation 'commons-io:commons-io:2.11.0' // filenameUtils

    // logging
//    implementation 'net.logstash.logback:logstash-logback-encoder:7.2'
}

 

3. resource 설정

각 모듈별로 application.yaml 혹은 yml, properties를 설정해 줍니다. 일단 이 과정에서는 DB 셋업만 해주면 크게 할 것은 없습니다.

아래 이미지와 같이 파일을 만들어 줍니다. core는 core를 붙여서 만들었고, dev, local, prod만 나누었습니다.

 

혼자할껀데 stage를 만들 필요는 없어보입니다. 작성한 것은 local 까지만 작성하였습니다. dev와 prod는 해당 서버를 클라우나, 온프레미스 컨테이너 환경을 어떻게 셋업할지에 따라서 다르게 가져갈 계획입니다.

core 모듈에서 application-core.yaml, application-core-local.yaml 파일을 작성하고

api 모듈에서 application.yaml 파일만 작성하면 됩니다.

 

application-core 파일에서 DB 정보를 설정하여 다른 모듈에서도 사용할 수 있도록 하였습니다.

 

application-core-local.yaml

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    flyway-url: jdbc:postgresql://localhost:5432/<DB명>
    url: jdbc:postgresql://localhost:5432/<DB>
    username: <설정한 정보>
    password: <설정한 정보>

  jpa:
    hibernate:
      ddl-auto: update
    database: postgresql
    properties:
      hibernate:
        format_sql: true
        show_sql: false
    database-platform: org.hibernate.dialect.PostgreSQLDialect

 

application.yaml

spring:
  profiles:
    active: local
    include:
      - core
  jpa:
    open-in-view: false

 

만약에 각 서비스별로 DB를 따로 사용하는 경우 각각 모듈별 application.yaml에서 작성해주면 됩니다. 저는 그냥 같이 쓸거이기에 core에 추가하였습니다. 

 

 

4. 기본 configuration 설정

 

아마 위 구성대로 실행을 하려고하면 application-core를 인식할 수 없을 것입니다. application-core 이런식으로 모듈명을 명시하는 것은 모듈별로 구분을 하기 위함입니다. 이렇게 사용하기 위해서는 PropertiesConfiguration에 대한 설정이 별도로 필요합니다. core 모듈에서 config 경로를 생성하고 PropertiesConfiguration 클래스를 생성합니다. 또 yaml 파일로 쓰기 위해서 YamlPropertySourceFactory도 추가해 줍시다.

package com.blog.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource(ignoreResourceNotFound = true,
    value = {
        "classpath:application-core.yaml",
        "classpath:application-core-${spring.profiles.active}.yaml"
    }, factory = YamlPropertySourceFactory.class)
public class PropertiesConfiguration {
}
package com.blog.common.config;

import java.util.Properties;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;

public class YamlPropertySourceFactory implements PropertySourceFactory {
  @Override
  public PropertySource<?> createPropertySource(String name, EncodedResource resource) {
    YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
    factory.setResources(resource.getResource());
    Properties properties = factory.getObject();
    return new PropertiesPropertySource(resource.getResource().getFilename(), properties);
  }
}

 

5. 모듈 main class 수정

core module은 main class를 삭제하면 됩니다. 다만 api module에서는 실행 시킬 main class가 필요하므로 Main.java를 적절히 수정합니다. 저는 아래와 같이 수정하였습니다.

 

아래 코드에서 scanBasePackages를 프로젝트 전체로 설정하였습니다. ( 이와 관련해서는 SpringBootApplication이 Package를 스캐닝 하는 순서를 좀 더 찾아보면 되는데, 해당 글과는 무관하므로 생략합니다. )

package com.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.blog")
public class BlogApiApplication {

  public static void main(String[] args) {
    SpringApplication.run(BlogApiApplication.class, args);
  }
}

 

구성 완료

완료가 되면 아래와 같이 잘 실행이 되어야 합니다. 여기까지는 사실 별다른 처리 로직이 없기에 잘 동작하는 것 처럼 보이지만, 반드시 core에 작성한 내용을 api에서 사용하였을 때 문제가 없어야 합니다.

 

간단하게 레이어 다 무시하고 class 3개만 작성해서 검증해보겠습니다.

먼저 core쪽에 아래 두개를 생성합니다.

package com.blog.domain.confirm.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;

@Entity
@Getter
public class Confirm {

  @Id
  @Column(name = "id", nullable = false)
  private Long id;

  private String description;

  public Confirm() {
  }

  @Builder
  public Confirm(Long id, String description) {
    this.id = id;
    this.description = description;
  }

}
package com.blog.domain.confirm.ropository;

import com.blog.domain.confirm.entity.Confirm;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ConfirmRepository extends JpaRepository<Confirm, Long> {

}

 

 

api 쪽에 아래 controller만 만들어 줍니다.

package com.blog.confirm;

import com.blog.domain.confirm.entity.Confirm;
import com.blog.domain.confirm.ropository.ConfirmRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TestController {
  private final ConfirmRepository confirmRepository;

  @GetMapping("/test")
  public Long test(){
    Confirm confirm = confirmRepository.saveAndFlush(Confirm.builder().id(1L).description("test").build());
    return confirm.getId();
  }
}

 

결과는 성공이네요.

 

 

 


결론

멀티모듈 구성은 이렇게 하면 된다는 것이구요. 사실 멀티모듈을 구성 할 때 build가 어떻게 진행되는지, 배포 과정에서 어떻게 배포가 되는지 이런 부분들이 훨씬 더 중요하기 때문에... 그냥 방법론 적인 부분만 정리해보았습니다.

 

주로 core에는 entity와 repository 관련 내용을 넣는다. 그래서 core의 entity를 다른 모듈이 공유한다면 DB 설정도 core에 잡아주는 것이 편하다.

그리고 다른 모듈에는 필요한 것들 적절하게 구성해서 쓰면 된다.

 

뭐 이런 내용입니다.