본문 바로가기
프로그래밍/코프링

코프링, 성공적인 서비스를 위한 멀티 모듈 프로젝트 구성 (Feat. 코틀린 Gradle)

by K-인사이터 2024. 4. 5.
반응형

안녕하세요 

K-인사이트 입니다. 

 

 

창업 초창기 빠른 개발을 위해 프로젝트들이 새롭게 개발되고 발전함에 따라 서서히 개발자들의 발목을 잡게되는 요소가 있습니다. 바로 의존성 관리와 중복되는 코드의 범람이 그것입니다. 또한 각 프로젝트들이 강한 의존성을 가지게 됨과 동시에 비슷하지만 약간은 다르게 개발된 코드들을 두고 치열한 리뷰가 계속되는 악전고투를 해야만 릴리즈를 할 수 있게되는 상황이라면 멀티 모듈 프로젝트로 전환하는 방향을 생각해야합니다. 

 

멀티 모듈 프로젝트란? 

멀티 모듈 프로젝트는 하나의 서비스로 관리되지만 여러 프로젝트로 분할되어 있는 것을 한데 뭉쳐서 개발하는 프로젝트 구성 방법입니다. 일반적으로 회원 API를 개발한다면 IDE 를 실행해서 회원 API 를 위한 프로젝트를 생성할 것입니다. 하지만 회원 기능을 확장하다보면 회원 API 외에도 회원을 관리하는 관리용 API, 회원 정보를 처리하는 Batch 등으로 프로젝트들이 늘어납니다. 이때마다 개별 프로젝트를 생성하게되면 모니터가 3대 5대가 있어도 부족할 지경에 이를 것입니다. 

 

그렇다면, 필연적으로 멀티 모듈 프로젝트라는 단어가 등장하면서 기존 프로젝트들을 하나로 뭉치자는 의견이 나오기 시작합니다. 이는 멀티 모듈 프로젝트가 이러한 문제를 해결하는데 최적이기 때문입니다. 회원 시스템 예시를 통해서 어떤 차이점이 있는지 살펴보겠습니다. 

 

회원 시스템을 관리하기 위해서 아래와 같이 여러 API 들이 있다고 가정하겠습니다. 

  • 회원 관리 API 프로젝트
  • 회원 API 프로젝트
  • 회원 Batch 프로젝트 

 

 

이들 프로젝트는 별도의 폴더에서 별도의 프로젝트를 구성하는 방식으로 되어 있습니다. 당연하게도 회원 관리를 위한 로직들이 어쩔 수 없이 중복되는 구조를 가집니다. 이들은 동일한 DB를 바라보기에 중복된 스키마 정보를 다루기 위한 코드를 내장해야되며 데이터 클래스 또한 중복될 것입니다. 그렇습니다. 중복이 넘쳐나는 상황입니다. 따라서, 이러한 프로젝트 구성은 중복된 코드가 프로젝트마다 배치되어 있어 만약 회원 DB의 구조의 변경이 발생한다면 모든 프로젝트들의 코드를 수정해야되는 상황이 발생합니다

 

이러한 상황의 좋은 해결책으로 언급되는 멀티 모듈 프로젝트로 구성을 변경한다면 프로젝트 구성이 어떻게 바뀔까요? 아래처럼 효율적인 구조를 가지게 됩니다. 

 

회원 시스템 프로젝트 

  • 회원 관리 API 모듈
  • 회원 API 모듈
  • 회원 Batch 모듈
  • 공통 로직 모듈

프로젝트라는 이름 대신에 모듈이라는 용어가 등장합니다. Oracle의 자바 문서에서는 모듈을 패키지의 한 단계 위의 집합체이며, 관련된 패키지와 리소스들의 재사용할 수 있는 그룹이라고 정의하고 있습니다. 즉, 재사용이 가능한 그룹이 핵심입니다. 공통 로직은 앞서 설명한 회원 DB와 관련된 데이터 클래스, 유틸리티, 스키마 정보 등을 보관하고 있습니다. 만약, DB 구조가 변경되더라도 공통 로직 개발자가 약간의 수정을 하게되면 다른 모듈은 변경하지 않아도 된다는 의미입니다. 따라서 멀티 모듈 프로젝트의 장점들을 정리하면 아래와 같습니다. 

 

  • 코드의 중복을 줄일 수 있음
  • 각 모듈의 기능을 파악하기 용이해짐
  • 손쉬운 빌드

그렇다면 코틀린으로 Spring Boot 프로젝트를 구성하는 케이스에 대해 멀티 모듈 프로젝트를 어떻게 구성하는지 알아보겠습니다. 

 

 

