개발 공부 기록하기/04. Spring & Spring Boot

[Kotlin + Spring Boot + Querydsl] gradle multi project 구성하기

lannstark 2021. 2. 8. 11:48

안녕하세요! 공부하는 개발자 입니다!

이번 포스트에서는 Kotlin, Spring Boot, Querdsl을 이용하여 gradle multi porject를 구성하는 방법에 대해 알아보겠습니다.

 

본 포스트를 작성하게 된 계기는 최근 이직한 회사의 프로젝트 구성이 git repository N개와 git submodule을 사용한 형태였기 때문인데요,

따라서 gradle multi module의 본격적인 구성 방법을 살펴보기에 앞서, 한 시스템 내에서 사용되는 모듈을 관리할 수 있는 또 다른 방법인 git repository N 개 + git submodule 에 대해서 간단히 설명드려 보겠습니다 🙂

Git repository N개 + Git submodule 방식

사실 생각해보면 꼭 gradle multi porject만이 모듈 관리를 할 수 있는 유일한 방법은 아닙니다. 단일 프로젝트를 여러개 만들고 각각을 독립적인 git repository에 올린뒤 git submodule을 사용하여 repository간의 관계를 맺어주는 방식으로 풀 수도 있죠.

 

예를 들면 다음과 같은 방식입니다.

시스템을 구성하는데 공통된 도메인을 가지고 있는 core 모듈과, core를 사용해야 하는 독립적인 두 개의 WAS가 있다고 가정해보면 총 3개의 git repository가 생성되고 하위 모듈의 git repository를 submodule로 등록하는 방법이죠

 

만약 새로운 요구사항을 위한 작업을 API와 ADMIN (그리고 당연히 Core에) 진행해야 한다면 다음과 같은 순서로 진행되게 됩니다.

  1. API 를 IDE로 열어 Core와 API를 작업한다
  2. API repo(로컬 폴더) 내에 있는 Core 폴더 (submodule 폴더)로 들어가 Core를 commit하고 push 한다
  3. API 를 commit하고 push한다
  4. ADMIN repo(로컬 폴더)로 들어가 최신 Core를 pull 받는다
  5. ADMIN을 IDE로 열어 작업한다.. 하지만 보통 ADMIN 작업을 하다보면 당연히 Core도 건드리게 된다
  6. 그럼 이제 ADMIN repo (로컬 폴더) 내에 있는 Core 폴더 (submodule 폴더)로 들어가 Core를 commit하고 push 한다
  7. ADMIN을 commit하고 push 한다
  8. 최신 Core가 업데이트 되었으므로 API repo (로컬 폴더) 에서 최신 Core 를 가리키도록 하고 API를 다시commit, push 한다

 

네.. 여러 모듈을 사용할 수는 있지만 굉장히 복잡하고 번거롭게 됩니다. 또한 단순히 번거롭다는 문제만 있는 것은 아닙니다. 제가 생각하기에는 다음과 같은 단점들이 있었습니다.

Git repository N개 + Git submodule 방식의 문제점

  • git repository가 늘어남에 따라 관리 포인트가 증가된다.
    • 실제로 repository 마다 git flow 전략도 통일되어 있지 않았습니다.
  • 도메인 변경이 있는 경우 혹은 요구사항의 범위가 넓은 경우 모든 repository를 작업하고 각각 PR을 넣어야 한다.
    • 위에서 말씀드린 것처럼 일반적인 작업은 상위 모듈 - 하위 모듈간의 변경이 함께 이루어지는 경우가 많기 때문에 전체적인 작업이 번거로워지죠.
  • 추가적인 모듈을 필요로 할 때 (중간계층 모듈 또는 외부 connector 모듈, 유틸성 모듈 등) 또다른 git repository가 필요하고 이는 당연히 번거로움을 야기한다.
    • 이 때문에 프로젝트가 '공통된 것을 잘 묶어보자' 보다는 'git repository 수를 줄이기 위해 상위 모듈에서 복사 붙여넣기 하자'는 방향으로 발전하게 됩니다.
  • IDE에 도움을 받을 수 없게 되며, 코드 추적이 제한적이다.
    • 예를 들어 도메인의 A 로직이 admin에서 두 번, 스케쥴러에서 한 번, api에서 한 번 쓰이고 있다면 인텔리제이를 3개 띄어두고 각각 하나씩 확인해보아야 합니다 ㅠㅠ
    • core의 코드가 어디서 쓰이는지 한 IDE project에서 확인할 수 없기 때문에 코드 삭제에 실수할 여지가 많고 이는 리팩토링을 어렵게 만들게 만들죠.
  • 운영에서 문제가 생겨 롤백을 진행해야 하는 경우 여러 project에 대해 롤백을 해야 한다.
  • 전체 테스트를 수행하기 어렵고, 소나큐브와 같은 정적분석 도구 사용이 어렵다.

 

다행히 이러한 단점을 기존 프로젝트 담당자 분께 잘 설명드렸고, 다행히 multi module을 빠르게 도입할 수 있었습니다.

Kotlin + Spring Boot + Querydsl gradle multi porject

  • Kotlin 버전 : 1.4.20
  • Spring Boot 버전 : 2.2.2
  • gradle 버전 : 6.7.1

예시로 보여드릴 multi project는 정말 뼈대만 잡아둔 프로젝트로 상위 모듈인 multi-module-api 와 하위 모듈인 multi-module-core 로 이루어져 있습니다.

