Home
Backend Engineering
Kotlin과 Spring을 사용한 Server에 여러개의 DataSource를 사용하자
울이
울이
July 02, 2022
1 min

Table Of Contents

01
만들어 볼 프로젝트
02
프로젝트 초기 설정
03
Configuration 조작하기 - Bean으로 직접 설정하기
04
Users 패키지 개발 - Controller, Service, Repository, Domain
05
Device 패키지 개발 - Controller, Service, Repository, Domain
06
IoTLog 패키지 개발 - Controller, Service, Repository, Domain
07
확인하기
  • 하나의 Server에서 하나의 DB만 사용하면 너무 편하겠지만 예외적인 상황들이 있을 수 있다
  • 중앙에서 관리하는 Server(Route역할)가 앞에서 filter를 거쳐, 뒤에 이어진 다양한 Server들을 이어주는 경우가 아니라면 여러개의 DataSource를 설정하는 고민을 해 보았을 것 이다

만들어 볼 프로젝트

  • Home IoT를 관리하는 서버를 만들자
  • 도메인은 IoTLog, Device, Users
  • 서로 매핑되면 좋은 프로젝트가 되겠지만, 엔티티의 매핑보다 DataSource를 다양하게 설정하는데에 주목하자

개발언어 및 사용스택

  • Kotlin
  • SpringBoot
  • MariaDB
  • PostgreSQL
  • Mongodb

프로젝트 초기 설정

  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • spring-boot-starter-data-mongodb
  • postgresql
  • mariadb
  • lombok

build.gradle.kts

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

plugins {
    id("org.springframework.boot") version "2.7.3"
    id("io.spring.dependency-management") version "1.0.13.RELEASE"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.jpa") version "1.6.21"
}

group = "wool"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

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")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
    compileOnly("org.projectlombok:lombok")
    runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
    runtimeOnly("org.postgresql:postgresql")
    annotationProcessor("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

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

tasks.withType<Test> {
    useJUnitPlatform()
}
  • dependency를 설정한 gradle파일이다

Configuration 조작하기 - Bean으로 직접 설정하기

  • application.properties는 설정을 자동으로 해 주기 때문에 기본적인 설정을 입력하면 자동으로 세팅을 완료 해준다
  • 우리가 만들려고 하는 서버에서는 기본적인 설정 외에 추가적인 설정을 필요로 하기때문에 Bean으로 직접 생성 해 주려고 한다

application.properties

# Maria DB - "db1"
users-data.datasource.url=jdbc:mariadb://localhost:3306/paul?characterEncoding=UTF-8&serverTimezone=UTC
users-data.datasource.username=paul
users-data.datasource.password=qwerqwer123
users-data.datasource.driver-class-name=org.mariadb.jdbc.Driver
users-data.datasource.hikari.connection-test-query=SELECT 1


# PostgreSQL DB - "db2"
device-data.datasource.url=jdbc:postgresql://localhost:5432/paul
device-data.datasource.username=paul
device-data.datasource.password=qwerqwer123
device-data.datasource.driver-class-name=org.postgresql.Driver
device-data.datasource.hikari.connection-test-query=SELECT 1

# MongoDB
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.username=paul
spring.data.mongodb.password=qwerqwer123
spring.data.mongodb.database=paul
spring.data.mongodb.authentication-database=admin

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

# logging
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
logging.level.org.hibernate.SQL=debug

server.port=8000
  • Maria DB, PostgreSQL DB, Mongo DB 를 따로 설정 해 주기 위해 따로 설정값들을 나열했다
  • Postgresql은 사용자 데이터(Users), Maria DB은 Device 관리 테이블(Device), Mongo DB는 Device가 사용된 기록을 raw로 남긴다
  • 사실 우리가 만들 코드에서는 MongoDB 설정값이 크게 필요 없다.
    • 이부분은 차후에 properties 파일에서 설정값을 읽어오는 방향으로 코드를 수정하면 된다

Configuration Bean 생성하기 1 - Postgresql DB 세팅

  • config 패키기를 생성하고, 하위에 PostgresqlConfing 파일을 생성한다
package wool.multidb.config

import com.zaxxer.hikari.HikariDataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.orm.jpa.JpaTransactionManager
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.persistence.EntityManagerFactory
import javax.sql.DataSource

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    entityManagerFactoryRef = "deviceEntityManagerFactory",
    basePackages = ["wool.multidb.device.repository"]
)
class PostgresqlConfig {

    @Primary
    @Bean(name = ["deviceDataSourceProperties"])
    @ConfigurationProperties("device-data.datasource")
    fun deviceDataSourceProperties(): DataSourceProperties {
        return DataSourceProperties()
    }


