콘텐츠로 이동
Bluetape4k 문서1.11

Transaction, 실패, 테스트

최신 안정판 Bluetape4k 1.11.0 릴리스 기준

transaction은 외부 경계가 소유한다

섹션 제목: “transaction은 외부 경계가 소유한다”

이 모듈의 확장 함수는 transaction manager를 만들지 않습니다. 현재 coroutine context에 Spring reactive transaction이 연결돼 있으면 underlying Spring Data operation이 그 connection을 사용하고, 없으면 각 publisher의 일반 lifecycle을 따릅니다.

여러 write가 함께 성공해야 한다면 service operation 전체를 Spring의 reactive transaction으로 묶습니다. bluetape4k-r2dbcwithTransactionSuspend를 사용할 수도 있지만, 같은 ConnectionFactoryDatabaseClient 경계를 유지해야 합니다.

suspend fun createPost(command: CreatePost): Post =
databaseClient.withTransactionSuspend {
val post = posts.insert(command.toPost())
val postId = checkNotNull(post.id)
comments.insert(command.toInitialComment(postId))
post
}

프로젝트의 transaction policy가 annotation이나 TransactionalOperator를 표준으로 정했다면 그 방식을 따릅니다. 핵심은 개별 extension이 transaction을 자동 시작한다고 가정하지 않는 것입니다.

확장 함수는 R2dbcEntityOperations로 publisher를 실행할 뿐 connection을 직접 획득하거나 닫지 않습니다. 다음 설정은 애플리케이션 또는 Spring Boot가 소유합니다.

  • R2DBC URL과 credential
  • driver와 ConnectionFactory
  • pool 크기, acquire timeout, validation
  • startup schema migration
  • shutdown 시 pool 종료

1.11.0 테스트의 R2dbcBlogApplication은 H2 ConnectionFactory를 직접 만들고 initializer bean을 등록합니다. 이는 테스트 애플리케이션 구성 예제이지 모듈이 제공하는 auto-configuration이 아닙니다.

상황결과
selectOneSuspending 0건하위 API 예외
selectOneOrNullSuspending 0건null
selectOne* 여러 건cardinality 예외
selectFirstOrNullSuspending 0건null
update/delete 대상 없음0L
count 대상 없음0L
connection, SQL, mapping 실패해당 예외 전파
coroutine cancellationcancellation 전파

null, 0L, exception은 서로 다른 계약입니다. repository에서 모두 같은 domain 결과로 뭉개지 않습니다.

테스트 application은 r2dbc:h2:mem:///test connection을 만들고 schema.sqlposts, comments table을 초기화합니다. ApplicationReadyEvent listener가 기본 post 2건과 comment 4건을 삽입합니다.

R2dbcEntityOperationsExtensionsTest는 한 test 안에서 insert→update→select→delete→exists를 실행합니다. write마다 반환 행 수와 최종 상태를 확인하므로 오류 위치를 좁히기 쉽습니다.

@Test
fun `insert update delete extensions`() = runTest {
val saved = operations.insertSuspending(createPost())
val query = Query.query(Criteria.where(Post::id.name).isEqual(saved.id))
operations.updateSuspending<Post>(query, Update.update("title", "Updated")) shouldBeEqualTo 1L
operations.selectOneSuspending<Post>(query).title shouldBeEqualTo "Updated"
operations.deleteSuspending<Post>(query) shouldBeEqualTo 1L
operations.existsSuspending<Post>(query).shouldBeFalse()
}

공유 H2 database는 빠르지만 test 간 데이터 충돌이 생길 수 있습니다. generated ID나 고유한 입력을 사용하고, 각 test가 만든 row만 정리하거나 transaction rollback을 적용합니다. seed 개수를 정확한 상수로 단정하기보다 변경 전후 차이를 검증하면 test 순서 의존성을 줄일 수 있습니다.

실제 운영 driver의 type conversion, locking, transaction isolation은 H2만으로 증명되지 않습니다. PostgreSQL이나 MySQL 동작이 중요하면 해당 R2DBC driver로 별도 통합 테스트를 직렬 실행합니다.

R2DBC 생태계 학습 경로에서 다음 abstraction과 workshop을 선택합니다.