[Spring] Kotlin + Spring boot 에 MongoDB을 도입(Spring Data MongoDB, Querydsl)

앞의 두글(개념 및 설치, 기본적인 사용법)에서 MongoDB에 대해 간단하게 알아가는 시간이 였다면 이 글에서는 Spring boot에 적용에 있어서 설정과 사용법 그리고 주의해야 할 점들에 대하여 알아보자.

프로젝트 설정

MongoDB를 사용하려면 프로젝트 설정 build.gradle.kts에서 아래와 같이 설정해 줘야 한다.

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

plugins {
    id("org.springframework.boot") version "2.5.6"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.5.31"
    kotlin("kapt") version "1.5.31"
    kotlin("plugin.spring") version "1.5.31"
}

group = "com.techfox"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}
allOpen {
    annotation("org.springframework.data.mongodb.core.mapping.Document")
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.MappedSuperclass")
    annotation("javax.persistence.Embeddable")
}

dependencies {
    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("com.querydsl:querydsl-jpa:4.4.0")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
    implementation("com.querydsl:querydsl-mongodb:4.4.0")
    kapt("com.querydsl:querydsl-apt:4.4.0:jpa")
    kapt("org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final")

}

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

tasks.withType<Test> {
    useJUnitPlatform()
}

kapt {
    correctErrorTypes = true
}

이글에서는 MongoDB를 사용하기 위하여 Spring Data Mongodb 뿐만 아니라 쉽게 query로 MongoDB를 다룰 수 있도록 Querydsl도 함께 도입 했다.

Mongo Config

MongoDB 접근을 위해서는 본 블로그에서 PostgreSQL를 연결하는 글에서와 같이 application.yml 파일에 명시하는것도 괜찮지만 이 글에서는 연결 설정을 코드 내에서 bean을 생성하는 방식으로 할 예정이다.

package com.techfox.order.config

import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.MongoDatabaseFactory
import org.springframework.data.mongodb.MongoTransactionManager
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories

@Configuration
@EnableMongoRepositories(basePackages = ["com.techfox.order"])
class MongoConfig : AbstractMongoClientConfiguration() {

    override fun getDatabaseName(): String {

        return "test"
    }

    override fun mongoClient(): MongoClient {

        val connectionString = ConnectionString("mongodb://monggo:1234@localhost:27017/test")
        val mongoClientSettings = MongoClientSettings
            .builder()
            .applyConnectionString(connectionString)
            .build()

        return MongoClients.create(mongoClientSettings)
    }
}

위에서 연결정보를 connectionString에 명시하면 된다. 만약 배포환경에서 해당 내용을 가져 오려면 이글을 참조하여 작성하면 된다,

마지막으로 Spring Data MongoDB를 활성화 하려면 Application class에 아래처럼 추가하면 된다.(추가하지 않으면 Datasoure URL를 찾지 못하는 에러가 발생한다.)

@SpringBootApplication(exclude = [DataSourceAutoConfiguration::class])
class OrderApplication

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

Entity 작성

여기서 사용되는 예제는 간이 프로젝트 음식점 주문 및 결산 시스템 backend의 일부분이다. 전체 코드는 Git 저장소를 참고하면 될것 같다.(주문 Entity로 본 글에서 사용될 내용을 적을 예정이다. 다른 부분에 대한 설계와 구현은 이 블로그의 간이 프로젝트 카테고리를 참고 하면 좋을것 같다)

order/src/main/kotlin/com/techfox/order/model/Order.kt

package com.techfox.order.model
import com.querydsl.core.annotations.QueryEntity
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.core.mapping.Field
import java.time.LocalDateTime
//import javax.persistence.ElementCollection
import javax.persistence.Entity
import javax.persistence.Id

@Entity
@QueryEntity
@Document(collection = "orders")
class Order {
    @Id
    var id: String? = null

//    @Field(name="transcationId")
//    var transcationId: String =""

    @Field(name="order_date")
    var orderDate: LocalDateTime = LocalDateTime.now()

    @Field(name="extendedPrice")
    var extendedPrice: Int = 0

    @Field(name="table")
    var table:String = ""

    @Field(name="menus")
    var items: List<MenuItem> = mutableListOf()

    @Field(name="status")
    var status: Int = 0
}

@Entity
@QueryEntity
class MenuItem {

    var category: String =""

    var menuId: String = ""

    var title: String = ""

    var price:Int = 0

    var count:Int = 0
}

여기서 중요한 필드는 items라는 필드 인것 같다. 이 필드는 주문 메뉴와 수량을 기록하는 필드로써 Nested Fied로 작성 했다.

실제 저장된 데이터를 보면 아래와 같다.

[
    {
        "id": "618f9020c67ea06af3a435d4",
        "orderDate": "2021-11-13T19:14:56.737",
        "extendedPrice": 72000,
        "table": "618f6d568bf5f67ff80124d9",
        "items": [
            {
                "category": "food",
                "menuId": "",
                "title": "양꼬치",
                "price": 12000,
                "count": 6
            }
        ],
        "status": 1
    }
]

Repository 작성

기본적인 CRUD를 수행하기 위해서는 MongoRepository로부터 상송 받아 interface를 구현 해야 한다. 추후 자유로운 query 수행을 위해 QuerydslPredicateExecutor으로부터도 상속을 받는다.

package com.techfox.order.repository

import com.techfox.order.model.Order
import com.techfox.order.model.QOrder.order
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.data.querydsl.QuerydslPredicateExecutor
import org.springframework.stereotype.Repository

@Repository
interface OrderRepository: MongoRepository<Order,String>, QuerydslPredicateExecutor<Order>