    @Primary
    @Bean(name = ["deviceDataSource"])
    @ConfigurationProperties("device-data.datasource.configuration")
    fun dataSource(@Qualifier("deviceDataSourceProperties") deviceDataSourceProperties: DataSourceProperties): DataSource {
        return deviceDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java).build()
    }

    @Primary
    @Bean(name = ["deviceEntityManagerFactory"])
    fun entityManagerFactory(
        builder: EntityManagerFactoryBuilder,
        @Qualifier("deviceDataSource") deviceDataSource: DataSource
    ): LocalContainerEntityManagerFactoryBean {
        return builder
            .dataSource(deviceDataSource)
            .packages("wool.multidb.device.domain")
            .persistenceUnit("device")
            .build()
    }

    @Primary
    @Bean(name = ["deviceTransactionManager"])
    fun transactionManager(
        @Qualifier("deviceEntityManagerFactory") deviceEntityManagerFactory: EntityManagerFactory
    ): PlatformTransactionManager{
        return JpaTransactionManager(deviceEntityManagerFactory)
    }
}

Configuration Bean 생성하기 2 - Maria DB 세팅

  • config 패키기를 생성하고, 하위에 MariaConfig 파일을 생성한다
package wool.multidb.config

import com.zaxxer.hikari.HikariDataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.orm.jpa.JpaTransactionManager
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.persistence.EntityManagerFactory
import javax.sql.DataSource


@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    entityManagerFactoryRef = "entityManagerFactory",
    basePackages = ["wool.multidb.users.repository"]
)
class MariaConfig {
    @Bean(name = ["dataSourceProperties"])
    @ConfigurationProperties("users-data.datasource")
    fun usersDataSourceProperties(): DataSourceProperties {
        return DataSourceProperties()
    }

    @Bean(name = ["dataSource"])
    @ConfigurationProperties("users-data.datasource.configuration")
    fun dataSource(@Qualifier("dataSourceProperties") usersDataSourceProperties: DataSourceProperties): DataSource {
        return usersDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource::class.java)
            .build()
    }

    @Bean(name = ["entityManagerFactory"])
    fun entityManagerFactory(
        builder: EntityManagerFactoryBuilder, @Qualifier("dataSource") dataSource: DataSource
    ): LocalContainerEntityManagerFactoryBean {
        return builder
            .dataSource(dataSource)
            .packages("wool.multidb.users.domain")
            .persistenceUnit("users")
            .build()
    }

    @Bean(name = ["transactionManager"])
    fun transactionManager(
        @Qualifier("entityManagerFactory") usersEntityManagerFactory: EntityManagerFactory
    ): PlatformTransactionManager {
        return JpaTransactionManager(usersEntityManagerFactory)
    }
}

Configuration Bean 생성하기 3 - Mongo DB 세팅

  • config 패키기를 생성하고, 하위에 MongoConfig 파일을 생성한다
package wool.multidb.config

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


@Configuration
@EnableMongoRepositories(basePackages = ["wool.multidb.iotlog.repository"])
class MongoConfig : AbstractMongoClientConfiguration() {

    override fun getDatabaseName(): String {
        return "iotlog"
    }

    override fun mongoClient(): MongoClient {
        val connectionString = ConnectionString("mongodb://paul:qwerqwer123@localhost:27017/paul?authSource=admin")

        val mongoClientSettings = MongoClientSettings
            .builder()
            .applyConnectionString(connectionString)
            .build()
        return MongoClients.create(mongoClientSettings)
    }

}
  • 여기에서 사용한 ConnectionString에서는 user,password, admin DB 정보가 모두 들어가있다.
    • 차후에 고도화과정에서 해당 내용들을 properties로 빼거나, kube의 secret를 넣으면 된다

Users 패키지 개발 - Controller, Service, Repository, Domain

Domain - UsersEntity

package wool.multidb.users.domain

import lombok.Data
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table


@Entity
@Data
@Table(name = "users")
data class UsersEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int? = null,
    val userName: String,
    val email: String,
)

repository - UsersEntityRepository

package wool.multidb.users.repository

import org.springframework.data.jpa.repository.JpaRepository
import wool.multidb.users.domain.UsersEntity

interface UsersEntityRepository: JpaRepository<UsersEntity, Int> {
}

service - UsersService

package wool.multidb.users.service

import org.springframework.stereotype.Service
import wool.multidb.users.domain.UsersEntity
import wool.multidb.users.repository.UsersEntityRepository

@Service
class UsersService(
        private val usersEntityRepository: UsersEntityRepository
) {
    fun getUsers(): List<UsersEntity> = usersEntityRepository.findAll()
}

controller - UsersController

package wool.multidb.users.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import wool.multidb.users.domain.UsersEntity
import wool.multidb.users.service.UsersService


@RestController
class UsersController(
    private val usersService: UsersService
) {
    @GetMapping("/users")
    fun getAllUsers(): List<UsersEntity> {
        return usersService.getUsers()
    }
}

Device 패키지 개발 - Controller, Service, Repository, Domain

Domain - DeviceEntity

package wool.multidb.device.domain

import lombok.Data
import javax.persistence.*

@Entity
@Data
@Table(name = "device")
data class DeviceEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int? = null,
    var deviceName: String? = null,
    var deviceType: String? = null
)

repository - DeviceEntityRepository

package wool.multidb.device.repository

import org.springframework.data.jpa.repository.JpaRepository
import wool.multidb.device.domain.DeviceEntity

