Spring Framework/WebFlux
WebFlux + Coroutine + R2DBC로 CRUD 구현해보기
Junuuu
2023. 12. 25. 00:01
728x90
개요
정확한 동작까지는 모르더라도 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