이처럼 상속받으면 추후 Predicate를 이용한 query수행을 자유롭게 할 수 있다.

Order Controller 생성

현재 작성하는 이글과는 약간 성격이 다르지만 RestAPI를 이용한 backend 개발을 위해서는 호출을 처리해 줄 곳이 있어야 된다고 생각하여 작성해 보았다.

package com.techfox.order.Controller

import com.techfox.order.model.MenuItem
import com.techfox.order.model.Order
import com.techfox.order.repository.OrderRepository
import com.techfox.order.service.OrderService
import org.springframework.web.bind.annotation.*

@RestController
class OrderController(
    var orderService: OrderService,
    var orderRepository: OrderRepository
) {

    @PostMapping("/store/name/table/{table_id}/order")
    fun order_menu(@PathVariable table_id : String, @RequestBody item: MenuItem){
        orderService.orderMenu(table_id,item)
    }
    @GetMapping("/store/name/orders")
    fun get_all_order():List<Order>{
        return orderRepository.findAll()
    }

    @DeleteMapping("/store/name/orders/all")
    fun delete_all(){
        orderRepository.deleteAll()
    }
}

기본적으로 CRUD에 대하여 구현 하였다. 하나하나 살펴보면 아래와 같다.

  • GetMapping : 현재 모든 주문을 가져오는 End Point이다. Spring Data MongDB에서 제공하는 것을 그대로 사용했다.
  • DeleteMapping: 위와 마찬가지로 제공하는 Method를 사용 했다.
  • PostMapping: 주문을 받는 End Point이다. 여기서 제공하는 기능은 주문하는 메뉴를 RequestBody로 받고 그 메뉴가 기존 주문에 존재 여부를 판단하여 처리해주는 역할을 하는 OrderService를 만들어 주어 처리할 예정이다.

Order Service 작성

먼저 코드부터 보자.

package com.techfox.order.service

import com.techfox.order.model.MenuItem
import com.techfox.order.model.Order
import com.techfox.order.model.QOrder.order
import com.techfox.order.repository.OrderRepository
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.*
import org.springframework.data.mongodb.core.query.Query.query
import org.springframework.data.mongodb.core.updateFirst
import org.springframework.stereotype.Service

@Service
class OrderService(
    var orderRepository: OrderRepository,
    val mongoTemplate: MongoTemplate
){
    fun orderMenu(tableId:String, menuItem: MenuItem){
        //현재 주문에 들어간 테이블이 있는지 체크
        var exOrder = orderRepository.findOne(
            order.status.eq(1).and(order.table.eq(tableId))
        )
        //없으면 첫 주문, 추후 list로 주문을 받는 형식으로 수정 예정
        if(exOrder.isEmpty){
            var tableOrder = Order()
            tableOrder.status = 1
            tableOrder.items = mutableListOf(menuItem)
            tableOrder.table = tableId
            tableOrder.extendedPrice = menuItem.price*menuItem.count
            orderRepository.save(tableOrder)
        }
        else
        {
            var tableOrder = exOrder.get()
            var found : Boolean = false
            for(item in tableOrder.items){
                if(item.menuId==menuItem.menuId){
                    //수량은 추가 주문 수량이 들어옴, 수량 수정 및 최종 가격 sum
                    item.count += menuItem.count
                    tableOrder.extendedPrice +=menuItem.price * menuItem.count
                    //저장
                    orderRepository.save(tableOrder)
                    found=true
                }
            }
            if(found==false){
                //새 메뉴 추가
                var extendedPrice = tableOrder.extendedPrice + menuItem.price*menuItem.count
                val updatedCount = mongoTemplate.updateFirst<Order>(
                    query(
                        where(Order::table).`is`(table_id)
                    ),
                    Update()
                        .set("extendedPrice",extendedPrice)
                        .addToSet("items",menuItem)
                ).modifiedCount         
            }
        }
    }
}

메뉴 주문에 대한 로직을 처리하는 부분이다.

  • 시작 코드는 Querydsl을 이용하여 조건에 맞는(기존 주문이 존재하는지) 판단하고 없으면 생성하여 주문을 넣는 코드
  • 만약 주문이 존재한다면 추가 주문으로 들어온 메뉴가 존재하는지 검색, 존재하면 수량만과 총 금액을 업데이트 하고 MongoDB에 save한다.
  • 만약 추가된 메뉴가 새 메뉴라면 MongoTemplate의 updateFirst를 이용하여 해당 내용을 추가하면 된다.
  • Update항목에 Order::items를 넣어도 되지만 내 경우 넣으면 타입를 가져오는 오류가 있어 key 값을 직접 넣으니 문제를 해결하였다.

여기까지 간단하게 Spring boot에서 MongoDB를 연동하는 방식과 예제를 통해서 어떻게 사용되는지에 대하여 알아 보았다. 이 글 작성에 큰 도움이 되었던 지단로보트의 Spring Boot, Spring Data MongoDB, Querydsl로 타입 세이프 쿼리 작성하기 도 참고하면 좋을것 같다.

본 간이 프로젝트의 전체 구성과 앞으로의 발전 방향을 다른 글로 정리할 예정이고 코드는 업데이트 되는대로 Github 저장소에서 만나볼 수 있다.

ref:

https://jsonobject.tistory.com/

 

Software Engineer, Java, Spring Boot, JAX-RS REST API, OAuth 2.0, Microservice, DevOps

미니멀리스트 Senior Software Engineer Java, Spring Boot, JAX-RS REST API, OAuth 2.0 Microservice, DevOps

jsonobject.tistory.com

https://docs.spring.io/spring-data/mongodb/docs/

 

Index of /spring-data/mongodb/docs

 

docs.spring.io