스프링

[스프링] 멀티 모듈(Multi Module) 개념/예제 feat. Gradle

Awdsd 2021. 12. 19. 21:44
반응형

최근 진행하는 토이 프로젝트의 API 서버는 서로 독립된 프로젝트 2개로 이루어져있었다.

  • 인증 서버
  • 어플리케이션 서버

처음에는 기능 구현 자체에 초점을 맞추고 각자의 프로젝트 크기도 크지 않아서 불편함을 느끼지 못했다. 하지만 프로젝트를 진행하면서 크기가 커지면서 domain 을 각각 프로젝트에 선언했기 때문에 동일성을 보장하기 위해 똑같은 작업을 두개의 Applicaton에 작업을 해줘야했다. 또한 공통적인 Repository도 각 Application에서 작성해줘야하는게 여간 번거로운게 아니었고, 똑같은 코드가 생긴다는게 썩 마음에 들지 않았다.

그리고 알아보던중 멀티 모듈(Multi Module)에 대해 알게 되었다.

이번 글에서는 멀티 모듈 개념/예제에 대해 알아보자.


멀티 모듈 이란?

멀티 모듈이란 서로 독립적인 프로젝트(인증, 어플리케이션)를 하나의 프로젝트로 묶어 모듈로서 사용되는 구조를 말한다.

멀티 모듈을 사용하면 공통적인 기능을 모아 하나의 모듈로 만드는 것이 가능하다. 즉, 인증과 어플리케이션에서 공통으로 사용하는 util, domain, Repository등을 모듈로 분리해 사용할 수 있는 것이다.

멀티 모듈에 관련해 더 자세하게 알고 싶은 경우 아래 글을 참고하길 바란다.

https://techblog.woowahan.com/2637/


멀티 모듈 간단 예제

이제 간단하게 Gradle을 사용해 멀티 모듈 프로젝트를 만들어보자.

먼저 모듈들을 모을 Gradle 프로젝트를 하나 만들어주자.

MacOS IntelliJ 기준 File→New→Project 경로에 위의 사진처럼 Gradle 프로젝트를 하나 생성한다.

그럼 위와 같은 구조의 프로젝트가 생성된다. 일단 모듈을 담을 프로젝트이기때문에 src폴더가 필요없기 때문에 src폴더는 삭제해주자 그리고 build.gradle 아래와 같이 입력한다.

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

allprojects {}

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

    group = 'kr.multi.ex'
    version = '1.0'
    sourceCompatibility = '11'

    repositories {
        mavenCentral()
    }

    dependencies {
        compileOnly 'org.projectlombok:lombok:1.18.16'
        annotationProcessor 'org.projectlombok:lombok:1.18.16'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation('org.springframework.boot:spring-boot-starter-test') {
            exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
        }
    }
}

위의 큰 3개의 Closure를 간단하게 설명하면 다음과 같다

buildscript : Gradle 이 빌드되기전 실행되는 설정

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

subprojects : 전체 서브 모듈 에 해당되는 설정

이제 모듈들을 생성해보자. 모듈은 core, api 를 생성해보자.

corespring-boot-starter 의존성을 가지고 있고 이것을 api가 사용할 수 있게 해보자.

root 폴더를 우클릭 → New → Module을 클릭해서 Gradle Module을 생성해주자

생성하면 다음가 같은 폴더 구조가 될 것이다.

이제 settings.gradle을 보면 다음과 같이 방금 생성한 coreinclude 된것을 확인할 수 있다. 이것은 하위 모듈로 선언한다는 의미이다.

IntelliJ의 Gradle Tab에서도 보면 다음과 같이 coreroot 하위에 존재하는 것을 확인할 수 있다.

corebuild.gradle 에 아래처럼 의존성만 추가하자

//core build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web:2.6.1'
}
bootJar {
    enabled = false
}

jar {
    enabled = true
}

여기서 중요한 것은 bootJarjarbootJar은 실행가능한 jar를 만들려 하기 때문에 main()이 필요하다 그렇기 때문에 main()이 없는 coreenabledfalse로 해줘야하고 결론적으로 저걸 넣지 않으면 추후 core에 있는 Bean Class를 다른 모듈에서 사용할 때 에러가 발생할 수 있다.

