ABOUT ME

-

Total
-
  • Spring boot + Kotlin Coroutine + WebFlux + Security 5 + MySQL 기본 셋업
    컴퓨터/Spring Boot 2024. 3. 18. 21:31
    728x90
    반응형

    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이 있어서 그걸 씀)

     

    GitHub - jasync-sql/jasync-sql: Java & Kotlin Async DataBase Driver for MySQL and PostgreSQL written in Kotlin

    Java & Kotlin Async DataBase Driver for MySQL and PostgreSQL written in Kotlin - jasync-sql/jasync-sql

    github.com

     

    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")
    }

     

    결과

    insomnia REST API 클라이언트

     

    참고

    https://alwayspr.tistory.com/44

     

    Spring WebFlux는 어떻게 적은 리소스로 많은 트래픽을 감당할까?

    위 그림은 DZone 게시글 중 하나인 Spring WebFlux를 이용한 Boot2와 Spring MVC를 이용한 Boot1을 비교한 그래프이다.

    alwayspr.tistory.com

     

    728x90

    댓글