본문 바로가기

개발 이야기/Spring Boot

[10분 Boot-up] Kotlin으로 스프링부트 Web Application 만들기 - (5) Spring Data 적용하기

Kotiln과 Spring Boot을 모르더라도 10분씩 따라하면서 자연스럽게 Web Application을 만들어보는 것이 목표입니다.
각 챕터에서 나오는 중요한 내용은 별도로 [더 알아보기]에 보충해서 작성할 예정입니다.

시작하기

대부분의 Web Application은 데이터베이스(DB)에 저장된 데이터의 CRUD를 함께 수행하면서 비지니스 로직을 처리하는 경우가 많다. 이런 데이터베이스 저장소(MySQL, Redis, MongoDB 등)의 종류가 매우 다양하기 때문에, 만약 특정 저장소에 강합 결합도를 가진 Web Application이 있다면 상황에 따라서 언제든지 변경될 수 있는 Storage Layer의 변경이 매우 어려워진다.

이런 이슈를 해소하기 위해서 Spring은 Storage Layer의 특성을 유지하면서 친숙하고 일관된 Repository라는 Generic한 Java 인터페이스를 제공하는데, 바로 Spring Data이다. Spring Data는 Storage Layer의 공통된 연산에 대한 구현을 동적으로 제공하여 결합도를 느슨하게 만들어주고 언제든지 Storage Layer의 변경을 쉽게 만들어준다.

데이터베이스 개발환경 구성하기

먼저 MySQL을 데이터베이스로 사용한다고 가정하고 프로젝트를 진행하기 전에 local 환경에 MySQL을 구성한다. MySQL을 직접 local에 설치하는 방법도 있지만, Docker 사용 환경을 갖췄다면 Docker 컨테이너로 구성하는 방법이 쉽고 빠르며 더 좋은 방법이다. 그 이유는 MySQL 뿐만 아니라 Redis나 Kafka 등 프로젝트를 구성하는 외부 시스템 요소가 언제든지 늘어나거나 없어질 수 있기 때문이다. 그때마다 local 환경에 해당 시스템을 설치하고 연동하고 삭제하는 일은 너무 번거로운 일이다.

MySQL 하나로 구성된 시스템 환경을 위해서 Docker 설정 전에 DB 스키마 생성을 위한 SQL 파일을 프로젝트 ./src/main/resources/sql/init-scheme.sql 경로에 생성하고 아래처럼 간단한 Table 생성 쿼리를 작성해둔다.

create table if not exists stocks
(
    id           VARCHAR(10) PRIMARY KEY,
    type         VARCHAR(20),
    name         VARCHAR(100),
    code         VARCHAR(20),
    ticker       VARCHAR(20),
    updated_date DATETIME,
    created_date DATETIME,
    index idx_name (name),
    index idx_code (code),
    index idx_ticker (ticker),
);

그리고 마찬가지로 프로젝트 root 경로에 ./docker-compose.yml 파일을 생성한다. 그리고 docker compose로 mysql-local 컨테이너가 생성되면서 자동으로 DB 스키마 정보까지 한번에 적용이 될 수 있도록 volumes: 프로퍼티를 추가하고 프로젝트의 ./src/main/resources/sql 경로를 설정해준다.

version: "3.4"

x-common-mysql-env: &common-mysql-env
  MYSQL_ROOT_PASSWORD: my-root-pwd
  MYSQL_USER: my-user
  MYSQL_PASSWORD: my-user-pwd

services:
  mysql:
    container_name: mysql-local
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: myapp_db
      <<: *common-mysql-env
    volumes:
      - ./src/main/resources/sql:/docker-entrypoint-initdb.d/
    ports:
      - 3306:3306
    command: --default-authentication-plugin=mysql_native_password

터미널을 열고 프로젝트 root 경로에서 $docker-compose up 명령어를 실행하여 Docker 컨테이너가 잘 동작하는지 확인한다. 아래처럼 ready for connections 로그가 보이면 MySQL 컨테이너가 정상적으로 실행된 것이다.

Spring Data 사용하기

MySQL의 스키마 관계(Relation)을 Java 객체로서 자연스럽게 매핑하여 사용하기 위해서 JPA를 사용한다. JPA(Java Persistence API)는 ORM(Object Relation Mapping)을 위한 Java (EE) 표준으로 하이버네이트(Hibernate) 구현체이다. Spring Data에서는 이런 JPA를 지원하기 위한 추상화 Layer로 Spring Data JPA를 제공하고 있다.

