Gradle 멀티프로젝트 관리

dragonappear·2022년 12월 31일
0

Gradle

목록 보기
5/5

출처

https://jojoldu.tistory.com/123

대부분의 서비스는 단일 프로젝트로 구성되는 일이 거의 없다

아무리 작게 구성해도 일정 수준 이상의 트래픽을 감당하려면 사용자와의 접점을 담당하는 서버(이하 Web 프로젝트), DB와의 접점을 담당하는 서버(Api 서버)로 구분하여 구성하게 된다

이럴 경우 고민이 되는 것이 WEB과 API 모두에서 사용되는 클래스들은 어떻게 다룰 것인가 이다.

가장 쉽고 흔한 방법은 봍붙이다.

한 프로젝트에서 Member 클래스 파일을 생성하고 이를 다른 프로젝트에서 코드를 복사하는 것이다.

하지만 이럴 경우 연동되는 프로젝트가 늘어날 경우, 혹은 MemberClass 클래스의 코드에 수정이 필요한 경우 정말 많은 양을 수정해야하고 실수할 여지가 많아진다.

어떻게 하면 이 문제를 해결할 수 있을까?

하나의 공통 프로젝트를 두고, 이 프로젝트를 여러 프로젝트에서 가져가서 사용할 수 있으면 좋지 않을까?

위처럼 해결하려면 몇가지 조건이 수반된다.

  • 개발시에는 바로바로 공통 프로젝트 코드를 사용할 수 있어야 한다.
  • 빌드 시에는 자동으로 공통 프로젝트가 포함되어야 한다.

Gradle Multi Module 방식을 사용하여 문제를 해결해보자.


프로젝트 구조

root: multi-module
하위 모듈: module-api, module-web , module-core


(application,library는 무시)

  • 빌드는 항상 루트 프로젝트를 기준으로 진행할 예정이기에 gs-multi-module 외에 나머지 프로젝트에는 gradle 폴더, gradlew 등의 파일이 없다.
  • build.gradlesrc 폴더만 존재한다.
  • module-core은 다른 프로젝트에서 사용할 중요한 혹은 공통 클래스를 모아놓은 프로젝트라고 생각하면 된다.

module-core:build.gradle

dependencies {
    api 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Member.java

package me.dragonappear.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Getter
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    private String email;

    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

MemberRepository.java

package me.dragonappear.repository;

import me.dragonappear.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member,Long> {
}

  • 그럼에도 Entity 클래스를 작성하고, 이 Entity 클래스의 repository, 그리고 간단한 repository test까지는 가능해야 하기 때문에 위와 같이 의존성을 추가하였다.
  • 이외 설정은 현재는 추가하지 않는다.

그럼 이제 다음 프로젝트(모듈)인 module-api의 코드를 작성해보자.

module-api은 실질적으로 module-core의 클래스들을 사용할 것이기 때문에 Service 를 작성해보자.

MemberService.java

package me.dragonappear;

import lombok.RequiredArgsConstructor;
import me.dragonappear.domain.Member;
import me.dragonappear.repository.MemberRepository;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public Long signup (Member member) {
        return memberRepository.save(member).getId();
    }

}

MemberRepositorybean injection@Autowired(이하 필드 injection)없이 생성자 injection을 사용하였다.

module-apibuild.gradle에도 사용할 의존성들을 추가

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

자 여기까지 하시면 아마 엄청난 양의 빨간줄을 볼 것이다.
이는 몇가지 이유가 있는데 이는 아래와 같은 이유 때문이다.

module-api가 사용하는 MemberMemberRepository를 찾지 못했다.
spring boot 관련 의존성이 관리되지 못하고 있다.
그래서 이를 수정해보자.

multi-modulesettings.gradle을 열어서 아래 코드를 추가한다.

rootProject.name = 'multi-module'
include 'module-core'
include 'module-api'
include 'module-web'

이 코드는 multi-module 프로젝트가 'module-common', 'module-api' 프로젝트를 하위 프로젝트로 관리하겠다는 의미이다.

settings.gradle의 내용은 여기서 끝이다. 바로 build.gradle 코드를 작성하겠습니다.

