-
Spring boot + Kotlin Coroutine + WebFlux + Security 5 + MySQL 기본 셋업컴퓨터/Spring Boot 2024. 3. 18. 21:31728x90반응형
Spring boot 2.7에서 kotlin coroutine을 써서 webflux를 사용할 것이다. (JDK 17)
시큐리티는 기본 셋업에 R2DBC MySQL을 써서 연결했다.
gradle-kts 버전이다.
MySQL 테이블
Geolocation Spatial 타입을 갖고 있는 간단한 table이다.
CREATE TABLE Markers ( MarkerID INT AUTO_INCREMENT PRIMARY KEY, UserID INT NULL, Location POINT NOT NULL SRID 4326, -- SRID Description VARCHAR(255), CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, Address VARCHAR(255) NULL; SPATIAL INDEX(Location), FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE SET NULL ); CREATE INDEX idx_markers_userId ON Markers(UserID);
entities/Marker
mysql table을 data class을 이용해서 만든다.
import com.fasterxml.jackson.annotation.JsonFormat import org.springframework.data.annotation.Id import org.springframework.data.relational.core.mapping.Column import org.springframework.data.relational.core.mapping.Table import java.time.LocalDateTime @Table("Markers") data class Marker( @Id @Column("MarkerID") val markerID: Int? = null, @Column("UserID") val userID: Int?, @Column("Latitude") val latitude: Double, @Column("Longitude") val longitude: Double, @Column("Description") val description: String?, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") @Column("CreatedAt") val createdAt: LocalDateTime, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss") @Column("UpdatedAt") val updatedAt: LocalDateTime, @Column("Address") val address: String? )
Repository/Service
CoroutineCrudRepository를 사용한다.
Point 타입을 기본적으로는 지원하지 않아서 ST_함수를 이용해서 값을 뽑았다.
MySQL에서 SRID를 설정한 후에 POINT(Longitude, Latitude)에서 POINT(Latitude, Longitude)로 변한다.
// repository import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.kotlin.CoroutineCrudRepository interface MarkerRepository : CoroutineCrudRepository<Marker, Int> { @Query(value = "SELECT MarkerID, UserID, ST_X(Location) AS Latitude, ST_Y(Location) AS Longitude, Description, CreatedAt, UpdatedAt, Address FROM Markers") suspend fun findAllMarkers(): List<Marker> } // service import org.springframework.stereotype.Service @Service class MarkerService(private val markerRepository: MarkerRepository) { suspend fun findAllMarkers(): List<Marker> = markerRepository.findAllMarkers() }
controllers/MarkerController
사실 굳이 Flux/Mono 타입을 return할 필요는 없다.
큰 데이터 셋에 프론트엔드에서 좀 스트리밍 개념처럼 받고 싶을 때 유용하지
suspend만 쓰더라도 webflux의 힘은 받으니 한 번에 받아서 한 번에 보내고 싶으면 그냥 List로 보낸다.
(Flow로 보낸다면, application/x-ndjson 을 produce하고 프론트엔드에서 각 라인별로 받으면 됨)
import kotlinx.coroutines.flow.toList import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController class MarkerController(private val markerService: MarkerService) { @GetMapping(value = ["/markers"], produces = ["application/json; charset=UTF-8"]) suspend fun getAllMarkers(): List<Marker> = markerService.findAllMarkers() }
config/JacksonConfig
기본적으로 LocalDateTime을 return하면 이상한 array 여서 변환을 해준다.
{ "createdAt": [2022,10,27,0,0] } -> { "createdAt": "2022-10-27T00:00:00" }
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Primary import java.time.LocalDateTime import java.time.format.DateTimeFormatter @Configuration class JacksonConfig { @Bean @Primary fun objectMapper(): ObjectMapper = ObjectMapper().registerModule(JavaTimeModule().apply { addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))) }) }
config/DatabaseConfig
메인에서 "R2dbcAutoConfiguration" 클래스를 exclude 안하면 이 설정은 작동을 안한다.
jasync-sql을 썻다. (R2DBC extension이 있어서 그걸 씀)
import com.github.jasync.r2dbc.mysql.JasyncConnectionFactory; import com.github.jasync.sql.db.SSLConfiguration import com.github.jasync.sql.db.mysql.pool.MySQLConnectionFactory import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Configuration import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories import java.nio.charset.Charset @Configuration @EnableR2dbcRepositories class DatabaseConfiguration : AbstractR2dbcConfiguration() { @Value("\${spring.r2dbc.url}") private lateinit var url: String @Value("\${spring.r2dbc.port}") private var port: Int = 0 @Value("\${spring.r2dbc.username}") private lateinit var username: String @Value("\${spring.r2dbc.password}") private lateinit var password: String @Value("\${spring.r2dbc.database}") private lateinit var database: String override fun connectionFactory(): io.r2dbc.spi.ConnectionFactory { val sslConfiguration = SSLConfiguration() val configuration = com.github.jasync.sql.db.Configuration( username, url, port, password, database, sslConfiguration, Charset.forName("UTF-8") ) return JasyncConnectionFactory(MySQLConnectionFactory(configuration)) } }
SecurityConfig5
간단한 셋업이다.
https://www.baeldung.com/spring-security-5-reactive
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.Customizer.withDefaults import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.config.web.server.ServerHttpSecurity.AuthorizeExchangeSpec import org.springframework.security.core.userdetails.MapReactiveUserDetailsService import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.web.server.SecurityWebFilterChain @EnableReactiveMethodSecurity @EnableWebFluxSecurity @Configuration class SecurityConfig { @Bean fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { // @formatter:off http .authorizeExchange { authorize -> authorize .pathMatchers("/a").hasRole("USER") .pathMatchers("/b").authenticated() .anyExchange().permitAll() } // @formatter:on return http.build() } @Bean fun userDetailsService(): MapReactiveUserDetailsService { // @formatter:off val user: UserDetails = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build() val admin: UserDetails = User.withDefaultPasswordEncoder() .username("admin") .password("password") .roles("ADMIN", "USER") .build() // @formatter:on return MapReactiveUserDetailsService(user, admin) } }
application.yml
spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration data: r2dbc: repositories: enabled: true r2dbc: url: blah.ap-northeast-2.rds.amazonaws.com database: DB이름 port: 3306 username: 유저네임 password: 비밀번호 server: address: 0.0.0.0 port: 8080 compression: enabled: true mime-types: text/html,text/plain,text/css,application/javascript,application/json min-response-size: 2KB logging: level: org.springframework.r2dbc.core: debug org: springframework: security: DEBUG # root: debug
build.gradle.kts
굉장히 헷갈렸다.
dev.miku 버전은 outdated여서 jasync-sql을 사용했는데 어떻게 쓰는지 몰라서 시간이 많이 걸렸다.
dependencies { implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") implementation("com.github.jasync-sql:jasync-r2dbc-mysql:2.2.0") }
결과
참고
https://alwayspr.tistory.com/44
728x90'컴퓨터 > Spring Boot' 카테고리의 다른 글
Spring Boot: Repository Extension 패턴 (CustomRepositoryImpl) (0) 2024.05.08 Spring Boot: Kotlin + Cassandra 에서 @CreatedDate null 해결 (0) 2023.12.16 Spring Boot: Kotlin + ScyllaDB 도커 시작 (0) 2023.12.14