-
WebFlux + Coroutine + R2DBC로 CRUD 구현해보기Spring Framework/WebFlux 2023. 12. 25. 00:01728x90
개요
정확한 동작까지는 모르더라도 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
'Spring Framework > WebFlux' 카테고리의 다른 글
Mono, Flux 이해하기 (1) 2024.01.05 R2DBC란 무엇인가? (0) 2023.12.27 Webflux란? (0) 2023.12.02