root 프로젝트인 multi-modulebuild.gradle을 아래와 같이 수정하자.

build.gradle

buildscript {
    ext {
        springBootVersion = '3.0.0'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "io.spring.gradle:dependency-management-plugin"
    }
}

subprojects {
    group = 'me.dragonappear'
    version = '1.0.0'
    sourceCompatibility = '17'

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

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        runtimeOnly 'com.h2database:h2'
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
    }
}

project(':module-api') {
    dependencies {
        implementation project(':module-common')
    }
}
  • subprojectssettings.gradle 에 include 된 프로젝트 전부를 관리한다.

    • 하위 프로젝트들 모두 SpringBootjava에 의존성을 두고 있기에 관련된 plugin을 여기에 등록하였다.
  • project의 경우 하위 프로젝트간의 의존성을 관리한다.

    • : 은 디렉토리 path를 이야기 한다.
    • root프로젝트에서 하위 프로젝트 사이에 계층이 하나 존재하기 때문이 추가하였다.
    • module-api는 module-core에 의존하고 있기 때문에 이를 등록하였다.
  • buildScript: gradle 이 빌드되기전 실행되는 설정

  • allprojects: 현재 root 프로젝트와 앞으로 추가될 서브 모듈에 대한 설정

    • root 프로젝트까지 적용하고 싶다면 allProject로 등록하면 된다.

이렇게 설정할 경우 각 프로젝트에 이제 module-core 코드를 사용할 수 있게 된다.

이게 끝일까?

그럼 잘 적용되는지 테스트 해보자

먼저 module-core이다.

MemberRepositoryTest.java

package me.dragonappear.repository;

import me.dragonappear.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
public class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    public void add () {
        memberRepository.save(new Member("test", "test@gmail.com"));
        Member member = memberRepository.findById(1L).get();
        Assertions.assertThat(member.getName()).isEqualTo("test");
    }
}

테스트를 한 번 실행해보자
테스트가 통과가 되지 않을 것이다.

테스트가 실패한 이유은 module-core 프로젝트의 경우 @SpringBootApplication 과 같은 SpringContext를 불러오는 포인트가 없어서 이다.

이를 위히 임시 시작 포인트용 클래스인 ModuleCoreApplicationTests를 생성하자

ModuleCoreApplicationTests.java

package me.dragonappear;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ModuleCoreApplicationTests {
    public void contextLoads() {}
}

그러면 프로젝트 코드가 정상적으로 작동될 것이다.

이제 멀티 모듈의 기능을 확인하기 위해 module-api 기능을 테스트해보자

ModuleApiApplicationTests.java

@SpringBootTest
class AccountServiceTest {

    @Autowired AccountService accountService;

    @DisplayName("signUp 테스트 - 성공")
    @Test
    void sign_up_success() throws Exception {
        Long id = accountService.signUp("test","test@gmail.com");
        Assertions.assertThat(id).isEqualTo(1L);
    }
}

즉, 서로 다른 프로젝트 간에 클래스들을 사용할 수 있음을 확인할 수 있다.

개발 단계에서는 이렇게 설정해서 사용한다면 동일한 클래스를 사용해야 하는 여러 프로젝트에서 중복된 클래스 없이 개발을 진행할 수 있다.

자 그럼 한가지 이슈가 더 남아있다. 바로 빌드 시에 공통 모듈을 각 프로젝트 별로 포함시키는 것이다.

현재 상황에서 바로 프로젝트 전체 빌드를 실행해보자.

module-core 프로젝트의 빌드가 실패한다.

그 이유는 gradle 빌드시에는 각 프로젝트를 실행가능한 jar 형태로 만들게 되는데, module-core 프로젝트의 경우 main 메서드가 없기 때문이다.

그렇다고 SpringMVC를 module-web에 추가할 수는 없다

이렇게 단순히 참조용 클래스들만 있는 프로젝트를 위해 gradle 에서는 bootRepackage.enabled 설정을 제공한다.

module-corebuild.gradle 을 아래와 같이 변경한다.

빌드를 다시 해보자

그러면 정상적으로 빌드가 될 것이다.

0개의 댓글