Spring Data JPA를 사용하기 위해서 가장 중요한 dependency인 spring-boot-starter-data-jpa를 추가한다. 그리고 Kotlin 환경에서 JPA를 사용하기 때문에 하이버네이트(Hibernate)이 가진 특징을 맞춰주기 위한 kotlin-allopen, kotlin-noarg도 dependency에 추가해주고 plugins 설정을 한다. 마지막으로 우리처럼 local 환경에서 Docker 컨테이너의 MySQL을 연동하기 위해서라면 mysql-connector-java도 추가한다.

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

plugins {
    id("org.springframework.boot") version "2.4.2"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.4.21"
    kotlin("plugin.spring") version "1.4.21"
    kotlin("plugin.jpa") version "1.4.21"
    kotlin("plugin.allopen") version "1.4.21"
    kotlin("plugin.noarg") version "1.4.21"
}

group = "me.sample"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {
    mavenCentral()
}

dependencies {
    implementation("io.github.microutils:kotlin-logging:1.12.5") // Logging
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlin:kotlin-allopen")
    implementation("org.jetbrains.kotlin:kotlin-noarg")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")

    runtimeOnly("mysql:mysql-connector-java") // MySQL

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

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

tasks.withType<Test> {
    useJUnitPlatform()
}

Gradle 설정을 모두 마치면 IDE의 gradle plugin re-load를 통해 아래처럼 spring-boot-starter-data-jpa와 관련된 dependency들의 목록을 새롭게 확인할 수 있다.

이제 Docker 컨테이너의 데이터베이스 접속 정보를 프로퍼티 파일에 작성한다. spring.datasource.url, spring.datasource.username, spring.datasource.password 3개를 기본으로 등록해주고 JPA 관련 기본 설정도 아래처럼 설정한다. 그리고 앞에서 테스트 목적으로 등록했었던 불필요한 my-app 프로퍼티 설정을 지우자.

spring:
  config:
    activate:
      on-profile: local
  datasource:
    url: "jdbc:mysql://localhost:3306/myapp_db"
    username: "my-user"
    password: "my-user-pwd"
  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: true
    show-sql: true

---

spring:
  config:
    activate:
      on-profile: dev
    datasource:
      url: "jdbc:mysql://somewhere-dev-host:3306/myapp_db"
      username: "username"
      password: "password"
    jpa:
      hibernate:
        ddl-auto: update
      generate-ddl: true
      show-sql: true

MyappApplication.kt 파일에서도 관련 코드를 지워준다.

package me.sample.myapp

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class MyappApplication

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

Repository 생성하기

다음으로는 실제 Spring Data의 Repository interface를 선언해준다. 기본적인 CRUD만 필요하다는 전제아래 아래처럼 간단하게 repository 패키지를 추가하고 StockRepository.kt를 생성한다.

package me.sample.myapp.repository

import me.sample.myapp.domain.Stock
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface StockRepository : JpaRepository<Stock, String>

Entity 생성하기

실제 데이터베이스의 데이터를 매핑할 Entity 객체를 위해서 entity 패키지를 추가하고 Stock.kt를 생성한다.

package me.sample.myapp.domain

import java.time.LocalDateTime
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table

@Entity
@Table(name = "stocks")
class Stock(
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    val id: String,
    val type: String,
    val name: String,
    val code: String,
    val ticker: String,
    val updatedDate: LocalDateTime,
    val createdDate: LocalDateTime,
)

Controller 생성하기

기존 테스트를 위해 작성했던 TestController를 삭제하고 StockController를 생성한다. 그리고 실제 비지니스 로직을 처리하는 Service Layer를 만들기 전에 Repository를 Controller에서 바로 주입받아서 동작 테스트를 진행해보자. RequestMapping을 위한 간단한 함수를 생성하고 Web Application을 실행해보자.

package me.sample.myapp.controller

import me.sample.myapp.domain.Stock
import me.sample.myapp.repository.StockRepository
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class StockController(
    private val stockRepository: StockRepository
) {

    @GetMapping("/stocks")
    fun listStocks(): List<Stock> {
        return stockRepository.findAll()
    }
}

Console 로그를 확인하면 Web Application이 잘 동작하고 우리가 만든 GET /stocks 요청을 받으면 Hibernate query문이 생성되는 것을 확인할 수 있다. 물론 결과는 현재 데이터베이스에 어떤 데이터도 존재하지 않으므로 Empty array를 결과값으로 응답한다.

마무리

Spring Data를 적용하고 @Entity와 @Repository를 사용해서 MySQL 데이터베이스와 연동해봤다. 다음에는 실제 비지니스 로직을 처리하는 Service Layer를 생성하고 Business Model을 생성하여 실제 Entity와 독립적으로 사용하도록 변경해보자.

더 알아보기

  • ORM이란?
  • Hibernate 란 무엇일까? 간단한 사용법은?
  • Kotlin으로 Spring Data JPA를 사용할 때 주의해야하는 점