interface DeviceEntityRepository : JpaRepository<DeviceEntity, Int> {
}

service - DeviceService

package wool.multidb.device.service

import org.springframework.stereotype.Service
import wool.multidb.device.domain.DeviceEntity
import wool.multidb.device.repository.DeviceEntityRepository


@Service
class DeviceService(
    private val deviceEntityRepository: DeviceEntityRepository
) {
    fun getAllDevices(): MutableList<DeviceEntity> {
        return deviceEntityRepository.findAll()
    }
}

controller - DeviceController

package wool.multidb.device.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import wool.multidb.device.domain.DeviceEntity
import wool.multidb.device.service.DeviceService


@RestController
class DeviceController(
        val deviceService: DeviceService
) {
    @GetMapping("/devices")
    fun getAllDevices(): MutableList<DeviceEntity> {
        return deviceService.getAllDevices()
    }
}

IoTLog 패키지 개발 - Controller, Service, Repository, Domain

  • MongoDB 세팅이기떄문에 기존의 Repository의 JPARepository를 상속받기보다 MongoTemplate을 상속받을 것
  • IoT Device 중, 전구(Light) 데이터를 저장 해 보려고 한다

Domain - Light

package wool.multidb.iotlog.domain

import org.springframework.data.mongodb.core.mapping.Document
import javax.persistence.Entity
import javax.persistence.Id

@Document(collection = "iotLog")
data class Light(
    @Id
    var id: String? = null,
    var status: Boolean = false
)

repository - IoTRepository

package wool.multidb.iotlog.repository

import org.springframework.data.mongodb.repository.MongoRepository
import wool.multidb.iotlog.domain.Light

interface IoTRepository : MongoRepository<Light, String>{}

service - IoTService

package wool.multidb.iotlog.service

import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.stereotype.Service
import wool.multidb.iotlog.domain.Light
import wool.multidb.iotlog.repository.IoTRepository


@Service
class IoTService(
    val iotRepository: IoTRepository,
    val mongoTemplate: MongoTemplate
) {
    fun getAllLight(): List<Light> {
        return iotRepository.findAll()
    }

    fun putLightLog(light: Light) {
        mongoTemplate.save(light)
    }
}

controller - IoTController

package wool.multidb.iotlog.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import wool.multidb.iotlog.domain.Light
import wool.multidb.iotlog.service.IoTService


@RestController
class IoTController(
    private val ioTService: IoTService
) {

    @GetMapping("/light")
    fun getAllLight(): List<Light> {
        return ioTService.getAllLight()
    }

    @PostMapping("/light")
    fun addLightLog(@RequestBody light: Light) {
        return ioTService.putLightLog(light)
    }
}

몽고디비 or Document DB config 보안관련

  • 각 회사나 일부 프로젝트에서는 CA 파일을 설정하여 연결을 요구할 수 있다 그때에는 아래와 같은 코드로 SSLConfig 를 추가하면 된다
override fun getDatabaseName(): String {
        return "DB이름"
    }

override fun mongoClient(): MongoClient {

    val trustStorePath = "./auth"

    val keyStore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())

    keyStore.load(null)

    val certF: CertificateFactory = CertificateFactory.getInstance("X.509")

    val cert: Certificate = certF.generateCertificate(URL("https://s3.amazonaws.com/rds-downloads/rds-ca-2019-root.pem").openStream())
    keyStore.setCertificateEntry("mongo-cert", cert)

    FileOutputStream(trustStorePath).use { out -> keyStore.store(out, "pass".toCharArray()) }

    val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
    trustManagerFactory.init(keyStore)

    val sslContext = SSLContext.getInstance("TLS")
    sslContext.init(null, trustManagerFactory.getTrustManagers(), SecureRandom())

    val builder = MongoClientSettings.builder()
        .applyConnectionString(ConnectionString("mongodb-url"))
        .applyToSslSettings { b: SslSettings.Builder ->
            b.enabled(true)
            b.context(sslContext)
        }
        .build()

    return MongoClients.create(builder)
}

확인하기

  • 스프링부트 서버를 실행시키고, 잘 동작하는지 확인한다
  • application properties에 작성했었던 Configuration 값들을 config 패키지를 만들어 내부로 이동시켰다
  • controller에서 정했던 url로 각각 호출 해 보면 잘 되는 것을 볼 수 있다

Tags

#Java#Kotlin#Spring#SpringBoot#Spring-Kotlin#Database #Database Configuration#Database Connection#MongoDB#MySQL#PostgreSQL
Previous Article
SpringBoot 에서 JPA를 사용한 Repository 테스트
울이

울이

개발자 울이

경험하고 사용하고 개선하는 것, 그리고 소통하면서 성장하는 것을 좋아합니다.

Expertise

Backend
Frontend
Data Engineering

Social Media

githublinkedinwebsiteinstagram

Related Posts

비밀번호 유효성 검증기 - TDD 활용하기
August 15, 2022
1 min
© 2022, All Rights Reserved.

Quick Links

Home

Social Media