콘텐츠로 이동

Bluetape4k Projects Part 5: utility와 adoption path

작은 로봇 작업자들이 utility 모듈과 예제, workshop, benchmark 노트를 서비스 채택 경로로 연결하는 일러스트
작은 공구는 작아서 좋습니다. 문제마다 공구함 전체를 끌고 오지만 않으면요.

이 글은 bluetape4k-projects 시리즈의 5편입니다. Part 4에서는 DB, Redis, messaging, observability 같은 운영 시스템 경계를 봤습니다. 이번에는 그보다 작은 모듈을 봅니다. ID를 만들고, 시간과 단위를 다루고, 돈과 지리 정보를 표현하고, rule/state/workflow를 조립하는 utility 영역입니다.

Utility 모듈은 domain repository와 성격이 다릅니다. bluetape4k-exposed, bluetape4k-leader, bluetape4k-text처럼 독립 저장소로 커진 기능은 자체 생태계와 릴리스 리듬이 있습니다. 반면 projects의 utility는 서비스 코드 여기저기에서 반복되는 작은 결정을 한곳에 모은 것입니다. 작지만 자주 쓰입니다. 그리고 자주 쓰이는 작은 코드는 생각보다 빨리 표준이 됩니다. 표준이 되기 전에 제멋대로 퍼지면, 나중에는 표준화보다 정리가 더 어려워집니다.

Identity, domain math, decision tools, minimal module selection, examples, workshops, benchmark notes, and service adoption map
Utility는 필요한 문제에서 시작해 최소 모듈을 고르고, 예제와 benchmark로 채택 경계를 확인하는 편이 좋습니다.

bluetape4k-idgenerators는 UUID v1~v7, ULID, KSUID, Snowflake, Flake, Hashids를 IdGenerator<T> 계열로 정리합니다. 여기서 중요한 것은 “많이 지원한다”가 아니라 “요구사항별 선택지가 README에 노출되어 있다”는 점입니다.

요구사항먼저 볼 생성기
DB PK 정렬성과 범용성UUID v7
문자열 ID와 시간 정렬ULID, KSUID
분산 환경에서 Long IDSnowflake
short URL 또는 난독화Hashids
완전 랜덤UUID v4
val orderId = Uuid.V7.nextId()
val publicId = Uuid.V7.nextBase62()
val eventId = Ksuid.Millis.generate()
val shardKey = Snowflakers.Default.nextId()

ID는 나중에 바꾸기 어렵습니다. database index, URL, event payload, log correlation, 외부 API 계약에 다 묻어납니다. 그래서 utility처럼 보여도 실제로는 architecture 결정입니다. “일단 random UUID”는 빠르지만, 정렬과 저장 비용, URL 노출, shard 전략을 나중에 다시 물어봅니다. 보통은 그 질문이 migration ticket으로 돌아옵니다.

money, measured, geo, science, javatimes는 domain value를 문자열과 숫자로 흘려보내지 않기 위한 도구입니다.

bluetape4k-measuredUnitsMeasure<T : Units>로 단위를 표현합니다. 길이, 시간, 질량, 부피, 온도, 각도, 면적, 저장 용량, 주파수, 에너지/전력, 속도/가속도 같은 단위를 조합할 수 있습니다.

val speed = 10 * Length.meters / Time.seconds
val duration = 5 * Time.seconds
val distance = speed * duration
println(distance `as` Length.meters)

돈과 단위는 primitive로 두면 처음에는 편합니다. BigDecimal amount, String currency, Double distance 정도로 시작합니다. 그런데 환율, 반올림, 단위 변환, display format이 붙으면 primitive가 갑자기 말을 너무 많이 해야 합니다. 그때부터는 type이 의미를 담게 하는 편이 낫습니다.

geoscience는 위치와 공간 데이터를 다룹니다. geocode/geohash/geoip2, 좌표계 변환, shapefile 읽기, JTS 기반 공간 연산, PostGIS 적재 pipeline이 포함됩니다. 이 영역은 domain이 맞는 서비스에서만 가져와야 합니다. 필요 없는 서비스에 붙이면 도메인과 관계없는 의존성만 늘어납니다.

rule-engine, states, workflow, probabilistic은 decision과 흐름을 표현하는 utility입니다.

