[위드마켓 개발기] 가게정보 관리 서버 설계 및 DynamoDB와 연결을 시켜보자

Doccimann·2022년 5월 21일
1

위드마켓 개발기

목록 보기
4/10
post-thumbnail

🌈 본격적인 작성 이전에

저는 본 서버를 멀티모듈을 기반 으로 해서 작성할 예정입니다. 따라서 이 글을 읽고도 저게 뭔 소리죠? 라는 기분이 드실수도 있습니다.

우선은, 멀티모듈로 설계를 하게된 이유에 대해서부터 설명을 드리도록 하겠습니다.


🤔 Why Multi-Module? (왜 멀티모듈로 설계해요?)

제가 프로젝트를 멀티모듈로 설계하려는 이유는 다음과 같습니다!

  • build.gradle.kts를 모듈별로 경량화시켜서, 각자의 역할과 책임에 맞게끔만 의존성을 가지게하자.
  • 역할과 책임을 모듈별로 의식적인 분리를 하여 스파게티 코드를 방지하자 (모듈별로 코드를 계층화 시켜서 모듈간의 Dependency가 Cycle이 벌어지지 않게한다)

그 외에도 여러가지 이유가 있겠지만, 그 외의 이유는 우아한 멀티모듈 이라는 영상을 보시는 것을 추천드리겠습니다! 저보다는 훨씬 설명을 잘하셔요...


🔥 본격적으로 멀티모듈 설계를 진행합시다.

일단 프로젝트를 하나 생성하셔서, 다음과 같이 작성을 해줍니다.

1️⃣ root project의 build.gradle.kts

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를 확인해보겠습니다.

2️⃣ root project의 settings.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를 확인해봅시다.

3️⃣ domain-dynamo module의 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 모듈의 구현을 살펴보겠습니다!


🔥 일단 인증 정보를 config server로부터 가져와서 DynamoDbConfig를 작성해봅시다

우선 저는 본 프로젝트에서는 개발용 로컬 환경배포용 서버 환경 이 따로 존재합니다. 따라서 application.yml을 여러개 두어서 관리를 하여, 개발용 환경과 서버 운영용 환경을 따로 정의해야합니다.

따라서 application-localdynamo.yml, application-serverdynamo.yml, application.yml을 따로 작성하겠습니다.

1️⃣ application-localdynamo.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을 작성하겠습니다.

2️⃣ 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를 기입했습니다.

3️⃣ application.yml을 작성합니다.

spring:
  profiles:
    active: localdynamo

정말 간단합니다. 개발 환경에 따라서 그 때마다 active시킬 profile을 바꿔주면됩니다.

저는 아직 local에서 작업을 진행하기 때문에 localdynamo profile을 activate 시킨 모습을 확인할 수 있습니다.

다음으로, DynamoDbConfig를 작성합시다.

4️⃣ DynamoDbConfig.kt를 작성합니다.

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, 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 작업 또한 지원을 하기 때문에, 나중에 이에 대한 내용은 추가하도록 하겠습니다. 아직 트랜잭션으로 뭘 작업해야할지 정의가 안되서 안한거는 안 비밀...

위 코드는 공식문서를 확인해보시면서 비교를 하면 크게 어려운 코드가 아니기 때문에, 이 부분에 대한 설명은 생략하고 여러분들의 몫에 맡기도록 하겠습니다!


🌲 References

AWS DynamoDB GitHub 예제코드

AWS DynamoDB Java 공식문서(테이블 예제)

AWS DynamoDB Transactions 예제 <- 나중에 이거 보고 추가 작성할 예정이에요

우아한 멀티모듈 <- 이거 보고 그 자리에서 눈물 흘렸습니다

우아한 테크블로그 - 우아한멀티모듈 <- 멀티모듈 설계하는데 여기서 큰 영감을 얻었습니다. 이거랑 흐름을 비슷하게 가져갈 예정이에요

profile
Hi There 🤗! I'm college student majoring Mathematics, and double majoring CSE. I'm just enjoying studying about good architectures of back-end system(applications) and how to operate the servers efficiently! 🔥

0개의 댓글