코틀린 Spring Boot 를 위한 멀티 모듈 프로젝트 밑그림 작업

코틀린 기반의 멀티 모듈 프로젝트를 구성하기 위해서는 아래의 주의사항을 기억해야 합니다.

  • 새 프로젝트를 생성할 때 별도의 제너레이터를 선택하지 않음
  • 언어와 빌드 시스템 선택 시, 코틀린-Gradle 혹은 코틀린-Maven 선택
  • gradle 또는 maven 설정을 조작하여 수동으로 구성
  • 내부 모듈 추가 시 Spring Initializr 를 사용하지 않음 

위 주의 사항을 숙지하셨다면 이 글의 핵심요결을 파악한 셈입니다. 많은 구독자 분들이 IDE 마법사 기능에 지나치게 의존하는 경향이 있어 아래의 질문들이 있었습니다. 

  • 내부 모듈을 추가할 때 스프링 이니셔라이저를 사용하면 내부 모듈을 인식하지 못해요
  • 루트 프로젝트를 이니셔라이저를 통해서 생성했는데 설정이 너무 복잡해졌어요
  • 등등

멀티 모듈 프로젝트를 구성하는 제일 좋은 방법은 Gradle, Maven 의 빌드 시스템에 익숙해지는 것입니다. 이 글에서는 이 부분에 초점을 맞추어 내용을 전개하므로 단단한 기본기를 챙겨갈 수 있습니다. 스텝 바이 스텝으로 같이 따라해보세요. 

 

 

프로젝트 생성 단계

IntelliJ IDE 기준으로 설명을 드리겠습니다. 프로젝트 생성할 때 Generators 섹션을 무시하고 New Project 를 선택합니다. 그리고 개발 언어로 Kotlin, 빌드 시스템으로 Gradle을 선택합니다. JDK 버전은 적당한 버전을 선택해줍니다. 프로젝트가 생성되면 src 폴더가 생성되어 있습니다. 여기서 주의할 점은 루트 프로젝트는 모듈 구성을 위한 용도로만 사용되며 코드를 관리하지 않는다라는 점입니다. 따라서 과감히 src 폴더를 삭제합니다. 

 


멀티 모듈 추가 

루트 프로젝트에 필요한 모듈들을 생성하겠습니다. 회원 시스템을 위해서 core, api, batch 모듈이 필요합니다. 아직 batch 모듈을 개발하는 일정은 없으나 미리 생성해두는 시나리오로 가보겠습니다. 프로젝트 최상위 폴더를 우클릭하고 New > Module 을 선택합니다. 그리고 동일하게 Generators 를 사용하지 않고 코틀린-Gradle 조합으로 생성합니다. 여기서 Parent 항목에 루트 프로젝트의 이름이 선택되어야 합니다. 아래의 이미지와 동일하게 api, core, batch 라는 이름으로 모듈을 생성해주면 모듈 추가가 완료됩니다. 

 

앞서 Parent 항목을 선택하는 이유는 루트 프로젝트의 settings.gradle.kts 파일을 통해서 확인할 수 있습니다. 모듈 추가 전에는 없었던 include 가 추가되어 있습니다. 이는 gradle 에게 하위 모듈로 이러한 모듈들이 있다고 알려주는 역할을 합니다. 

 

 

루트 프로젝트 빌드 설정

루트 프로젝트 역할은 하위 모듈들의 의존성을 관리하는 역할입니다. 따라서, src 폴더는 의미가 없으니 삭제합니다. build.gradle.kts 를 통해서 전체적은 프로젝트 설정, 플러그인, 하위 모듈의 의존성 설정을 수행할 수 있습니다. 이를 통해 하위 모듈은 해당 모듈에 필수적인 설정만 유지하는 방식을 채택하여 관리가 간편해집니다. 루트 프로젝트의 Build.gradle.kts 파일은 총 3 부분으로 나누어 구성합니다.

  1. 플러그인 세팅
  2. 모든 모듈(=프로젝트)에 공통으로 적용될 설정
  3. 하위 모듈에만 공통으로 적용될 설정

각각에 대해서 Gradle 에 대한 설명과 함께 루트 프로젝트 빌드 설정을 해보겠습니다.

Gradle Plugin(플러그인)의 역할

Gradle Plugin은 Gradle Task 집합입니다. 여기서 Gradle Task는 빌드, 테스트 등을 수행하는 작업을 말합니다. 따라서, Gradle Plugin은 “어플리케이션 빌드부터 테스트까지 다양한 작업을 수행하는 작업 단위의 집합”으로 풀어 쓸 수 있습니다.

  • 플러그인 = Gradle Task 집합
  • Gradle Task = 빌드, 테스트 등 다양한 작업을 수행하는 작업 단위
  • 플러그인 = 빌드, 테스트 등 다양한 작업을 수행하는 작업들의 집합

 

 

 