멀티 프로젝트와 querydsl 동작여부 확인은 상위 모듈에 querydsl repository를 만들어 테스트 코드로 확인할 예정입니다! 자 바로 가보시죠~

우선 최상위 build.gradle 의 코드입니다.


buildscript {
  ext {
    kotlinVersion = '1.4.20'
    springBootVersion = '2.2.2.RELEASE'
    querydslVersion = '4.2.1'
  }

  repositories {
    mavenCentral()
  }

  dependencies {
    // kotlin-spring classpath
    classpath("org.jetbrains.kotlin:kotlin-allopen:${ kotlinVersion}")
    // kotlin-jpa classpath
    classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
    // Kotlin with gradle project classpath
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
  }
}

plugins {
  id "org.springframework.boot" version "${springBootVersion}"

  // 요즘 querydsl을 플러그인 방식 말고 다르게도 설정하던데
  // 여기서는 플러그인 방법을 소개하고 있습니다!
  id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"

  id "java-library"
  id "application"
}

// id 'application'과 관련이 있으며 mainClassName을 설정하지 않는 경우 
// https://wbluke.tistory.com/28 와 같은 일이 일어납니다..
mainClassName = 'com.package.ServieNameAPI'

repositories {
  mavenCentral()
}

allprojects {
  group = "com.package"
  version = "0.1.0"
  java.sourceCompatibility = JavaVersion.VERSION_11

  apply plugin: 'io.spring.dependency-management'
  apply plugin: "org.springframework.boot"
  apply plugin: "kotlin"

  apply plugin: "kotlin-jpa"
  apply plugin: "kotlin-allopen"
  allOpen {
    annotation "javax.persistence.Entity"
    annotation "javax.persistence.MappedSuperclass"
  }

  apply plugin: "kotlin-spring"
  apply plugin: "kotlin-kapt"

  sourceCompatibility = 1.8
  targetCompatibility = 1.8

  repositories {
    mavenCentral()
  }

  dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")

    // kotlin standard library for jdk 8
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"

    // QUERY DSL
    implementation("com.querydsl:querydsl-jpa:${querydslVersion}")
    kapt("com.querydsl:querydsl-apt:4.2.1:jpa")

    runtimeOnly("com.h2database:h2")
    runtimeOnly("mysql:mysql-connector-java")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
      exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
  }

  compileKotlin {
    kotlinOptions {
      jvmTarget = "1.8"
      languageVersion = "1.4"
    }
  }

  compileTestKotlin {
    kotlinOptions {
      jvmTarget = "1.8"
      languageVersion = "1.4"
    }
  }

  test {
    useJUnitPlatform()
  }

}

간단하죠?!

 

다음은 API 모듈과 Core 모듈의 build.gradle입니다 ㅎㅎ

// API
dependencies {
  implementation project(':multi-module-core')
}

// Core
bootJar.enabled = false
jar.enabled = true

 

아 추가로 (다 아실테지만..) settings.gradle.kts 입니다~

rootProject.name = "multi-module"

include(
  "multi-module-api",
  "multi-module-core"
)

이제 멀티 모듈 자체의 설정은 끝났으니 잘 동작하는지 확인하기 위해 도메인 객체와 테스트 코드를 작성해보겠습니다!

 

먼저 초간단 User 도메인 객체와 Repository Core 모듈에 들어갑니다~

// [Core 모듈] User.kt (id, name)만 있는 초 간단 User 테이블
@Entity
class User(
  var name: String
) {

  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = 0

}

// [Core 모듈] UserRepository.kt
interface UserRepository : JpaRepository<User, Long>

이제 API 모듈에서 querydsl을 이용한 repository를 만들건데요! querydsl 생성은 gradle > compileKotlin을 누르시면 됩니다! (정확히 이것만 되는것은 아닐 것 같은데 Java와 다르게 compileQuerydsl로는 안되더라고요.. 제가 설정을 잘못한 것일 수도...)

// [API 모듈] UserApiRepository.kt
@Repository
class UserApiRepository(
  private val queryFactory: JPAQueryFactory // Bean 주입이 필요합니다~ (블로그 글에서 생략)
) {

  fun findUser(name: String): User {
    return queryFactory.select(user)
      .from(user)
      .where(
        user.name.eq(name)
      )
      .fetchFirst()
  }

}

그 다음 간단히 테스트 코드를 작성해주고 (물론 @SpringBootApplication main method가 필요합니다 ㅎㅎ) 테스트를 돌려주면...

@SpringBootTest
class UserApiRepositoryTest(
  @Autowired private val userApiRepository: UserApiRepository,
  @Autowired private val userRepository: UserRepository
) {

  @Test
  fun `이름으로_유저를_검색한다`() {
    // given
    userRepository.save(User("lannstark"))

    // when
    val findUser = userApiRepository.findUser("lannstark")

    // then
    assertThat(findUser.id).isNotEqualTo(0)
    assertThat(findUser.name).isEqualTo("lannstark")
  }

}

정상적으로 성공하게 됩니다!

자 이상으로 Kotlin + Spring Boot + Querydsl gradle multi project 구성을 간단히 알아보았습니다 ㅎㅎ

 

가장 상단의 build.gradle만 복잡하고 나머지는 크게 어려울게 없었네요 ㅋㅋㅋㅋ

해피 코딩 되세요~!