콘텐츠로 이동

Bluetape4k JaVers Part 2: Exposed, Redis, Kafka 영속 저장소 선택

작은 로봇 작업자들이 JaVers 스냅숏을 Exposed, Redis, Kafka 저장소로 분류하는 3D 작업대 일러스트
모두 영속화 모듈이지만 읽기, 쓰기, 복구 모델은 서로 다릅니다.

Part 1에서는 JaVers의 커밋, 스냅숏, 차이 모델을 살펴봤습니다. 이제 운영 환경에서 마주치는 질문으로 넘어가겠습니다.

스냅숏을 어디에 저장할 것인가?

bluetape4k-javers에는 Exposed JDBC, Redis, Kafka 저장소가 있습니다. 모두 영속화 모듈이지만 역할은 다릅니다. Exposed는 SQL에 남기는 영속 이력이고, Redis는 빠른 스냅숏 저장소이며, Kafka는 조회 저장소가 아닌 스냅숏 이벤트 스트림입니다. 이 구분을 놓치면 Kafka에서 이력을 직접 조회하거나, 영속성 정책을 정하지 않은 Redis를 감사 원장으로 운영하는 구조가 나오기 쉽습니다.

JaVers 커밋을 Exposed, Redis, Kafka 영속화 역할로 나누는 선택 지도
저장소는 모듈 이름이 아니라 읽기·쓰기 역할과 복구 모델을 기준으로 선택해야 합니다.

JaVers 커밋은 하나지만 저장 대상은 운영 목적에 따라 달라집니다. 사람이 이력을 조회해야 한다면 영속 저장소가 필요합니다. 화면에서 최신 스냅숏을 자주 읽는다면 빠른 조회 저장소가 필요합니다. 외부 감사 파이프라인이나 프로젝션을 구성한다면 이벤트 스트림이 필요합니다. 한 저장소에 세 역할을 모두 맡기면 읽기 경로와 복구 정책이 불분명해집니다.

Exposed JDBC: SQL에 남기는 영속 이력

섹션 제목: “Exposed JDBC: SQL에 남기는 영속 이력”

ExposedCdoSnapshotRepository는 JaVers 스냅숏과 커밋 메타데이터를 Exposed 테이블에 저장합니다. 기본적으로 CommitTableCdoSnapshotTable을 만들고, 각 스냅숏의 전역 ID, 커밋 ID, 버전, 유형, 상태, 변경 속성, 관리 유형을 저장합니다. 외부 마이그레이션 도구가 스키마를 관리한다면 createSchemaOnEnsure = false로 자동 생성을 끌 수 있습니다.

val snapshotRepository = ExposedCdoSnapshotRepository(database)
snapshotRepository.ensureSchema()
val javers = JaversBuilder.javers()
.registerJaversRepository(snapshotRepository)
.registerEntity(Order::class.java)
.build()

이 방식은 감사 이력을 SQL 백업·복구 체계와 함께 운영할 때 적합합니다. 저장소의 각 작업은 Exposed transaction {} 경계에서 실행됩니다. 다만 애플리케이션 데이터 변경과 JaVers 커밋이 자동으로 하나의 분산 트랜잭션이 되는 것은 아니므로, 호출 계층에서 트랜잭션 경계를 별도로 검토해야 합니다.

장점주의점
영속 저장과 백업·복구 절차가 명확함이력 테이블이 커질 수 있으므로 쿼리 형태를 검토해야 함
Exposed 트랜잭션 경계를 사용함대량 이력 조회 시 메모리 구체화 비용을 주의해야 함
SQL로 상태를 확인할 수 있음스냅숏 JSON 코덱과 스키마 변경 정책을 관리해야 함

Redis 모듈은 Lettuce와 Redisson 구현을 제공합니다. Lettuce 구현은 전역 ID별 LIST에 스냅숏을 최신순으로 저장하고, Redis HASH로 전역 ID 인덱스와 커밋 시퀀스를 관리합니다. Redisson 구현은 RListMultimapRMap을 사용합니다. 두 구현의 기본 코덱은 LZ4 + Fory 조합입니다.

val repo = LettuceCdoSnapshotRepository("order", redisClient)
val javers = JaversBuilder.javers()
.registerJaversRepository(repo)
.build()

Redis는 최신 스냅숏이나 짧은 이력을 자주 읽는 경로에 적합합니다. Redis를 주 저장소로 사용할 수도 있지만, 그 경우에는 AOF/RDB, 복제, 백업, 복구 목표를 감사 데이터의 보존 요구에 맞춰야 합니다. SQL을 기준 저장소로 두고 Redis를 명시적인 프로젝션이나 캐시 저장소로 사용하는 구성이 운영 책임을 구분하기 쉽습니다.

Lettuce 구현은 saveSnapshot에서 LIST 추가와 인덱스 갱신을 MULTI/EXEC로 묶습니다. 커밋 단위 persist도 모든 스냅숏과 커밋 시퀀스 갱신을 하나의 MULTI/EXEC 경계에서 처리합니다. 공유 동기 연결에서 트랜잭션 명령이 뒤섞이지 않도록 별도 쓰기 연결과 잠금을 사용합니다. 반면 Redisson 구현은 여러 자료 구조를 갱신하는 과정에서 부분 저장이 발생할 수 있으므로, 커밋 단위 원자성이 필요하면 Lettuce 구현을 선택해야 합니다.

Kafka: 조회 저장소가 아니라 이벤트 스트림