예를 들어, 안드로이드 어플리케이션 개발을 위해서 필요한 라이브러리들을 받아온 후 이 라이브러리들을 사용해 코틀린이나 자바 코드를 컴파일하고, 컴파일된 바이트코드의 묶음을 패지킹하여 APK나 AAB 파일로 만들어야 합니다. 이러한 작업들을 일일히 Gradle 파일로 정의를 하면 개발자들의 부담이 매우 클 것입니다. 따라서, 구글, Jetbrains와 같은 개발사에서 미리 플러그인을 제작해두고 사용하도록 공개하고 있습니다. 따라서 우리는 앞의 예시의 작업들을 플러그인을 통해서 대체하고 개발작업에 집중할 수 있게됩니다.

 

그렇다면, 루트 프로젝트의 플러그인이 해야 될 역할은 전체 프로젝트를 아우르는 기능들이 포괄되어야 할 것입니다. 프로젝트는 코틀린 언어로 개발되고 Spring Boot 프레임워크를 사용하도록 결정했습니다. 따라서, 관련된 플러그인들을 등록하여 빌드, 테스트, 의존성 관리 등을 적절히 수행하도록 설정해야 합니다.

  • “org.springframework.boot” 플러그인은 Spring Boot 프로젝트를 관리하는 데 사용됩니다. Spring Boot 기능을 Gradle 빌드에 통합하며, 자동 설정, 의존성 관리, 빌드 설정 등을 제공합니다.
  • “io.spring.dependency-management” 플러그인은 Spring 프로젝트의 의존성 버전을 관리하는데 사용되며 의존성 버전이 일관되게 유지하고 관리하는데 사용됩니다.
  • kotlin("jvm") 플러그인은 코틀린(Kotlin)언어를 JVM(Java Virtual Machine)에서 실행할 수 있도록 전환합니다. 버전 1.9.23 버전은 코틀린 컴파일러의 버전을 지정합니다.
  • “kotlin(“plugin.spring”) 및 kotlin(“plugin.jpa”)” 플러그인들은 각각 코틀린과 Spring 프레임워크 통합을 위해 사용됩니다. Spring 프로젝트에서 코틀린 코드를 사용하고 Spring 기능을 활용할 수 있도록 도와주는 역할을 합니다.

 

이제, 첫번째 영역으로 플러그인을 설정하는 내용을 작성할 수 있습니다. 루트 프로젝트는 소스코드를 관리하지 않으므로 apply false를 통해서 적용하지 않도록 일단 설정합니다.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.2.3" apply false
    id("io.spring.dependency-management") version "1.1.4" apply false
    kotlin("jvm") version "1.9.23"
    kotlin("plugin.spring") version "1.9.22" apply false
    kotlin("plugin.jpa") version "1.9.22" apply false
}

모든 프로젝트(allprojects) 공통 설정

Gradle 설정은 모든 프로젝트에 공통으로 적용되는 구성을 정의하는 기능을 제공합니다. allprojects 블록 내에서 이러한 설정을 구현할 수 있습니다. 루트 프로젝트 및 하위 모듈들이 모두 코틀린으로 개발한다는 전제로 공통 구성을 구현하면 아래와 같습니다. 구성은 크게 3 파트로 나누어집니다. 프로젝트 의존성을 해결하기 위한 Maven 리포지토리 설정, Java 및 코틀린 컴파일 설정, 마지막으로 테스트 설정입니다.

allprojects {
    group = "com.kin"
    version = "1.0-SNAPSHOT"

    repositories {
        mavenCentral()
    }

    tasks.withType<JavaCompile>{
        sourceCompatibility = "20"
        targetCompatibility = "20"
    }

    tasks.withType<KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "20"
        }
    }

    tasks.withType<Test> {
        useJUnitPlatform()
    }
}

 

 

위 Gradle 설정은 모든 프로젝트에 대해 공통적으로 적용되는 구성을 정의합니다. 설정의 각 부분에 대해 자세히 설명하겠습니다. 모든 프로젝트에 공통적으로 적용되어야할 내용은 무엇일까요? 프로젝트의 버전(Version), JAVA 언어에서 계승되는 그룹 식별자가 있을 것입니다. 그리고 언어의 버전을 통일성 있게 가져가기 위한 자바, 코틀린 언어 버전을 설정하는 영역도 필요합니다. 마지막으로 Unit 테스트를 위한 설정도 필요하겠습니다.

 

