20 KiB
문자열 명령 대신 타입을 노출하는 RedisOperations 코드 지도
Redis 코드 상세 시리즈 10/20 · 전체 지도 · 이전: Redis 값의 스키마를 코드로 고정하기: Registry·Envelope·Version · 다음: Batch·Transaction·Script·Function·Pub/Sub·Admin·Raw를 분리한 이유
이 글이 답하는 코드 질문
GET, HSET, ZRANGE 같은 command string 대신 애플리케이션이 무엇을 호출하며, sync와 reactive API가 같은 정책을 적용한다는 근거는 어디에 있습니까?
public aggregate interface는 RedisOperations와 ReactiveRedisOperations입니다. 둘 다 12개 accessor를 노출합니다. 각 operation은 typed key와 value codec을 받고, 공통 request builder가 CommandRequest를 만든 뒤 sync 또는 reactive executor로 보냅니다.
다만 이 aggregate interface를 구현한 production class와 Spring bean은 확인되지 않습니다. 세부 operation 구현과 contract 테스트가 존재한다는 사실과 application이 aggregate facade를 주입받을 수 있다는 사실을 구분해야 합니다.
먼저 보는 클래스·리소스 지도
| 클래스 | 입력 | 출력 | 다음 호출 |
|---|---|---|---|
| RedisOperations | 없음, accessor 호출 | sync operation group | 각 LettuceRedis*Operations |
| ReactiveRedisOperations | 없음, accessor 호출 | reactive operation group | 각 LettuceReactiveRedis*Operations |
| typed key interfaces | QualifiedRedisKey와 codec |
ValueKey, HashKey 등 |
request builder |
| operation interface | typed key, value, option, permit, budget | domain-shaped result | Lettuce implementation |
| package-private request builder | operation arguments | CommandRequest<R> |
executor |
| sync executor | deferred request | value/collection | gateway |
| reactive executor | deferred request | Mono/Flux |
gateway |
대표 호출을 볼 때는 LettuceRedisValueOperations과 ValueOperationRequests을 함께 읽으면 구조가 드러납니다.
Aggregate가 노출하는 12개 그룹
sync와 reactive aggregate accessor는 다음과 같습니다.
| accessor | sync surface | 대표 자료형·명령군 |
|---|---|---|
values() |
RedisValueOperations |
value/string, GET·SET·counter |
hashes() |
RedisHashOperations |
hash field/value |
lists() |
RedisListOperations |
ordered list |
sets() |
RedisSetOperations |
unordered set·algebra |
sortedSets() |
RedisSortedSetOperations |
score/rank/range |
bitmaps() |
RedisBitmapOperations |
bit offset·BITOP |
bitFields() |
RedisBitFieldOperations |
typed bitfield subcommand |
hyperLogLogs() |
RedisHyperLogLogOperations |
PFADD·PFCOUNT·PFMERGE |
geo() |
RedisGeoOperations |
point·distance·bounded search |
streams() |
RedisStreamOperations |
append·range·group·pending |
keys() |
RedisKeyOperations |
exists·delete·expiry·scan·rename |
batches() |
RedisBatchOperations |
bounded pipelined batch |
aggregate accessor 선언은 blocking list/stream, transaction, Pub/Sub, admin, raw, script/function을 포함하지 않습니다. 이 surface들은 connection ownership이나 ACL이 달라 별도 API로 남습니다.
Key type이 data structure와 codec을 고정합니다
operation은 String key와 byte[] value를 받지 않습니다. 예를 들어 ValueKey<V>는 qualified key와 RedisCodec<V>를 묶고, HashKey<F,V>는 field codec과 value codec을 함께 가집니다.
이 형태가 고정하는 계약은 다음과 같습니다.
- namespace 없는 raw key가 typed operation signature에 들어오지 않습니다.
- 같은 key를 hash API와 list API에 우연히 넘길 수 없습니다.
- encode/decode codec이 call마다 따로 선택되지 않습니다.
Optional<V>,ExpirationResult,ScanPage<T>같은 결과가 Redis reply sentinel을 감춥니다.
server에 이미 다른 data type으로 저장된 key라면 compile-time type만으로 막을 수 없습니다. 이 경우 driver의 WRONGTYPE을 exception translator가 RedisDataTypeMismatchException으로 바꿉니다.
Sync value read의 호출 순서
sequenceDiagram
participant A as Application
participant V as LettuceRedisValueOperations
participant B as ValueOperationRequests
participant C as RedisOperationContext
participant E as SyncRedisCommandExecutor
participant G as RedisCommandGateway
A->>V: get(ValueKey<V>)
V->>B: get(key)
B->>C: renderKey(key.key)
B->>B: GET CommandRequest 구성
V->>E: execute(request)
E->>E: guard.validate
E->>G: deferred get(bytes)
G-->>B: stored bytes/null
B->>C: value codec으로 decode
C-->>A: Optional<V>
ValueOperationRequests.get은 key를 render하고 GET command id, request byte 수, deferred gateway call을 한 객체에 넣습니다. reply가 오면 key에 묶인 codec으로 decode합니다.
이 기본 GET에는 OperationBudget이 없고 expectedReplyBytes도 0입니다. 공통 decode 함수는 codec만 호출하므로 관측한 reply byte ceiling을 집행하지 않습니다. MGET의 decodeAll이 budget을 검사하는 것과 다른 경로입니다.
LettuceRedisValueOperations.get은 request builder와 executor를 연결할 뿐 command 정책을 다시 구현하지 않습니다.
Reactive path가 공유하는 부분과 다른 부분
reactive value implementation도 같은 ValueOperationRequests를 사용합니다. 따라서 command 선택, key rendering, permit, budget, encoding 분기가 sync와 reactive에서 따로 복제되지 않습니다.
다른 것은 executor와 반환 shape입니다.
- sync는
CompletionStage를 deadline까지 기다리고 값을 반환합니다. - reactive는
Mono.defer안에서 admission을 실행하고Mono.fromCompletionStage로CompletionStage를Mono로 변환합니다. Optional<T>sync 결과는 reactive에서 emptyMono<T>가 됩니다.List<T>/Set<T>sync 결과는Flux<T>가 됩니다.- primitive는 boxed
Mono가 됩니다.
ApiParityInspector의 규칙은 method name과 generic parameter를 비교하고 예상 reactive return shape를 계산합니다.
Pub/Sub은 mechanical parity 대상에서 의도적으로 빠집니다. sync는 handler와 closeable subscription을 반환하고 reactive는 publisher cancellation을 lifecycle로 사용하기 때문입니다.
Group별로 봐야 하는 정책 지점
Value
RedisValueOperations은 ordinary SET 계열을 expiration이 필수인 public method로 표현합니다. setIfAbsent와 setIfPresent는 각각 SET NX와 SET XX, getAndSet은 SET GET, getAndExpire는 GETEX 옵션으로 내려갑니다. deprecated command 이름인 SETNX와 GETSET 자체는 policy에서 BLOCKED이며 이 API가 전송하지 않습니다.
multi-get과 range/append는 permit·budget을 요구합니다. 다만 APPEND와 SETRANGE의 method에는 expiration이나 PersistentKeyPermit이 없습니다. append request와 setRange request는 absent key를 만들 수 있는데도 TTL 경계를 호출하지 않습니다.
Hash
RedisHashOperations은 field와 value codec을 분리합니다. full collection read나 scan은 bound를 가진 API로 표현됩니다. hash write에는 expiration 인자가 없다는 현재 공백이 있습니다.
List
RedisListOperations은 side를 enum으로 표현하고 count/range를 bound합니다. blocking pop/move는 aggregate 밖의 blocking surface입니다.
Set과 sorted set
set algebra의 multi-key 비용은 permit과 budget으로 드러납니다. sorted set은 ScoreRange, RankRange, LexRange, page/bound 자료형으로 overload ambiguity를 줄입니다.
Bitmap과 bitfield
bitmap은 bit offset과 multi-key bit operation을 구분합니다. bitfield는 raw subcommand string 대신 BitFieldSubcommand, overflow enum, typed result를 사용합니다.
HLL과 Geo
HyperLogLog merge는 multi-key permit 대상입니다. Geo search는 center/radius/unit/page를 자료형으로 묶고 reply 수를 제한합니다.
Stream
stream은 StreamId, StreamRange, StreamReadOffset, StreamGroup, StreamConsumer, pending/claim result를 사용합니다. blocking read와 version-gated deletion은 기본 aggregate와 분리됩니다.
Key
key group은 expiry, TTL, scan, delete/unlink, rename을 담당합니다. scan은 전 keyspace materialization 대신 cursor page를 반환합니다.
Batch
batch는 aggregate에 있지만 atomic transaction이 아닙니다. per-command outcome과 partial failure를 반환하는 latency optimization입니다.
Request builder가 공유하는 guardrail
각 family의 package-private *OperationRequests는 다음 일을 맡습니다.
- null과 local option을 검사합니다.
- key를 render합니다.
- value/member/field를 codec으로 encode합니다.
- request byte와 expected reply byte를 계산합니다.
- 필요한 permit과
OperationBudget을 붙입니다. - gateway call을 supplier로 지연합니다.
- reply를 typed result로 decode합니다. 관측 reply budget 검사는 builder가
requireReplyWithinBudget을 호출한 MGET, bounded range, collection page 등 일부 경로에만 있습니다.
예를 들어 multiGet은 empty key list를 거절하고, 모든 rendered key byte를 합산하고, collection budget과 multi-key permit을 MGET request에 넣습니다.
정상·실패 분기
정상
- absent GET/hash field/list pop은
Optional.empty등 typed absence로 돌아옵니다. - conditional write는 boolean 또는 typed outcome으로 조건 불충족을 표현합니다.
- cursor operation은 elements와 next cursor/complete state를 반환합니다.
- sync와 reactive는 같은 request builder를 거쳐 같은 command·permit·budget을 적용합니다.
전송 전 거절
- malformed key와 foreign namespace
- forged/missing permit
- empty 또는 configured maximum을 넘긴 collection
- request/reply estimate가 budget을 넘긴 경우
- Cluster cross-slot
- server version에 없는 version-gated command
- codec encode size 초과
server reply 실패
WRONGTYPE:RedisDataTypeMismatchException- ACL 오류:
RedisAccessDeniedException - redirection/partition:
RedisRedirectionException - busy/loading: typed busy failure
- timeout/connection loss: read/write와 ambiguity에 따라 분기
decode 실패
schema, version, framing이 맞지 않으면 cache miss로 바뀌지 않고 RedisSerializationException입니다.
테스트가 고정하는 계약
PAIRS 선언은 15개 sync/reactive surface pair를 열거합니다. 기본 12개 외에 blocking list, blocking stream, hash field expiration도 pair 대상이며, 전체 pair parity 테스트가 각 pair의 method shape를 비교합니다.
aggregate accessor 테스트는 accessor가 정확히 batches, bitFields, bitmaps, geo, hashes, hyperLogLogs, keys, lists, sets, sortedSets, streams, values인지 고정합니다. publisher 반환 테스트는 모든 reactive method의 return type을 별도로 검사합니다.
family별 contract 테스트도 있습니다.
- RedisValueOperationsContractTest
- RedisHashOperationsContractTest
- RedisListOperationsContractTest
- RedisSetOperationsContractTest
- RedisSortedSetOperationsContractTest
- RedisBitmapGeoOperationsContractTest
- RedisStreamOperationsContractTest
- RedisKeyOperationsContractTest
이 테스트는 in-memory gateway와 contract fixture를 많이 사용합니다. 일부 live test가 별도 존재하지만 이번 문서 작업에서는 어떤 테스트도 실행하지 않았습니다.
WAIT와 typed surface
RedisOperations와 세부 typed interface에는 WAIT method가 없습니다. command policy에도 WAIT가 없습니다. durability 문맥에서 WAIT를 언급한 기존 문서를 typed API 지원 증거로 읽으면 안 됩니다. 현재는 catalog default-deny입니다.
현재 구현 공백과 잘못 읽기 쉬운 지점
RedisOperations와ReactiveRedisOperations구현 class를 production source에서 찾지 못했습니다.- 두 aggregate type의 Spring bean도 확인되지 않습니다.
- guard, executor, translator의 production DI가 확인되지 않으므로 세부 Lettuce operation을 application에 연결하는 bridge가 미조립입니다.
- tests의
RedisOperationsFixture는 production composition 증거가 아닙니다. - sync/reactive parity는 signature와 return shape를 고정하지만 실서버에서 두 path의 모든 동작이 같다는 증명은 아닙니다.
- expiration 의무는 ordinary value
SET계열과 nontransactional increment에는 적용되지만 모든 write에 완결되지 않았습니다. APPEND, SETRANGE, transaction의 INCRBY, transaction collection write, hash/list/set/zset write는 absent key를 만들 수 있어도 expiration이나 persistent permit을 받지 않습니다. - budget 객체와 관측 reply ceiling은 같은 뜻이 아닙니다. 기본 GET과 advanced script/function/raw/admin/extension path에는 관측 reply 크기를 검사하는 호출이 없습니다.
- version-gated extension은 aggregate accessor에 자동으로 들어오지 않습니다.
다음에 source를 열 때는 aggregate interface, 한 family interface, sync/reactive implementation, 공통 request builder, context, executor, family contract test 순으로 보면 됩니다.
시리즈의 관련 문서
관련 범위는 command admission, keyspace·expiration, codec, advanced surfaces, execution failure certainty입니다.