ABOUT ME

-

Total
-
  • Spring Boot: Kotlin + ScyllaDB 도커 시작
    컴퓨터/Spring Boot 2023. 12. 14. 21:34
    728x90
    반응형

    Datastax 드라이버로 Configuration을 만들고 간단한 도메인을 만들어서 저장하는 과정을 썼다.

    풀소스는 아래 github에서 참고.

     

    GitHub - Alfex4936/spring-boot-kotlin-scylladb-demo: Spring Boot written in Kotlin with two scyllaDB nodes in Docker

    Spring Boot written in Kotlin with two scyllaDB nodes in Docker - GitHub - Alfex4936/spring-boot-kotlin-scylladb-demo: Spring Boot written in Kotlin with two scyllaDB nodes in Docker

    github.com

     

    DataStax Documentation Home :: DataStax Documentation Home

    Find the details you need to create scalable, reliable databases & streaming solutions. Explore Astra DB serverless vector databases for Generative AI workloads, Astra Streaming, and on-prem DataStax Enterprise (DSE) with our developer guides, tutorials, a

    docs.datastax.com

     

    1. 소개

    일단 Docker랑 Kotlin + Spring boot 프로젝트를 준비한다.

    Discord가 mongodb에서 scyllaDB를 옮기면서 남긴 회고록이 생각나서 scyllaDB란 것을 써보고 싶었다.

    [/SILL-ah/ 라고 읽는다 (씔라)]

    일단 ScyllaDB는 Apache Cassandra 데이터베이스랑 compatible이어서 cassandra 설정을 그대로 써도 된다. (cql 도 동일)

    datastax 드라이버는 카산드라랑 scylla 최신 기능과 최적화가 좀 더 담겨져 있다고 들어서 주로 사용한다고 한다. (spring boot cassandra driver도 괜찮은 것 같은데 비교는 못 해봤다)

    scyllaDB 노드 2개를 만들고 spring boot 프로젝트도 같이 컨테이너화 하여서 실행할 것이다.

     

    2. Docker

    CPU가 힘들어서 노드 2개로 로컬에서 돌리고 있다. 더 추가하려면 계속 비슷하게 추가하면 된다.

    노드 설정을 어떻게 하는지 예제가 제대로 된 걸 찾기가 어려워서 꽤 헤매었다..

    그리고 드라이버 configuration이나 init.cql을 처음에 돌리고 싶었는데 이것도 힘들었다.

     

    build.gradle.kts

    import org.jetbrains.kotlin.gradle.plugin.mpp.SourceSetMetadataLayout.METADATA.archiveExtension
    import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
    import org.springframework.boot.gradle.tasks.bundling.BootJar
    
    plugins {
    	id("org.springframework.boot") version "3.2.1-SNAPSHOT"
    	id("io.spring.dependency-management") version "1.1.4"
    	kotlin("jvm") version "1.9.20"
    	kotlin("plugin.spring") version "1.9.20"
    }
    
    group = "csw.scylladb.jwt"
    version = "0.0.1-SNAPSHOT"
    
    java {
    	sourceCompatibility = JavaVersion.VERSION_17
    }
    
    val bootJar: BootJar by tasks
    
    bootJar.enabled = true
    bootJar.archiveFileName = "scylladb-${version}.${archiveExtension}"
    
    configurations {
    	compileOnly {
    		extendsFrom(configurations.annotationProcessor.get())
    	}
    }
    
    repositories {
    	mavenCentral()
    	maven { url = uri("https://repo.spring.io/milestone") }
    	maven { url = uri("https://repo.spring.io/snapshot") }
    }
    
    dependencies {
    	implementation("org.springframework.boot:spring-boot-starter-data-cassandra")
    //	implementation("org.springframework.boot:spring-boot-starter-data-cassandra-reactive")
    	implementation("org.springframework.boot:spring-boot-starter-security")
    	implementation("org.springframework.boot:spring-boot-starter-web")
    //	implementation("org.springframework.boot:spring-boot-starter-webflux")
    	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    
    	implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    	implementation("org.jetbrains.kotlin:kotlin-reflect")
    	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    
    	implementation("com.scylladb:java-driver-core:4.15.0.0")
    	implementation("com.scylladb:java-driver-query-builder:4.15.0.0")
    
    	compileOnly("org.projectlombok:lombok")
    	annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
    	annotationProcessor("org.projectlombok:lombok")
    	testImplementation("org.springframework.boot:spring-boot-starter-test")
    	testImplementation("io.projectreactor:reactor-test")
    	testImplementation("org.springframework.security:spring-security-test")
    }
    
    tasks.withType<KotlinCompile> {
    	kotlinOptions {
    		freeCompilerArgs += "-Xjsr305=strict"
    		jvmTarget = "17"
    	}
    }
    
    tasks.withType<Test> {
    	useJUnitPlatform()
    }

    도커 scylla.yml

    version: "3"
    
    services:
      spring-boot:
        container_name: scylladb-spring-boot
        build:
          context: ./build/libs
          dockerfile: Dockerfile.spring
        depends_on:
          scylla-node1:
            condition: service_started
          scylla-node2:
            condition: service_started
        ports:
          - "8082:8082"
        networks:
          web:
    
    
      scylla-node1:
        container_name: scylla-node1
        image: scylladb/scylla:5.4.0
        restart: unless-stopped
        command: --seeds=scylla-node1 --memory 1G --smp 1 --overprovisioned 1 --api-address 0.0.0.0
        volumes:
          - "./scylla/scylla.yaml:/etc/scylla/scylla.yaml"
          - "./scylla/cassandra-rackdc.properties.dc1:/etc/scylla/cassandra-rackdc.properties"
        ports:
          - "10000:10000"
          - "9042:9042"
          - "24:22"
          - "7000:7000"
          - "7001:7001"
          - "9180:9180"
          - "9160:9160"
        networks:
          web:
    
      scylla-node2:
        container_name: scylla-node2
        image: scylladb/scylla:5.4.0
        restart: unless-stopped
        command: --seeds=scylla-node1 --smp 1 --memory 750M --overprovisioned 1 --api-address 0.0.0.0
        volumes:
          - "./scylla/scylla.yaml:/etc/scylla/scylla.yaml"
          - "./scylla/cassandra-rackdc.properties.dc1:/etc/scylla/cassandra-rackdc.properties"
        ports:
          - "9043:9042"
        networks:
          web:
    
    #  scylla-node3:
    #    container_name: scylla-node3
    #    image: scylladb/scylla:5.4.0
    #    restart: unless-stopped
    #    command: --seeds=scylla-node1,scylla-node2 --smp 1 --memory 500M --overprovisioned 1 --api-address 0.0.0.0
    #    volumes:
    #      - "./scylla/scylla.yaml:/etc/scylla/scylla.yaml"
    #      - "./scylla/cassandra-rackdc.properties.dc1:/etc/scylla/cassandra-rackdc.properties"
    #    ports:
    #      - "9044:9042"
    #    networks:
    #      web:
    #    deploy:
    #      resources:
    #        limits:
    #          cpus: '0.5'
    
    networks:
      web:
        driver: bridge

     

    scylla.yaml 과 cassandra-rackdc.properties는

    scylla.zip
    0.01MB

    다운로드하여서 설정하거나 그대로 scylla 폴더를 만들고 그 안에 넣는다. 위 설정에는 비밀번호 auth 설정이 기본으로 바꿔놔서 cassandra 이름/비번으로 접속하면 된다.

     

    application.yml

    scylla:
      contactPoints: scylla-node1,scylla-node2 (도커 이미지 이름, localhost 아님)
      #  port: 9042
      localDC: datacenter1
      username: cassandra
      password: cassandra
      keyspace: 키스페이스
    #  consistency: LOCAL_QUORUM

     

    User.kt

    거의 primitive 타입만 가능하다

    @JsonInclude(JsonInclude.Include.NON_NULL)
    @Table("users")
    data class User(
        @field:PrimaryKey val id: UUID = Uuids.timeBased(),
    
        val email: String,
        val password: String,
    
        var nickname: String? = null,
    
        @field:Column("profile_image")
        var profileImage: String? = null,
    
        @field:Column("phone_number")
        var phoneNumber: String? = null,
    
        @field:Column("created_at")
        @CreatedDate val createdAt: Instant? = null,
    
        @field:Column("updated_at")
        @LastModifiedDate val updatedAt: Instant? = null,
    
        @field:Column("last_login")
        var lastLogin: Instant? = null,
    
        @field:Column("authorities")
        private var _authorities: String? = null
    ) {
        var authorities: Set<String>
            get() = _authorities?.split(",")?.toSet() ?: setOf()
            set(value) {
                _authorities = value.joinToString(",")
            }
    }

     

    ScyllaConfiguration.kt

    
    @Configuration
    @Profile("!unit-test & !integration-test")
    class ScyllaConfiguration {
        @Value("\${scylla.contactPoints}")
        private lateinit var rawContactPoints: String
    
        private val contactPoints: List<String>
            get() = rawContactPoints.split(",").filter { it.isNotBlank() }
    
        @Value("\${scylla.port:9042}")
        private val port: Int = 0
    
        @Value("\${scylla.localdc}")
        private val localDc: String? = null
    
        @Value("\${scylla.keyspace}")
        private val keyspaceName: String? = null
    
        @Value("\${scylla.consistency:LOCAL_QUORUM}")
        private val consistency: String = "LOCAL_QUORUM"
    
        @Value("\${scylla.username}")
        private val username: String? = null
    
        @Value("\${scylla.password}")
        private val password: String? = null
    
        @Value("\${scylla.replicationFactor:2}")
        private val replicationFactor: Int = 2 // two nodes
    
        @Bean
        fun keyspacePopulator() = ResourceKeyspacePopulator().apply {
            setScripts(ClassPathResource("init.cql"))
        }
    
        @Bean
        fun getSchemaAction() = SchemaAction.CREATE_IF_NOT_EXISTS
    
    
        @Bean
        fun configLoaderBuilder() = DriverConfigLoader.programmaticBuilder().apply {
            withString(DefaultDriverOption.REQUEST_CONSISTENCY, consistency)
            if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) {
                withString(DefaultDriverOption.AUTH_PROVIDER_CLASS, PlainTextAuthProvider::class.java.name)
                withString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, username)
                withString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, password)
            }
        }
    
        @Bean
        fun sessionBuilder(driverConfigLoaderBuilder: ProgrammaticDriverConfigLoaderBuilder) = CqlSessionBuilder().apply {
            withConfigLoader(driverConfigLoaderBuilder.build())
            contactPoints.forEach {
                addContactPoint(InetSocketAddress.createUnresolved(it, port))
            }
            withLocalDatacenter(localDc ?: throw IllegalStateException("Local DC must be specified"))
        }
    
        // create keyspace in init.cql
        @Bean
        fun session(sessionBuilder: CqlSessionBuilder, keyspacePopulator: KeyspacePopulator): CqlSession {
            // Build and use a temporary session to populate the keyspace
            sessionBuilder.build().use { tempSession ->
                keyspacePopulator.populate(tempSession)
            }
    
            // Rebuild and return the session with the keyspace
            return sessionBuilder.withKeyspace(keyspaceName).build()
        }
    
    
        // creating keyspace in java
    //    @Bean
    //    fun session(sessionBuilder: CqlSessionBuilder, keyspacePopulator: KeyspacePopulator): CqlSession =
    //        sessionBuilder.build().apply {
    //            keyspaceName?.let { createKeyspaceIfNeeded(this, it) }
    //            close()
    //        }.let {
    //            sessionBuilder.withKeyspace(keyspaceName).build().apply {
    //                keyspacePopulator.populate(this)
    //            }
    //        }
        private fun createKeyspaceIfNeeded(session: CqlSession, keyspaceName: String) {
            val query =
                "CREATE KEYSPACE IF NOT EXISTS $keyspaceName WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor' : $replicationFactor};"
            session.execute(query)
        }
    }

     

    src/main/resources/init.cql

    이건 드라이버 config에서 설정해 준다.

    CREATE KEYSPACE IF NOT EXISTS cswkotlin
        WITH replication = {'class':'NetworkTopologyStrategy', 'datacenter1':2}
         AND durable_writes = true;
    
    CREATE TABLE IF NOT EXISTS cswkotlin.users
    (
        id            uuid PRIMARY KEY,
        email         text,
        password      text,
        nickname      text,
        profile_image text,
        phone_number  text,
        created_at    timestamp,
        updated_at    timestamp,
        last_login    timestamp,
        authorities   text
    );

    여기까지 셋업 하면 UserRepository/Service/Controller는 mongodb/mysql 이런 것들 사용하는 방식이랑 똑같다.

    728x90

    댓글