group 및 version 설정:

  • group = "com.kin": 프로젝트의 그룹 식별자를 설정합니다. 보통은 회사 또는 조직의 도메인을 역순으로 사용합니다.
  • version = "1.0-SNAPSHOT": 프로젝트의 버전을 설정합니다. "-SNAPSHOT"은 개발 중인 버전임을 나타냅니다.

레포지토리 설정:

  • repositories { mavenCentral() }: 프로젝트에서 의존성을 해결하기 위해 Maven Central 레포지토리를 사용하도록 설정합니다. Maven Central은 널리 사용되는 Java 라이브러리를 호스팅하는 중앙 저장소입니다.

Java 및 Kotlin 컴파일러 설정:

  • tasks.withType<JavaCompile>: Java 컴파일러 타입의 모든 작업에 대해 설정을 적용합니다.
  • sourceCompatibility = "20" 및 targetCompatibility = "20": Java 코드의 소스와 대상 호환성을 Java 20으로 설정합니다.
  • tasks.withType<KotlinCompile>: Kotlin 컴파일러 타입의 모든 작업에 대해 설정을 적용합니다.
  • kotlinOptions { ... }: Kotlin 컴파일러 옵션을 설정합니다.
  • freeCompilerArgs = listOf("-Xjsr305=strict"): JSR-305 어노테이션을 엄격하게 처리하도록 설정합니다.
  • jvmTarget = "20": Kotlin 코드의 JVM 타겟을 Java 20으로 설정합니다.

테스트 설정:

  • tasks.withType<Test>: 테스트 작업에 대해 설정을 적용합니다.
  • useJUnitPlatform(): JUnit 플랫폼을 사용하여 테스트를 실행하도록 설정합니다. JUnit 플랫폼은 JUnit 5부터 사용 가능한 새로운 테스트 엔진입니다.

하위 모듈(subprojects) 설정

앞서 Build 과정에 사용될 플러그인들을 나열하고 모든 프로젝트에 공통으로 적용될 사항들을 정의하였습니다. 마지막으로 하위 모듈들의 설정을 다루어보겠습니다. 여기서 정의한 하위 모듈 Build 과정은 settings.gradle.kts 파일에서 include 처리한 모듈들에 한해서 적용됩니다. 보통 플러그인을 적용하거나 하위 모듈에 공통으로 필요한 의존성을 선언해주는 역할을 합니다.

subprojects {
    apply(plugin = "org.springframework.boot")
    apply(plugin = "io.spring.dependency-management")
    apply(plugin = "org.jetbrains.kotlin.plugin.spring")
    apply(plugin = "org.jetbrains.kotlin.plugin.jpa")
    apply(plugin = "kotlin")
    apply(plugin = "kotlin-kapt")

    dependencies {
        //Spring Web 의존성
        implementation("org.springframework.boot:spring-boot-starter-web")

        //공통사용
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        testImplementation("org.springframework.boot:spring-boot-starter-test")
    }
}

 

공통 라이브러리 및 API 모듈 구성

멀티 모듈 프로젝트 목적은 모듈 간의 의존성을 관리하고 단일 프로젝트에서 여러 모듈들을 개발하는 것입니다. 따라서, kin-core 모듈을 통해서 로깅 인터페이스를 제공하고 kin-api 모듈에서 이를 사용하는 예시를 통해서 하위 모듈간의 의존성을 정의하는 방법을 효과적으로 학습할 수 있습니다. 예제 프로젝트는 bitbucket 을 통해서 관리하고 있으며 이 글 맨하단에 repository 주소를 공유드리오니 글을 끝까지 읽고 다운로드해서 적용해보는 것을 추천드립니다.

공통 라이브러리 모듈 구성

루트 프로젝트 경로를 기준으로 이미 내부 모듈들을 생성하였습니다. 우선 core 라이브러리에서 로깅 인터페이스를 제공하는 구현을 진행해보겠습니다. core 모듈에 한정된 의존성을 우선 설정하는 것 부터 시작하겠습니다. 아래의 설정은 build.gradle.kts 파일의 내용으로 core 모듈만이 사용할 의존성을 정의합니다.

 

멀티 모듈 프로젝트를 구성할 때 공통 라이브러리를 구성하게되면 두가지 사항을 기억해야 합니다. Spring Boot 와 관련된 BootJar Task 설정 그리고 Jar Task 설정이 그것입니다. 아래 설정을 보면 Jar Task 를 활성화하고 BootJar Task 를 비활성화 한것을 볼 수 있습니다. 그리고 로깅을 위한 의존성 설정이 추가되었습니다. 로깅 의존성 설정은 공통 라이브러리에서 로깅 관련 인터페이스를 제공할 예정이므로 쉽게 이해가 가능합니다.

 

