ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WebFlux + Coroutine + R2DBC로 CRUD 구현해보기
    Spring Framework/WebFlux 2023. 12. 25. 00:01

    개요

    정확한 동작까지는 모르더라도 WebFlux + Coroutine + R2DBC를 활용하여 CRUD를 구현해보고자 합니다.

     

    MVC만 다루던 개발자로써 모르는 개념이 다수 등장할 수 있습니다.

    저는 개인적으로 아래의 개념에 대해서 잘 모른다고 느꼈고 이번 포스팅 이후에 하나씩 알아가보려고 합니다.

    • r2dbc
    • Mono와 Flux, Flow
    • CoroutineCrudRepository
    • coRouter와 @RestController의 차이

     

    의존성 추가

    dependencies {
    	implementation("org.springframework.boot:spring-boot-starter-webflux")
    	implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    
    	implementation("io.r2dbc:r2dbc-h2")
    	runtimeOnly("com.h2database:h2")
    
    	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    	implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    
    	testImplementation("io.projectreactor:reactor-test")
    }

    webflux, r2dbc, h2를 추가하였습니다.

     

    application.yml

    spring:
      r2dbc:
        username: sa
        password:
        url: r2dbc:h2:mem:///test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    
      h2:
        console:
          enabled: true
          path: /h2-console
      sql:
        init:
          mode: always

     

    h2 database를 활용하기 위한 설정을 구성합니다.

     

    resources/schema.sql

    CREATE TABLE IF NOT EXISTS member
    (
        id bigint NOT NULL AUTO_INCREMENT,
        name VARCHAR(255),
        PRIMARY KEY (id)
    );

    spring boot에서는 데이터베이스 초기 설정을 위해 DDL(schema.sql), DML(data.sql)을 사용할 수 있습니다.

    resources/data.sql

    insert into member (name) values ('James')
    insert into member (name) values ('Josh')
    insert into member (name) values ('jun')
    insert into member (name) values ('Roman')

     

     

    Member

    @Table
    data class Member(
        @Id val id: Long = 0,
        val name: String,
    )

     

     

    Repository

    @Repository
    interface MemberRepository: CoroutineCrudRepository<Member, Long>{}

     

    CoroutineCrudRepository를 활용합니다.

    기본적으로 CRUD에 대한 기능을 제공합니다.

     

    Persistence Layer 테스트

    @SpringBootTest
    class WebFluxApplicationTests @Autowired constructor(
    	private val memberRepository: MemberRepository,
    ) {
    
    	@Test
    	fun contextLoads() {
    	}
    
    	@Test
    	fun `DB에 junwoo 라는 member를 넣으면 5명이 조회되어야 한다`(){
    		runBlocking {
    			memberRepository.save(Member(name = "junwoo"))
    			val members = memberRepository.findAll()
    			Assertions.assertNotNull(members.last().id)
    			Assertions.assertEquals(members.count(), 5)
    		}
    	}
    }

     

    기존에 데이터베이스 세팅이 4개가 되어있기 때문에 junwoo라는 Member로 신규로 넣으면 총 5명의 회원이 조회되어야 합니다.

     

    Handler(Service) 구현

    @Component
    class MemberHandler(private val repository: MemberRepository) {
        suspend fun getAllMembers(): Flow<Member> {
            return repository.findAll()
        }
    
        suspend fun getMemberById(id: Long): Member? {
            return repository.findById(id)
        }
    
        suspend fun createMember(member: Member): Member {
            return repository.save(member)
        }
    
        suspend fun updateMember(id: Long, updatedMember: Member): Boolean {
            val existingMember = repository.findById(id)
            return if (existingMember != null) {
                repository.save(updatedMember.copy(id = id))
                true
            } else {
                false
            }
        }
    
        suspend fun deleteMember(id: Long): Boolean {
            val existingMember = repository.findById(id)
            return if (existingMember != null) {
                repository.deleteById(id)
                true
            } else {
                false
            }
        }
    }

     

    repository의 구현을 활용합니다.

    다만 특이사항으로는 suspend 키워드가 붙어있습니다.

     

    Router(Controller) 구현

    @Configuration
    class MemberRouter(private val handler: MemberHandler) {
        @Bean
        fun memberRoutes(): RouterFunction<ServerResponse> = coRouter {
            GET("/members") { request ->
                val members = handler.getAllMembers()
                ok().bodyAndAwait(members)
            }
    
            GET("/members/{id}") { request ->
                val memberId = request.pathVariable("id").toLong()
                val member = handler.getMemberById(memberId)
                if (member != null) {
                    ok().bodyValueAndAwait(member)
                } else {
                    notFound().buildAndAwait()
                }
            }
    
            POST("/members") { request ->
                val member = request.bodyToMono<Member>().awaitSingle()
                val savedMember = handler.createMember(member)
                created(create("/members/${savedMember.id}")).bodyValueAndAwait(savedMember)
            }
    
            PUT("/members/{id}") { request ->
                val memberId = request.pathVariable("id").toLong()
                val updatedMember = request.bodyToMono<Member>().awaitSingle()
                val result = handler.updateMember(memberId, updatedMember)
                if (result) {
                    ok().buildAndAwait()
                } else {
                    notFound().buildAndAwait()
                }
            }
    
            DELETE("/members/{id}") { request ->
                val memberId = request.pathVariable("id").toLong()
                val result = handler.deleteMember(memberId)
                if (result) {
                    noContent().buildAndAwait()
                } else {
                    notFound().buildAndAwait()
                }
            }
        }
    }

    coRouter를 활용하여 Endpoint들을 정의할 수 있습니다.

    bodyToMono 등의 메서드등을 활용해 볼 수 있습니다.

     

    Http로 통합 시나리오 테스트 하기

    ### 전체회원조회
    GET localhost:8080/members
    
    
    ### 특정회원조회 - 존재
    GET localhost:8080/members/1
    
    ### 특정회원조회 - 존재하지 않음
    GET localhost:8080/members/100
    
    ### 회원가입
    POST localhost:8080/members
    Content-Type: application/json
    
    {
      "name" : "jun1"
    }
    
    ### 수정
    PUT localhost:8080/members/5
    Content-Type: application/json
    
    {
      "name" : "updated"
    }
    
    ### 삭제
    DELETE localhost:8080/members/5

     

     

     

     

    참고자료

    https://www.youtube.com/watch?v=pXtTp4Uxuhk

    https://github.com/bezkoder/spring-boot-r2dbc-h2-example/blob/master/src/test/java/com/bezkoder/spring/r2dbc/h2/SpringBootR2dbcH2ExampleApplicationTests.java

     

    'Spring Framework > WebFlux' 카테고리의 다른 글

    Mono, Flux 이해하기  (1) 2024.01.05
    R2DBC란 무엇인가?  (0) 2023.12.27
    Webflux란?  (0) 2023.12.02

    댓글

Designed by Tistory.