섹션 제목: “Kafka: 조회 저장소가 아니라 이벤트 스트림”

KafkaCdoSnapshotRepository는 의도적으로 쓰기 전용입니다. saveSnapshot은 스냅숏 이벤트를 지정한 토픽이나 KafkaTemplate 기본 토픽으로 발행하고, 전역 ID를 레코드 키로 사용합니다. getKeys, contains, getSeq, loadSnapshots 같은 읽기 메서드는 빈 값, false, 0을 반환합니다. 첫 읽기 호출에는 경고를 남겨 계약을 명시적으로 드러냅니다.

val repo = KafkaCdoSnapshotRepository(kafkaTemplate)
val javers = JaversBuilder.javers()
.registerJaversRepository(repo)
.build()
// 커밋하면 스냅숏 이벤트를 Kafka에 발행한다.
javers.commit("system", order)

Kafka 토픽은 하위 소비자, 프로젝션, 외부 감사 파이프라인으로 스냅숏 이벤트를 전달하는 데 적합합니다. 하지만 특정 객체의 최신 스냅숏을 조회하는 요청에는 Kafka 저장소만으로 응답할 수 없습니다. 이 조회는 Exposed나 Redis 같은 읽기 저장소가 맡아야 합니다. Kafka 레코드를 읽기 가능한 저장소로 재생성할 때는 KafkaCdoSnapshotProjector를 사용할 수 있습니다.

실제 운영에서는 여러 역할을 조합하기도 합니다. CompositeCdoSnapshotRepository는 읽기 기준이 되는 주 저장소 하나와, 순서대로 쓰기를 전달할 보조 저장소 목록을 받습니다. Exposed를 주 저장소로, Kafka를 보조 저장소로 두면 SQL에서 이력을 조회하면서 같은 커밋을 이벤트 스트림으로 발행할 수 있습니다. Redis와 Kafka를 함께 보조 저장소로 두면 한 번의 JaVers persist 호출에서 조회 프로젝션과 이벤트 발행을 순서대로 실행할 수 있습니다.

복합 저장소가 읽기를 주 저장소에 위임하고 쓰기를 주 저장소와 보조 저장소에 순서대로 전달하는 구성도
읽기는 주 저장소로 일원화하고, 쓰기는 주 저장소가 성공한 뒤 보조 저장소에 순서대로 전달합니다.
val repository = CompositeCdoSnapshotRepository(
primary = exposedRepository,
secondaryRepositories = listOf(
redisProjectionRepository,
kafkaRepository,
),
options = CompositeCdoSnapshotRepositoryOptions(
writeFailurePolicy = CompositeCdoSnapshotFailurePolicy.FAIL_FAST,
),
)

읽기 작업은 모두 주 저장소에 위임합니다. 쓰기는 주 저장소를 먼저 갱신한 뒤 보조 저장소를 순서대로 호출합니다. 기본 FAIL_FAST는 첫 보조 저장소 실패에서 중단하고, BEST_EFFORT는 나머지 보조 저장소까지 모두 시도한 뒤 실패를 모아 예외를 던집니다. BEST_EFFORT도 실패를 무시하거나 자동 재시도하지는 않습니다.

이 구성은 분산 트랜잭션이 아닙니다. 보조 저장소가 실패할 때 주 저장소에는 커밋이 이미 보일 수 있습니다. 따라서 재시도, 중복 이벤트 허용 여부, 운영 알림과 복구 절차는 애플리케이션에서 별도로 정해야 합니다.

기준Exposed JDBCRedisKafka
주 역할영속 스냅숏 이력빠른 스냅숏 조회·프로젝션스냅숏 이벤트 스트림
읽기JaVers 쿼리 지원최신 스냅숏·이력 조회항상 빈 값/false/0
쓰기Exposed 트랜잭션에서 행 저장LIST·멀티맵과 인덱스 갱신Kafka 발행 승인까지 대기
복구DB 백업·복구Redis 영속화 또는 재구성 정책 필요소비자 재처리·프로젝션 정책 필요
운영 복잡도DB 스키마와 용량 관리키 설계, TTL·영속화 정책토픽, 소비자, 재처리 관리
권장 용도감사 이력을 DB와 함께 운영조회가 많은 이력·프로젝션하위 감사·이벤트 파이프라인

Kafka 모듈이 영속화 패키지에 있다는 이유만으로 Kafka만 선택해서는 안 됩니다. 조회가 필요하면 Kafka 스트림과 별도의 읽기 저장소를 함께 두거나, KafkaCdoSnapshotProjector로 이벤트를 읽기 저장소에 반영해야 합니다.

bluetape4k-javers의 영속화 저장소는 읽기·쓰기·복구 책임에 맞춰 선택해야 합니다. 감사 이력을 SQL과 함께 운영하려면 Exposed, 스냅숏 조회 지연을 줄이려면 Redis, 스냅숏 이벤트를 전달하려면 Kafka가 적합합니다. 두 역할 이상이 필요하면 CompositeCdoSnapshotRepository로 주 저장소와 보조 저장소를 명시하되, 보조 저장소 실패 이후의 복구 정책까지 함께 설계해야 합니다.

다음 글에서는 이 선택지를 DDD 명령 처리 흐름과 워크숍 예제에 적용합니다. 애그리거트를 저장하고 JaVers에 커밋한 뒤 도메인 이벤트를 발행하는 순서가 왜 중요한지 살펴봅니다.

댓글

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