저는 본 서버를 멀티모듈을 기반 으로 해서 작성할 예정입니다. 따라서 이 글을 읽고도 저게 뭔 소리죠? 라는 기분이 드실수도 있습니다.
우선은, 멀티모듈로 설계를 하게된 이유에 대해서부터 설명을 드리도록 하겠습니다.
제가 프로젝트를 멀티모듈로 설계하려는 이유는 다음과 같습니다!
- build.gradle.kts를 모듈별로 경량화시켜서, 각자의 역할과 책임에 맞게끔만 의존성을 가지게하자.
- 역할과 책임을 모듈별로 의식적인 분리를 하여 스파게티 코드를 방지하자 (모듈별로 코드를 계층화 시켜서 모듈간의 Dependency가 Cycle이 벌어지지 않게한다)
그 외에도 여러가지 이유가 있겠지만, 그 외의 이유는 우아한 멀티모듈 이라는 영상을 보시는 것을 추천드리겠습니다! 저보다는 훨씬 설명을 잘하셔요...
일단 프로젝트를 하나 생성하셔서, 다음과 같이 작성을 해줍니다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-noarg:1.3.71")
}
}
plugins {
id("org.springframework.boot") version "2.6.6" apply false
id("io.spring.dependency-management") version "1.0.11.RELEASE" apply false
id("org.jetbrains.kotlin.plugin.allopen") version "1.5.10" apply false
id("org.jetbrains.kotlin.plugin.noarg") version "1.5.10" apply false
kotlin("jvm") version "1.5.10" apply false
kotlin("plugin.spring") version "1.5.10" apply false
kotlin("plugin.jpa") version "1.6.10" apply false
}
allprojects {
group = "team.bakkas.yumarket"
version = "1.0.0"
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
}
subprojects {
repositories {
mavenCentral()
}
apply {
plugin("org.springframework.boot")
plugin("io.spring.dependency-management")
}
}
여기서 주목해야할 부분은, plugins{ ... } 부분입니다.
모든 모듈에서 사용하게될 플러그인들을 모두 root project에서 선언하되, root project에서는 모든 plugin들을 apply false로 설정을 하여서, 나중에 모듈에서 가져다쓰는 형식으로 루트프로젝트의 build.gradle.kts를 구성하였습니다.
그리고 subprojects { ... } 부분을 보시게되면, subprojects 블록은 모든 모듈에서 공통적으로 적용되는 옵션 을 정의하는 부분입니다.
여기서 저는 springframework.boot, spring.dependency-management 플러그인을 활성화 시켜두었는데요, 이 두개의 플러그인은 각 모듈의 역할과 책임에 관계없이 사용해야하는 플러그인 속성들이기 때문에 저는 모든 모듈에 대해서 열어두었습니다.
다음으로, root project의 setting.gradle.kts를 확인해보겠습니다.
rootProject.name = "yumarket"
include(
":application-shop",
":domain-rds",
":domain-dynamo"
)
settings.gradle.kts 에서는 모듈의 이름만 include 시켜서 root project에 모듈을 등록시키면 됩니다.
저는 이번 포스트에서는 domain-dynamo 모듈만 다뤄볼 예정입니다. domain-dynamo의 build.gradle.kts를 확인해봅시다.
import org.jetbrains.kotlin.builtins.StandardNames.FqNames.annotation
plugins {
kotlin("jvm")
kotlin("plugin.spring")
id("org.jetbrains.kotlin.plugin.allopen")
id("org.jetbrains.kotlin.plugin.noarg")
}
extra["springCloudVersion"] = "2021.0.2"
dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation("org.springframework.cloud:spring-cloud-starter-config")
implementation("software.amazon.awssdk:dynamodb-enhanced:2.17.191")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
allOpen {
annotation("software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean")
}
noArg {
annotation("software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean")
}
tasks.register("prepareKotlinBuildScriptModel") {}
저는 이 모듈에서 dynamoDB를 이용해서 repository를 구성해야하기 때문에 dynamodb-enhanced에 관한 의존성을 추가해두었습니다.
그리고 dynamoDB의 테이블과 매핑되는 entity 객체를 만들어서 통신을 할 계획이기 때문에, Spring에서 제공하는 Entity 명세 에 맞추기 위해서 appOpen, noArgs를 DynamoBean 어노테이션에 관해서 열어두기로 했습니다.
Spring에서 Entity는 Proxy화 시킬수 있어야하기 때문에 해당 Entity 객체를 상속할 수 있어야하며, 그리고 noArgsConstructor가 존재해야합니다.
- allOpen 플러그인을 통해서 data class를 open class로 자동으로 변경시켜준다. (data class는 기본적으로 final class로 취급하기 때문이다)
- noArgsConstructor를 통해 파라미터가 존재하지 않는 생성자를 자동 생성하게한다.
그리고 저는 config server에서 인증 정보를 가져와야하기 때문에 spring-cloud-starter-config를 implementation한 모습을 확인할 수 있습니다.
그러면 이제 domain-dynamo 모듈의 구현을 살펴보겠습니다!
우선 저는 본 프로젝트에서는 개발용 로컬 환경 과 배포용 서버 환경 이 따로 존재합니다. 따라서 application.yml을 여러개 두어서 관리를 하여, 개발용 환경과 서버 운영용 환경을 따로 정의해야합니다.
따라서 application-localdynamo.yml, application-serverdynamo.yml, application.yml을 따로 작성하겠습니다.
spring:
config:
activate:
on-profile: localdynamo
import: "optional:configserver:http://localhost:9500/"
cloud:
config:
name: nosql
profile: dynamo
encrypt:
key: my-encrypt-key
application-localdynamo.yml에서는 activate profile을 localdynamo로 설정하였고, config server와 연결된 repository에서 nosql-dynamo.yml으로부터 환경설정 정보를 가져와서 사용하겠다는 의미로 받아들이시면 될 것 같습니다.
encrypt.key의 경우, 본인의 config-server가 암호화된 환경설정 정보를 반환한다면, 이를 디코딩할 때 사용하는 대칭키를 적어주시면 되겠습니다.
다음으로, application-serverdynamo.yml을 작성하겠습니다.
spring:
config:
activate:
on-profile: serverdynamo
import: "optional:configserver:http://[my server ip]:9500/"
cloud:
config:
name: nosql
profile: dynamo
encrypt:
key: my-encrypt-key
위의 application-localdynamo.yml과 크게 다르지 않습니다. 다만 서버 uri가 달라진 모습인데, 저는 private subnet 상에 config-server를 두어서 운영을 하기 때문에 저의 server uri를 기입했습니다.
spring:
profiles:
active: localdynamo
정말 간단합니다. 개발 환경에 따라서 그 때마다 active시킬 profile을 바꿔주면됩니다.
저는 아직 local에서 작업을 진행하기 때문에 localdynamo profile을 activate 시킨 모습을 확인할 수 있습니다.
다음으로, DynamoDbConfig를 작성합시다.
DynamoDbConfig는 DynamoDB를 코틀린 코드로 제어하기 위해 필요한 Configuration class입니다. 여기서 EnhancedClient를 Bean으로 등록시켜 관리할 예정입니다.
코드부터 보시겠습니다!
@Configuration
class DynamoDbConfig(
@Value("\${aws.dynamodb.credentials.access-key}")
val accessKey: String,
@Value("\${aws.dynamodb.credentials.secret-key}")
val secretKey: String
) {
// dynamoDB Client Bean 생성
@Bean
fun dynamoDbClient(): DynamoDbClient = DynamoDbClient.builder()
.region(Region.AP_NORTHEAST_2)
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)
)
)
.build()
// dynamoDB Enhanced Client 생성
@Bean
fun dynamoDbEnhancedClient(
@Qualifier("dynamoDbClient") dynamoDbClient: DynamoDbClient
): DynamoDbEnhancedClient = DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDbClient)
.build()
}
우선 생성자 파라미터에 accessKey, secretKey를 Value 어노테이션을 통해 컴파일 시점에서 생성자 주입 을 이용해서 DI를 하도록 하겠습니다. 이는 컴파일 시점에서 config-server와 연결이 되어 config-server로부터 해당 정보들을 가져오게됩니다.
그리고 dynamoDbClient, dynamoDbEnhancedClient를 각각 작성하여 Bean으로 등록을 해주면 되는데요, 각자의 환경에 맞게 작성을 하면 되겠습니다. 저는 DynamoDB를 ap-Northeast-2 region(흔히, 서울 리전이라 부르죠! 특별한 사연이 없다면 다들 여기서 운용하실겁니다) 에서 운용을 하고있기 때문에 거기에 맞게 작성을 해주었습니다.
다음으로, Shop entity를 작성하고, repository를 작성하겠습니다.
우선, Shop class부터 확인하겠습니다
@DynamoDbBean
data class Shop(
@get:DynamoDbPartitionKey
@get:DynamoDbAttribute("shop_id")
var shopId: String = UUID.randomUUID().toString(),
@get:DynamoDbSortKey
@get:DynamoDbAttribute("shop_name")
var shopName: String = "",
@get:DynamoDbAttribute("is_open")
var isOpen: Boolean,
@get:DynamoDbAttribute("open_time")
var openTime: LocalDateTime,
@get:DynamoDbAttribute("close_time")
var closeTime: LocalDateTime,
@get:DynamoDbAttribute("lot_number_address")
var lotNumberAddress: String,
@get:DynamoDbAttribute("road_name_address")
var roadNameAddress: String,
@get:DynamoDbAttribute("latitude")
var latitude: Double,
@get:DynamoDbAttribute("longitude")
var longitude: Double,
@get:DynamoDbAttribute("created_at")
var createdAt: LocalDateTime = LocalDateTime.now(),
@get:DynamoDbAttribute("updated_at")
var updatedAt: LocalDateTime?,
@get:DynamoDbAttribute("average_score")
var averageScore: Double,
@get:DynamoDbAttribute("review_number")
var reviewNumber: Int
): Serializable
저는 shop_id라는 파티션키를 고유하게 유지하기 위해서 UUID를 이용해 랜덤키를 발급받도록 하였습니다.
그리고 shopRepository를 확인해보겠습니다.
@Repository
class ShopRepository(
private val dynamoDbEnhancedClient: DynamoDbEnhancedClient,
) {
// table 변수 선언
val table: DynamoDbTable<Shop> = dynamoDbEnhancedClient.table("shop", TableSchema.fromBean(Shop::class.java))
// shop을 파라미터로 받아서 table에 shop을 등록시킨 후, 등록된 shop을 그대로 리턴하는 메소드
fun createShop(shop: Shop): Shop {
table.putItem(shop)
return shop
}
// PartitionKey와 SortKey를 이용해서 Item을 가져오는 메소드
fun findShopByIdAndName(shopId: String, shopName: String): Shop? {
val shopKey = generateKey(shopId, shopName)
return table.getItem(shopKey) ?: null
}
// shopId, shopName을 받아서 해당 key에 해당하는 item을 삭제시키는 메소드
fun deleteShop(shopId: String, shopName: String): Unit {
val shopKey = generateKey(shopId, shopName)
table.deleteItem(shopKey)
}
// shop table 상에 존재하는 모든 데이터를 가져오는 메소드
fun findAllShop(): List<Shop> = table.scan().items().toList()
private fun generateKey(shopId: String, shopName: String): Key = Key.builder()
.partitionValue(shopId)
.sortValue(shopName)
.build()
}
저는 우선 repository를 여기까지만 작성을 해두었는데요, DynamoDB는 Transaction 작업 또한 지원을 하기 때문에, 나중에 이에 대한 내용은 추가하도록 하겠습니다. 아직 트랜잭션으로 뭘 작업해야할지 정의가 안되서 안한거는 안 비밀...
위 코드는 공식문서를 확인해보시면서 비교를 하면 크게 어려운 코드가 아니기 때문에, 이 부분에 대한 설명은 생략하고 여러분들의 몫에 맡기도록 하겠습니다!
AWS DynamoDB Java 공식문서(테이블 예제)
AWS DynamoDB Transactions 예제 <- 나중에 이거 보고 추가 작성할 예정이에요
우아한 멀티모듈 <- 이거 보고 그 자리에서 눈물 흘렸습니다
우아한 테크블로그 - 우아한멀티모듈 <- 멀티모듈 설계하는데 여기서 큰 영감을 얻었습니다. 이거랑 흐름을 비슷하게 가져갈 예정이에요