-
Spring Boot: Kotlin + ScyllaDB 도커 시작컴퓨터/Spring Boot 2023. 12. 14. 21:34728x90반응형
Datastax 드라이버로 Configuration을 만들고 간단한 도메인을 만들어서 저장하는 과정을 썼다.
풀소스는 아래 github에서 참고.
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 폴더를 만들고 그 안에 넣는다. 위 설정에는 비밀번호 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'컴퓨터 > Spring Boot' 카테고리의 다른 글
Spring Boot: Repository Extension 패턴 (CustomRepositoryImpl) (0) 2024.05.08 Spring boot + Kotlin Coroutine + WebFlux + Security 5 + MySQL 기본 셋업 (0) 2024.03.18 Spring Boot: Kotlin + Cassandra 에서 @CreatedDate null 해결 (0) 2023.12.16