그리고 의존성을 추가할 때 implementation 대신 compile을 사용했는데 그 이유에 대해서는 밑에서 설명하겠다

이제 gradle을 갱신하면 rootbuild.gradle subprojects에 선언한 lombokspring-boot-starter가 추가된 것을 확인할 수 있다.

바로 api 모듈도 core가 똑같은 과정을 통해 만들면 다음과 같은 구조가 될 것이다.

apibuild.gradle은 다음과 같이 아무런 의존성을 넣지 않았다.

//api build.gradle
dependencies {}

이제 rootbuild.gradle 맨 아래에 다음을 추가해보자.

project(':api') {
    dependencies {
        implementation project(':core')
    }
}

api 모듈에 core의 의존성을 추가하라는 의미이다.

갱신해보면 아래처럼 apispring-boot-starter 추가된 것을 확인할 수 있다.

이제 api에서 @SpringBootApplication을 선언했을 때 정상적으로 사용할 수 있다.

coreBean Class를 만들고 api에서 호출하는 예제는 다음과 같다. 간단하게 @Service Bean을 만들어 테스트한 코드다.

//core 모듈
@Service
public class TestService {
    public String test() {
        return "core의 Bean Class 테스트";
    }
}

//api 모듈
@RestController
@RequiredArgsConstructor
public class TestController {
    private final TestService testService;
    @GetMapping("/test")
    public String test() {
        return testService.test();
    }
}

구조로 보면 다음과 같다.

이제 api 를 실행후 /test를 호출하면 아래의 결과를 볼 수 있다.

여기서 주의할 점은 coreapi의 패키지 구조다. 현재 상위 패키지가 kr.multi.ex로 통일 된 것을 볼 수있는데 이렇게 통일하지 않으면 api에서 core의 bean을 읽어오지 못해 에러가 발생한다.

만약 위처럼 api의 패키지를 kr.multi.ex2로 변경하면 패키지가 공통이 되지 않기 때문에 아래와 같은 에러를 호출한다.

좀 더 정확히 말하면 Application 클래스의 위치한 곳의 패키지가 공통으로 맞춰져 있어야한다. 만약 패키지를 못맞춘다면 아래와 같이 scanBasePackages 옵션을 통해 Bean을 스캔할 수 있다.

@SpringBootApplication(scanBasePackages = "kr.multi")
public class ApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class);
    }
}
core의 의존성을 compile로 선언한 이유

위에서 core 의존성을 추가할 때 compile을 사용했는데 그 이유는 implementation은 직접 의존하는 모듈(core)외에서는 사용할 수 없기 때문이다.

corespring-boot-starterimplementation으로 선언할 경우 api에서는 spring-boot-starter를 가져오지 않는다.

위처럼 core의 의존성을 implementation으로 할 경우 api에서는 못가져오는 것을 볼 수 있다.

하지만 compilegradle에서 권장되지 않는 방식이다. 심지어 현재는 예제의 gradle6.x버전이라 compile을 사용할 수 있지만 7.0 버전 이상부터는 compile을 사용할 수 없다.

그럼 어떻게 해야하나?

7.0버전 이상부터는 api 키워드를 사용할 수 있다. apicompile이랑 비슷한 키워드라 생각하면 된다.

만약 gradle 버전을 올리고 싶다면 root 프로젝트 위치에서 해당 명령어를 통해 gradle 버전을 변경할 수 있다.

$ ./gradlew wrapper --gradle-version=7.x

api를 사용하기 위해서는 root build.gradlesubprojects의 다음을 수정해준다.
apply plugin:’java’apply plugin:’java-library’

그리고 다음처럼 의존성을 추가하면 된다.

//core build.gradle
dependencies {
    api 'org.springframework.boot:spring-boot-starter-web:2.6.1'
}
bootJar {
    enabled = false
}

jar {
    enabled = true
}

비고

https://kwonnam.pe.kr/wiki/gradle/multiproject

https://velog.io/@sangwoo0727/Gradle을-이용한-멀티-모듈

https://wellbell.tistory.com/253#comment16473032

반응형