rule-engine은 Kotlin DSL rule, annotation 기반 rule, coroutine rule, YAML/JSON/HOCON reader, MVEL2, SpEL, Kotlin Script, Janino, Groovy 기반 동적 rule을 제공합니다. 핵심 모델은 단순합니다. Facts를 보고 condition을 평가하고, 만족하면 action을 실행합니다.

val discountRule = rule {
name = "discount"
priority = 1
condition { facts -> (facts.get<Int>("amount") ?: 0) > 1000 }
action { facts -> facts["discount"] = true }
}
ruleEngine { skipOnFirstAppliedRule = true }
.fire(ruleSetOf(discountRule), Facts.of("amount" to 1500))

states는 Kotlin DSL 기반 finite state machine입니다. guard 조건, coroutine FSM, StateFlow 상태 관찰이 필요할 때 맞습니다. workflow는 sequential/parallel/conditional/repeat/retry 흐름을 조합합니다. 동기 경로는 virtual thread, 비동기 경로는 coroutine/Flow 모델을 지원합니다.

선택 기준은 간단합니다. 정책 계산이 많고 입력 fact에 따라 결과가 달라지면 rule-engine입니다. 객체가 몇 개의 명확한 상태를 이동하면 states입니다. 여러 step을 실행하고 retry/partial success/cancel을 표현해야 하면 workflow입니다. 이 셋을 한꺼번에 붙이면 작은 주문 처리도 필요보다 큰 프로세스처럼 보일 수 있습니다. 그럴 때는 한 번 더 줄여 봐야 합니다.

예제와 workshop으로 채택 경로를 확인한다

섹션 제목: “예제와 workshop으로 채택 경로를 확인한다”

Utility를 채택할 때는 README만 읽고 끝내지 말고 예제를 같이 봐야 합니다. projects/examples에는 coroutines, JPA/Blaze Persistence, Querydsl, Redisson, virtual threads, idgenerator Spring Boot/Ktor demo가 있습니다. bluetape4k-workshop은 cache, Redis, coroutines, virtual threads, observability, messaging, Spring Data 쪽 채택 경로를 더 application에 가깝게 보여줍니다.

서비스에서 보이는 필요먼저 볼 utility/example이유
public ID와 batch ID endpoint가 필요함idgenerators, idgenerator Spring Boot/Ktor demogenerator 선택과 HTTP 노출 방식을 같이 확인
Redis lock/cache pattern이 필요함redisson-demo, cache seriesprimitive와 cache strategy를 분리해 볼 수 있음
coroutine 흐름을 팀에 설명해야 함coroutines-demo, workshop coroutines문법보다 cancellation/Flow/context 사용법을 확인
virtual thread 전환을 검토함virtualthreads-demo, virtual thread blog seriespooling 금지, semaphore, synchronized 위험을 먼저 점검
Spring Data coroutine helper가 필요함Spring Boot R2DBC/MongoDB README, workshoprepository 경계와 test fixture를 같이 확인

채택 순서는 이렇게 잡으면 덜 위험합니다.

  1. BOM을 먼저 가져와 버전 줄을 맞춥니다.
  2. 지금 반복되는 문제 하나에 필요한 utility 모듈만 추가합니다.
  3. README의 선택 기준과 예제를 같이 봅니다.
  4. benchmark가 있는 모듈은 숫자의 방향과 측정 조건을 확인합니다.
  5. service test에서 실패 경계와 serialization/ID/time 정책을 고정합니다.

이 순서를 지키면 projects는 큰 framework가 아니라 작은 공구함으로 남습니다. 공구함은 작을수록 좋습니다. 모듈을 추가할 때마다 사용법과 정책을 새로 설명해야 한다면, 이미 너무 많이 들여온 겁니다.

Utility 모듈은 작아서 도입하기 쉽지만, 그래서 더 조심해야 합니다. ID, 시간, 돈, 단위, rule, workflow는 서비스 안쪽 깊숙이 들어갑니다. 처음에는 helper 하나였는데, 몇 달 뒤에는 데이터 계약이 됩니다.

좋은 채택 경로는 거창하지 않습니다. BOM으로 버전을 맞추고, 한 문제에 한 모듈만 들이고, 예제에서 실제 사용 모양을 보고, 테스트로 정책을 고정합니다. 이 정도면 충분히 보수적이고, 충분히 빠릅니다.

다음 글에서는 Spring Boot 4와 Ktor 3 application layer를 봅니다. 이제 작은 공구와 adapter를 실제 애플리케이션 경계에 얹을 차례입니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.