Bluetape4k Projects Part 4: data와 infrastructure

이 글은 bluetape4k-projects 시리즈의 4편입니다. Part 3에서는
byte, serializer, HTTP/RPC, encryption 경계를 봤습니다. 이번에는 byte가 목적지에 도착한 뒤의 이야기를
다룹니다. DB에서 읽고, Redis에 저장하고, broker로 보내고, metric과 trace로 남기고, 실패를 resilience
정책으로 제어하는 구간입니다.
이 영역의 공통점은 외부 시스템과 직접 붙는다는 점입니다. 코드 안에서는 await, Flow, transaction,
cache.get, send처럼 보이지만 실제로는 connection pool, timeout, retry, codec, backpressure, broker
partition, observability backend가 같이 움직입니다. 그래서 모듈 선택은 “어떤 라이브러리를 선호하나”보다
“어떤 실행 모델과 실패 경계를 서비스 코드에 노출할 것인가”에 가깝습니다.

Data: JDBC부터 R2DBC까지
섹션 제목: “Data: JDBC부터 R2DBC까지”data/ 아래의 모듈은 persistence 기술을 새로 추상화하려는 층이 아닙니다. JDBC, R2DBC, Hibernate,
Hibernate Reactive, MongoDB, Cassandra를 Kotlin 코드에서 덜 반복적으로 쓰게 하는 얇은 보조 층입니다.
bluetape4k-jdbc는 DataSource와 Connection 사용, ResultSet nullable getter, row mapping,
batch/update helper를 제공합니다. 동기 DB 접근을 명확히 유지해야 할 때 유용합니다. transaction 경계를
보여주는 코드가 그대로 JDBC라서 디버깅도 직선적입니다.
val users = dataSource.runQuery("SELECT id, name FROM users WHERE active = ?", true) { rs -> rs.toList { row -> User(row.getLong("id"), row.getString("name")) }}bluetape4k-r2dbc는 반응형 DB 접근을 coroutine/Flow 코드에서 다루기 쉽게 만듭니다. DatabaseClient
확장, binding helper, awaitSingle, flow 계열 helper가 핵심입니다. 특히 R2dbcPoolConfig는 high-throughput
프리셋, bounded pending acquire, acquire timeout 같은 운영 옵션을 README에서 실측 benchmark와 함께 다룹니다.
풀은 “크면 좋다”가 아니라 DB 연결 한도와 connection hold time으로 계산해야 합니다.
val posts = databaseClient .sql("SELECT * FROM posts WHERE author_id = :authorId") .bind("authorId", authorId) .fetch() .flow { row, _ -> Post(row.get("id") as Long, row.get("title") as String) }Hibernate 계열은 ORM을 Kotlin 서비스에서 덜 장황하게 쓰기 위한 확장입니다. EntityManager.save,
findAs, countAll, entity base class, converter, Querydsl helper가 들어 있습니다. Spring Boot 4
마이그레이션에서는 TestEntityManager shim 같은 테스트 경계도 제공합니다. ORM은 생산성을 주지만,
transaction과 lazy loading 경계를 흐리기 쉽습니다. 이 모듈은 그 경계를 숨기기보다 반복 코드를 줄이는
쪽에 가깝습니다.
MongoDB와 Cassandra는 reactive/coroutine access pattern을 맞추는 방향입니다. MongoDB 모듈은 coroutine
driver 확장, mongoClient {} DSL, findFirst, exists, upsert, findAsFlow, documentOf {}와
aggregation DSL을 제공합니다. Cassandra는 driver 사용을 Kotlin 쪽에서 정돈합니다.
Redis와 cache: codec, coroutine, Near Cache
섹션 제목: “Redis와 cache: codec, coroutine, Near Cache”Redis 계열은 infra/redis umbrella에서 시작하되 실제 선택지는 Lettuce와 Redisson입니다. 둘을 모두
가져올 수도 있지만, 보통은 서비스가 원하는 모델에 맞춰 하나를 고릅니다.
Lettuce는 async command와 coroutine adapter가 강점입니다. RedisFuture.awaitSuspending(), batch flush를
위한 withPipeline {}, Collection<RedisFuture>.awaitAll()이 대표적입니다. Codec도 빼놓을 수 없습니다.
LettuceBinaryCodecs는 JDK/Kryo/Fory 직렬화와 GZip/LZ4/Snappy/Zstd 압축 조합을 제공합니다. 사람이
읽어야 하는 값은 JSON codec, 내부 캐시 값은 Fory/LZ4 같은 profile을 선택할 수 있습니다.
Redisson은 분산 객체와 coordination primitive가 강점입니다. RMap, RLocalCachedMap, lock, semaphore,
batch/transaction DSL, memorizer, Near Cache가 필요할 때 자연스럽습니다. 다만 FastFory 계열 codec은
wire format 호환 경고가 있습니다. 휘발성 cache처럼 경계가 분명한 곳에 써야지, 장기 저장소처럼 다루면
나중에 디버깅하기 어려운 함정을 남기게 됩니다.
Cache 모듈은 별도 cache 시리즈에서 깊게 다뤘으니 여기서는 위치만 잡겠습니다. cache-core가 JCache,
SuspendCache, NearCache, memoizer 계약을 정의하고, Hazelcast/Redisson/Lettuce provider가 붙습니다.
cache abstraction, local/distributed cache, Near Cache 흐름을 더 자세히 보려면
Bluetape4k Cache Part 1: cache module에서 시작하면 됩니다.
Hibernate second-level cache는 hibernate-cache-lettuce가 Caffeine L1 + Redis L2 구조를 제공합니다.
Messaging, search, observability
섹션 제목: “Messaging, search, observability”인프라 모듈은 “외부 시스템별 adapter”에 가깝습니다.
| 영역 | 모듈 | 역할 |
|---|---|---|
| Messaging | kafka, kafka4, nats, pulsar | client DSL, coroutine/schema helper, Spring Kafka 4.x 라인 분리 |
| Search | elasticsearch | Elasticsearch Java API client DSL과 coroutine 지원 |
| Rate limit | bucket4j | token bucket 기반 rate limiting |
| Metrics | micrometer | metric instrumentation helper |
| Tracing | opentelemetry | distributed tracing helper |
| Resilience | resilience4j | coroutine retry, circuit breaker, rate limiter, timeout, coroutine cache |
여기서 중요한 것은 “모듈을 많이 붙이면 안정적이다”가 아닙니다. 오히려 반대입니다. 실패가 전파되는 경로마다 필요한 것만 붙여야 합니다. HTTP client timeout과 Resilience4j timeout과 database timeout이 서로 다른 시간표로 움직이면 장애가 완화되는 것이 아니라 설명하기 어려워집니다. 운영에서 가장 난감한 장애는 실패 자체보다 실패를 설명하는 로그가 서로 다른 이야기를 할 때입니다.
실행 모델을 먼저 고른다
섹션 제목: “실행 모델을 먼저 고른다”Part 4 모듈을 고를 때는 backend 종류보다 실행 모델을 먼저 봐야 합니다.
| 서비스 경계 | 먼저 볼 모듈 | 선택 기준 |
|---|---|---|
| 단순 동기 SQL, 기존 JDBC driver | bluetape4k-jdbc | transaction이 동기 코드에서 분명하고 blocking을 허용하는 경로 |
| reactive DB, coroutine service | bluetape4k-r2dbc | Flow, suspend query, bounded pool pressure를 다뤄야 하는 경로 |
| JPA/ORM domain model | bluetape4k-hibernate | entity lifecycle, converter, query helper, Spring Boot 4 테스트 shim이 필요한 경로 |
| Redis command 중심 | bluetape4k-lettuce | async command, coroutine adapter, pipeline, codec 선택이 중요한 경로 |
| Redis 분산 객체/lock/cache | bluetape4k-redisson | lock, semaphore, local cached map, memorizer가 필요한 경로 |
| local/distributed cache abstraction | cache-* | provider 교체, Near Cache, suspend cache 계약이 필요한 경로 |
| messaging/search/observability | infra/* | broker/search/metric/tracing backend와 붙는 adapter 경로 |
선택 기준은 단순합니다. 서비스 코드가 동기 API를 노출하는지, suspend API를 노출하는지, Flow를 흘리는지
먼저 정합니다. 그다음 외부 시스템을 고르고, 마지막으로 retry, timeout, metric, trace를 붙입니다. 순서를
반대로 하면 infrastructure가 service design을 끌고 다닙니다. 그때부터 서비스 코드는 도메인 코드보다
설정 코드에 가까워집니다.
참고 링크
섹션 제목: “참고 링크”- Repository README: README.ko.md
- Data root:
data/ - Infrastructure root:
infra/ - Cache root:
cache/ - R2DBC README:
data/r2dbc/README.ko.md - Lettuce README:
infra/lettuce/README.ko.md - Cache deep dive: Bluetape4k Cache Part 1
마무리
섹션 제목: “마무리”Data와 infrastructure 모듈은 service code가 외부 시스템과 만나는 구간을 정리합니다. DB, Redis, broker, metric, trace, retry는 모두 좋은 도구지만, 동시에 모두 자기 방식으로 실패합니다. 이 모듈들의 목적은 실패를 없애는 것이 아니라 실패 경계를 비슷한 모양으로 만드는 것입니다.
비슷한 모양의 실패는 테스트하기 쉽고, dashboard로 보기 쉽고, 다음 서비스에 설명하기 쉽습니다. 운영에서는 그 차이가 큽니다. 특히 새벽에 장애를 볼 때는 더 그렇습니다.
다음 글에서는 utility 모듈과 adoption path를 봅니다. 거대한 adapter보다 작은 공구가 먼저 문제를 해결할 때가 많습니다.
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.