Jar 와 BootJar 를 이해하기 위해서는 약간의 지식이 필요합니다.

  • Jar 태스크: 프로젝트의 소스코드 및 리소스를 사용하여 Java Archive(JAR)파일을 생성하는데 사용합니다. 즉, 라이브러리 혹은 애플리케이션 배포를 위해 JAR 파일이 필요하므로 필요한 설정입니다.
  • BootJar 태스크: Spring Boot 애플리케이션을 실행하는데 필요한 JAR 파일 빌드를 위해 사용합니다. Spring Boot는 내장 Tomcat 서버를 사용하여 어플리케이션을 실행할 수 있도록 설계되어 있기 때문에 내장된 Tomcat 서버를 같이 배포하려면 별도의 태스크가 필요합니다. 또한, Spring Boot 설정을 제어하는 application.properties 또는 application.yaml 설정 파일에 정의된 구성을 로드하기 위해서도 필요합니다. 하지만, 공통 라이브러리에서는 Spring Boot 를 사용하지 않으므로 이 설정이 필요하지 않습니다. 따라서, 비활성화 처리를 해줍니다.

 

 

import org.springframework.boot.gradle.tasks.bundling.BootJar

val jar: Jar by tasks
val bootJar: BootJar by tasks

bootJar.enabled = false
jar.enabled = true

dependencies {
    //로그 의존성
    api("io.github.microutils:kotlin-logging-jvm:2.0.11")
}

 

공통 라이브러리에서 로깅 인터페이스를 제공하기 위해 Logging.kt 파일을 생성해서 아래와 같이 로깅 인터페이스를 구현해줍니다.

package com.kin.kincore.log

import mu.KotlinLogging

val logger = KotlinLogging.logger {}

Spring Boot 모듈 구성 및 의존성 연결

마지막 단계로 Core 모듈을 사용하는 Spring Boot 앱 모듈을 구성합니다. 앞서 루트 프로젝트의 빌드 설정에서 Spring Boot 의존성을 이미 구성하였기에 Core 모듈을 의존성으로 지정만하면 작업은 완료됩니다. 또한, Spring Boot 빌드를 위한 BootJar 태스크를 활성화하는 작업이 필요합니다. 아래의 설정을 참고해주세요.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

import org.springframework.boot.gradle.tasks.bundling.BootJar

val bootJar: BootJar by tasks
bootJar.enabled = true

dependencies {
    implementation(project(":kin-core"))
}

 

 

src 경로에 Application.kt 파일을 생성해서 앱 실행시 진입점인 main 함수를 선언해줍니다.

package com.kin.kinapi

import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Configuration

@SpringBootApplication
class MongoExampleApplication

fun main(args: Array<String>) {
    runApplication<MongoExampleApplication>(*args)
}

 

 

그리고, core 모듈의 로깅 인터페이스를 사용하는 간단한 컨트롤러를 만들어줍니다.

package com.kin.kinapi

import com.kin.kincore.log.logger
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/")
class Controller() {

    @GetMapping
    fun getCount(): String {
        logger.info { "hello world" }
        return "HelloWorld"
    }
}

 

프로젝트 실행 및 테스트

API 모듈을 실행하고 공통 라이브러리가 잘 호출되는지 테스트해보겠습니다. kin-api 모듈에서 Application.kt 파일을 선택하고 실행하면 아래 그림처럼 정상적으로 실행됨을 확인할 수 있습니다.

 

 

localhost:8080 주소로 접속해보겠습니다. 의도한 대로 core 모듈에서 정의한 logging이 호출되며 로그가 출력되는 것을 확인할 수 있습니다.

 

 

샘플 프로젝트 다운로드

이 글에서 다룬 내용을 직접 다운로드하여 실습할 수 있도록 코드를 Bitbucket을 통해 공유한 상태입니다. 아래의 링크에 접속하여서 Clone 버튼을 누르면 실습 내용을 그대로 재현하여 테스트할 수 있습니다.

 

 

https://bitbucket.org/kinstory/multi_ex01/src/main/

 

Bitbucket

 

bitbucket.org

 

 

맺음말

지금까지 코틀린 개발을 위한 필수 과정인 멀티 모듈 프로젝트를 구성하는 방법에 대해 알아보았습니다. 최대한 여러분이 헤매지 않도록 필수적인 내용을 검증하여 설명드렸습니다. 관련해서 궁금한 내용이 있다면 댓글을 통해서 소통해주시면 감사하겠습니다.

 

이상입니다.

K-인사이트 올림. 

반응형