- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다 - 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5 (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를 techviz 로 만들었다 - 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs 돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다. Concept 이 인용한 코드가 SSOT 에 없어 뺐다 - candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
15 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a10-f004-pub-sub | 상한을 주입받는 자리는 있고 주입하는 곳은 없다 | caching-and-redis | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a10-f004-pub-sub | 2026-09-02 | case-a10-f004-pub-sub.body.md |
|
|
|
상한을 주입받는 자리는 있고 주입하는 곳은 없다
조립된 문자열이 최대 바이트를 넘지 않는지 보는 검사가 같은 성격의 세 타입 중 하나에만 있다. 검사를 부르는 다른 한 곳은 키 렌더러이고, 키 렌더러는 상수가 아니라 생성자로 받은 상한을 본다. 그 인자를 채우는 코드가 저장소에 없다.
관계
- 채워 넣은 상한은 자기가 잴 요청에서 값을 가져온다 같은 리프에서 상한 값을 어디서 가져오는지가 문제가 된 다른 사례다.
- R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다 두 사례 모두 같은 성격의 두 자리 중 한쪽에만 검사가 있다.
- 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다 경계가 어디서 지켜지는지 묻는 규칙이다.
문제
키 규칙에 조립된 문자열의 렌더 크기를 검사하는 메서드가 있다. Pub/Sub 쪽에는 같은 성격의 타입이 셋 있고, 그중 하나만 그 검사를 부른다.
결론
부르는 쪽은 패턴 타입이다. 컴팩트 생성자에서 이름공간 접두와 접미를 이어 놓고 크기를 잰다. 재는 때가 렌더보다 한 발 앞이다.
두 채널 타입은 부르지 않는다. 널 검사만 하고 렌더에서 세 조각을 잇는다.
상한이 상수 512 인 한 그 빠진 검사가 발화할 입력이 없다. 조각 셋이 모두 같은 규칙을 먼저 통과한 값이다. 이름공간의 세 토큰과 개체 토큰은 최대 64자, 식별자는 최대 128자다. 두 정규식은 ASCII 만 받으므로 길이가 곧 바이트다. 세 토큰을 모두 최대로 채운 렌더가 388 바이트다. 설정 기본 이름공간에서는 221 바이트다.
패턴은 다르다. 접미에 붙는 규칙은 공백이 아닐 것과 구분자를 넘지 않을 것 둘뿐이고 길이 제한이 없다. 이름공간을 최대치로 잡으면 접미 317자까지 512 바이트로 통과하고 318자에서 513 바이트가 되어 생성자가 거부한다.
원본의 판단은 조각마다 길이 제한이 있어 현실적인 초과가 어렵다는 것이었다. 그 반례가 같은 패키지의 키 경로에 있다. 키 렌더러는 이름공간과 개체와 식별자 사이에 슬롯 태그를 하나 더 넣는데, 그 태그도 식별자 규칙을 받아 최대 128자다. 넷을 최대로 채우면 519 바이트가 되고 같은 검사가 거부한다. 조각이 규칙을 받는다는 것과 합이 상한 안에 있다는 것은 다른 말이다. 다만 이것도 이름공간이 194 바이트일 때의 값이고, 기본 이름공간에서는 같은 태그를 붙여도 352 바이트다.
두 번째 차이는 상한을 어디서 가져오느냐다. 키 렌더러가 보는 상한은 생성자 인자이고 1 부터 512 까지 받는다. 패턴은 인자를 받지 않고 상수를 본다. 채널은 아무것도 보지 않는다. 상한 256 으로 만든 렌더러는 388 바이트짜리 키를 거부하는데, 같은 크기의 채널 이름은 그대로 나간다.
그런데 그 인자를 채우는 배선이 없다. 저장소의 src/main 전체에서 렌더러를 만드는 곳이 0 이고, 만드는 곳 열둘은 전부 시험 소스다. 설정 쪽도 마찬가지다. max-key-bytes 쪽도 같다. 등록과 범위 검증은 지나는데 읽는 자리가 없다.
그래서 지금 배포에서는 상수 512 가 셋을 다 덮는다. 셋이 갈리는 것은 그 인자가 설정과 이어지는 날이다.
판정은 P3 이다. 세 타입이 같은 문자열 공간을 쓰는데 상한을 하나는 주입받고 하나는 상수로 박고 하나는 아예 보지 않는다.
두 채널 타입의 렌더 본문은 서로 완전히 같다. 갈린 이유는 전송 경로이지 렌더링이 아니다 — 군집에서 슬롯을 소유한 샤드로만 전달된다. 타입을 나눈 것은 맞고, 두 벌이 된 것은 렌더 규칙 쪽이다.
검증 환경
OpenJDK : 21.0.12 확인 방식 : 세 타입의 생성자와 렌더 대조, 구성 요소 규칙 확인, 배선 탐색, 실행 탐침 소스 수정 : x
재현 조건
- 세 타입의 컴팩트 생성자와 렌더 메서드를 각각 읽는다.
- 렌더 크기 검사가 나오는 곳을 저장소 전체에서 센다. 선언과 호출을 구분한다.
- 채널 구성 요소가 받는 토큰과 식별자 규칙, 그리고 상한 상수를 읽는다.
- 키 렌더러가 조각을 어떤 순서로 잇는지, 상한을 어디서 받는지 읽는다.
- 그 렌더러를 만드는 곳과 설정값을 읽는 곳을 src/main 과 src/test 로 나눠 센다.
- 구성 요소를 최대로 채운 채널과 키를, 그리고 기본 이름공간의 채널과 키를 각각 만들어 길이를 잰다.
- 슬롯 태그를 붙인 키를 만들어 같은 검사가 거부하는지 본다.
- 상한 256 으로 만든 렌더러에 같은 키를 넣고, 같은 크기의 채널과 대조한다.
본문
키 규칙에 조립된 문자열의 렌더 크기를 재는 메서드가 있고, Pub/Sub 쪽 세 타입 중 하나만 그것을 부른다.
부르는 하나와 부르지 않는 둘
:::evidence key="a10-f004-pub-sub" alt="Pub/Sub 세 타입의 컴팩트 생성자와 렌더 메서드, 그중 두 채널 타입의 렌더 본문이 같다는 것. 렌더 크기 검사가 나오는 세 줄 — 선언 하나와 호출 둘. 채널 조각이 받는 토큰·식별자 정규식과 상한 상수, 그 규칙을 부르는 세 타입. 키를 렌더하는 메서드가 조각 사이에 슬롯 태그를 넣는 줄과 그 상한을 생성자로 받는 줄. 그 생성자를 부르는 곳을 src/main 과 src/test 로 나눠 센 수, 설정값을 읽는 src/main 코드, 그리고 기본 이름공간 값을 출력한 터미널 기록." caption="크기 검사를 부르는 곳은 키 렌더러와 패턴 생성자 둘 · 두 채널 타입은 널 검사만 하고 렌더 본문이 동일 · 조각은 토큰 64자와 식별자 128자 규칙을 받고 슬롯 태그도 같은 규칙 · 렌더러 생성은 src/main 0건 src/test 12건이고 max-key-bytes 를 읽는 src/main 코드도 없음 — 89줄 · exit 0" zoom="true" :::
public PubSubPattern {
...
if (suffixPattern.isBlank() || suffixPattern.indexOf(':') >= 0) {
throw new IllegalArgumentException(
"a pattern suffix must be non-blank and must not cross a namespace separator");
}
RedisKeyRules.requireRenderedSize(
namespace.prefix() + ':' + suffixPattern, RedisKeyRules.MAX_KEY_BYTES);
}
렌더 시점이 아니라 생성 시점에 잰다. 두 채널 타입의 생성자에는 널 검사만 있다.
sdk/api/key/RedisKeyRules.java:89: public static String requireRenderedSize(String rendered, int maxKeyBytes) {
sdk/api/key/RedisKeyRenderer.java:45: return RedisKeyRules.requireRenderedSize(rendered.toString(), maxKeyBytes);
sdk/api/operations/PubSubPattern.java:30: RedisKeyRules.requireRenderedSize(
첫 줄은 선언이고 부르는 곳은 아래 둘이다. 시험 소스까지 포함해 이게 전부다.
조각이 받는 규칙
19: public static final int MAX_KEY_BYTES = 512;
21: private static final Pattern TOKEN = Pattern.compile("^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$");
23: private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$");
91: if (maxKeyBytes < 1 || maxKeyBytes > MAX_KEY_BYTES) {
92: throw new IllegalArgumentException("maximum key bytes must be in 1.." + MAX_KEY_BYTES);
RedisNamespace.java:16: RedisKeyRules.requireToken("environment", environment);
RedisNamespace.java:17: RedisKeyRules.requireToken("service", service);
RedisNamespace.java:18: RedisKeyRules.requireToken("domain", domain);
RedisKeyName.java:12: RedisKeyRules.requireToken("entity", entity);
RedisKeyName.java:13: RedisKeyRules.requireIdentifier(identifier);
RedisSlotTag.java:15: RedisKeyRules.requireIdentifier(value);
채널이 잇는 세 조각은 전부 이 규칙을 통과한 값이다. 두 정규식이 ASCII 밖을 받지 않아 바이트 수가 자 수와 같다. 패턴이 잇는 접미는 규칙을 받지 않는다 — 공백이 아닐 것과 구분자를 넘지 않을 것뿐이다.
최대로 채워 보면
:::evidence key="a10-f004-pub-sub-bound" alt="이름공간 세 토큰과 개체 토큰과 식별자를 각 규칙의 최대치로 채워 만든 두 채널 타입의 렌더 길이, 같은 조각으로 만든 키와 거기에 슬롯 태그를 더했을 때의 결과, 설정 기본 이름공간으로 만든 같은 둘의 길이, 접미 길이를 64·317·318 로 바꿔 가며 만든 패턴의 결과, 그리고 상한 256 으로 만든 렌더러가 같은 키와 같은 크기의 채널을 각각 어떻게 처리하는지 출력한 터미널 기록." caption="최대로 채운 채널 렌더는 388 바이트, 기본 이름공간에서는 221 바이트 · 같은 조각에 슬롯 태그를 더한 키는 519 바이트로 거부되지만 기본 이름공간에서는 352 바이트 · 상한 256 렌더러는 388 바이트 키를 거부하고 같은 크기 채널은 검사 자체가 없음 — 28줄 · exit 0" zoom="true" :::
[구성 요소의 상한] 토큰 64, 식별자 128, 슬롯 태그 128
namespace.prefix() 길이 : 194
[채널] 구성 요소를 최대로 채운 렌더
PubSubChannel 렌더 388 바이트 여유 124
ShardedPubSubChannel 렌더 388 바이트 여유 124
이 388 바이트는 이름공간 세 토큰을 모두 64자로 채웠을 때의 값이다. 설정 기본값인 local:sample-service:shared 는 27 바이트라 같은 조각으로 만든 채널이 221 바이트에 그친다.
패턴은 같은 최대 이름공간에서 접미 317자까지 512 바이트로 통과하고 318자에서 넘긴다.
조각이 규칙을 받는다는 말의 한계
원본은 조각이 각자 길이를 제한하므로 현실적인 초과가 어렵다고 봤다. 같은 규칙을 받은 조각들이 상한을 넘는 경우가 같은 패키지에 있다.
rendered.append(key.namespace().prefix()).append(':');
key.slotTag().ifPresent(tag -> rendered.append('{').append(tag.value()).append("}:"));
rendered.append(key.name().entity()).append(':').append(key.name().identifier());
return RedisKeyRules.requireRenderedSize(rendered.toString(), maxKeyBytes);
키 렌더러는 조각 사이에 슬롯 태그를 하나 더 넣는다. 그 태그도 식별자 규칙을 받아 최대 128자다.
[키] 같은 규칙을 받은 조각들, 슬롯 태그 하나가 더 붙는다
태그 없음 -> 렌더 388 바이트
태그 있음 -> rendered key is 519 bytes and exceeds the configured 512
[기본 이름공간] 설정 기본값 local:sample-service:shared
prefix 길이 : 27
채널 렌더 : 221 바이트
태그 붙인 키 : 352 바이트
519 바이트도 이름공간이 194 바이트일 때의 값이다. 기본 이름공간에서는 같은 태그를 붙여도 352 바이트로 통과한다.
상한을 어디서 가져오는가
[상한을 어디서 가져오는가]
RedisKeyRenderer : 생성자 인자 (1..512)
PubSubPattern : RedisKeyRules.MAX_KEY_BYTES 상수
상한 256 렌더러에 키 -> rendered key is 388 bytes and exceeds the configured 256
같은 배포의 채널 388 바이트 -> 검사 없음
키 렌더러만 상한을 주입받는다. 그런데 주입하는 곳이 없다.
src/main 에서 new RedisKeyRenderer( : 0 건
src/test 에서 new RedisKeyRenderer( : 12 건
getMaxKeyBytes / getLimits 를 부르는 src/main 코드
sdk/config/RedisSdkSettings.java:272: public int getMaxKeyBytes() {
sdk/config/RedisSdkSettings.java:908: public Limits getLimits() {
두 줄 다 선언 자신이다. max-key-bytes 는 환경 키 레지스트리에 등록되어 있고 부팅 검증이 1..512 범위까지 보지만, 읽는 코드가 없다.
그래서 이 대비는 지금 배포에서 일어나는 일이 아니다. 그 인자가 설정과 이어지는 날 일어날 일이다.
채널 이름이 렌더러를 지나가는 일도 없다. Pub/Sub 요청은 channel.render() 를 직접 부르고 키 목록으로 빈 리스트를 넘긴다.
남는 것
두 채널 타입은 렌더 본문이 한 글자도 다르지 않다.
public String render() {
return namespace.prefix() + ':' + name.entity() + ':' + name.identifier();
}
갈린 이유는 전송 경로다. 군집에서 슬롯을 소유한 샤드로만 전달된다는 성질이지 렌더링이 아니다. 분리 자체는 옳고, 렌더 규칙만 두 벌이라 한 곳에서 고칠 수 없다.
확인하지 못한 것
상한을 넘는 채널 이름을 브로커에 보내면 어떻게 되는지 확인하지 않았다. 상수 상한을 넘는 이름은 이 타입들로 만들 수 없고, 상한을 낮춘 렌더러로 만든 키는 애초에 나가지 못한다.