Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/caching-and-redis/case/case-a10-f006-requireidentifier.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

14 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-f006-requireidentifier 지워도 test가 초록인 검사가 셋이다 caching-and-redis clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a10-f006-requireidentifier 2026-09-02 case-a10-f006-requireidentifier.body.md
key file
a10-f006-requireidentifier ../../../final/evidence/rendered/a10-f006-requireidentifier.svg
key file
a10-f006-requireidentifier-branch ../../../final/evidence/rendered/a10-f006-requireidentifier-branch.svg
../../../final/evidence/raw/a10-f006-requireidentifier.txt
../../../final/evidence/raw/a10-f006-requireidentifier-branch.txt
원본 분석 절은 analysis/10-adapter-outbound-cache-redis.md#L375 이다. 등급은 P3 이다. 문자 클래스가 메일과 전화 형태의 필수 문자를 이미 배제하므로 두 분기가 도달 불가라는 관찰, 대응 test 가 예외 타입만 보므로 그 사실을 가리지 않는다는 지적, 보안 효과는 그대로이고 잃는 것은 진단 품질과 검증 겹수의 착시라는 판단, 그리고 두 분기를 지우거나 검사 순서를 뒤집으라는 제안이 그 절에 있다.
이 기록이 더한 것은 셋이다. 두 입력에 실제로 돌아오는 메시지가 문자 클래스 메시지라는 실행 결과. 웹 토큰 분기는 도달하지만 그것을 겨냥한 test 입력이 접두 검사에도 걸려 지워도 초록이고, 혼자 잡는 것은 실제 토큰이 아니라 합성 값이라는 것. 그리고 네 메시지를 단언하는 곳이 저장소에 하나도 없다는 것이다. 원본이 도달 불가로 센 것은 둘이고, 지워도 test 가 초록인 것은 셋이다.

지워도 test가 초록인 검사가 셋이다

식별자 검증이 다섯 겹으로 보인다. 그중 둘은 앞선 문자 클래스 검사가 이미 걸러내 도달하지 않고, 하나는 도달하지만 그것을 겨냥한 test 입력이 뒤 검사에도 걸린다. 셋 다 지워도 test 는 초록이다.

관계

  • 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다 두 사례 모두 조건이 성립할 수 없어 그 분기가 실행되지 않는다.
  • 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다 예외 타입만 보는 단언은 어느 검사가 던졌는지 구분하지 않는다.
  • 상한을 주입받는 자리는 있고 주입하는 곳은 없다 같은 리프의 다른 검증 사례다.

문제

식별자 검증에 검사가 다섯 있다. 문자 클래스 하나와 구체적 형태 넷이다. 메일 주소, 웹 토큰, 국제 전화번호, 인증 재료 접두다.

각 검사가 실제로 발화하는지, 그리고 발화한다면 어느 test 가 그것을 붙들고 있는지 확인했다.

결론

문자 클래스는 첫 문자로 영숫자를 요구하고 이후 문자로 영숫자와 점과 물결과 밑줄과 붙임표를 허용한다. 길이는 128 이하다.

거기에 골뱅이도 더하기도 없다.

그래서 메일 분기는 도달하지 않는다. 골뱅이를 포함한 값은 첫 검사에서 탈락한다. 전화 분기도 도달하지 않는다. 국제 전화번호 패턴이 반드시 더하기로 시작하는데 더하기는 첫 문자로도 이후 문자로도 허용되지 않는다.

실행으로 확인했다. 두 형태를 넣으면 돌아오는 메시지가 문자 클래스 메시지다. 무작위 입력 20만 개에서도 이 두 검사에서 갈린 값이 하나도 없다.

웹 토큰 분기는 도달한다. 웹 토큰 패턴이 쓰는 문자를 문자 클래스가 모두 허용하므로, 26자에서 128자 사이이고 영숫자로 시작하는 토큰 형태는 첫 검사를 통과해 전용 검사에 닿는다.

다만 그것이 잡는 것은 진짜 토큰이 아니다. 진짜 토큰은 늘 같은 세 글자로 시작하고 길이도 128자를 넘긴다. 그런 값은 접두 검사나 문자 클래스가 먼저 잡는다. 혼자 걸리는 값은 토큰 흉내를 낸 합성 문자열뿐이다.

그리고 그것을 겨냥한 test 입력 하나가 다음 검사에도 걸린다. 그 값이 eyJ 로 시작해서 접두 검사가 같은 타입의 예외를 낸다. 웹 토큰 분기를 지워도 그 test 는 초록이다.

남는 둘은 붙들려 있다. 문자 클래스 검사는 128자 초과 입력과 구분자 주입을 거부하는 test 가 붙들고, 접두 검사는 웹 토큰 형태가 아닌 두 입력이 붙든다. 둘 중 하나를 지우면 대응 test 가 빨개진다.

저 네 메시지는 저장소에서 던지는 자리에만 있다. 단언하는 곳이 없다.

판정은 P3 이다.

거부는 그대로다. 세 형태는 여전히 전부 거부된다.

잃는 것은 둘이다. 운영자가 받는 메시지가 구체 형태에서 문자 클래스로 내려앉는다. 그리고 세 분기가 test 에 붙들리지 않은 채 검증이 다섯 겹인 것처럼 보이게 만든다.

원본은 두 분기를 지우고 문자 클래스 메시지에 그 의도를 포함시키거나, 검사 순서를 뒤집어 구체적 형태를 먼저 판정하는 수정을 제안했다. 뒤집는 쪽을 고르면 세 분기가 전부 발화한다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 문자 클래스와 후속 분기 대조, 실행 탐침, test 단언 대상 확인 소스 수정 : x

재현 조건

  1. 식별자 검증의 다섯 검사를 순서대로 읽는다.
  2. 각 검사가 쓰는 패턴 넷을 읽는다.
  3. 각 형태 검사가 요구하는 필수 문자가 문자 클래스 안에 있는지 본다.
  4. 이 메서드에 값을 넣는 test 를 모아 무엇을 단언하는지 확인한다.
  5. 그 입력들을 그대로 넣고 실제로 돌아오는 메시지를 본다.
  6. 각 입력이 다섯 중 어느 검사에 걸리는지 전부 표시한다.
  7. 웹 토큰 분기에만 걸리는 값과, 실제 크기의 토큰을 각각 넣어 본다.
  8. 무작위 입력으로 각 검사에서 갈린 수를 센다.
  9. 저장소 전체에서 네 메시지가 나오는 곳을 센다.

본문

검사는 아래 순서로 돈다.

다섯 검사

:::evidence key="a10-f006-requireidentifier" alt="식별자 검증 메서드의 다섯 검사 전체와 그 검사들이 쓰는 정규식 넷, 이 메서드에 값을 넣는 test 다섯 개가 무엇을 단언하는지, 그리고 저장소 전체에서 네 예외 메시지가 나오는 곳을 파일 형식 제한 없이 센 터미널 기록." caption="검사 다섯은 문자 클래스·메일·웹 토큰·전화·접두 순 · test 다섯 개의 단언은 모두 예외 타입뿐 · 네 메시지는 던지는 자리 넷에만 있고 단언하는 곳이 없다 — 78줄 · exit 0" zoom="true" :::

if (value == null || !IDENTIFIER.matcher(value).matches()) {
  throw new IllegalArgumentException(
      "identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a"
          + " key separator");
}
String lowerCase = value.toLowerCase(Locale.ROOT);
if (value.indexOf('@') >= 0) {
  throw new IllegalArgumentException("identifier must not contain a mail address");
}
if (JSON_WEB_TOKEN.matcher(value).matches()) {
  throw new IllegalArgumentException("identifier must not contain a JSON web token");
}
if (INTERNATIONAL_PHONE.matcher(value).matches()) {
  throw new IllegalArgumentException("identifier must not contain a phone number");
}
if (lowerCase.startsWith("bearer") || lowerCase.startsWith("eyj")) {
  throw new IllegalArgumentException("identifier must not contain authentication material");
}

첫 검사가 쓰는 문자 클래스에는 골뱅이도 더하기도 없다.

  private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$");

  private static final Pattern JSON_WEB_TOKEN =
      Pattern.compile("^[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}$");

  private static final Pattern INTERNATIONAL_PHONE = Pattern.compile("^\\+\\d[\\d.~-]{7,}$");

넣어 보면 어느 메시지가 오는가

:::evidence key="a10-f006-requireidentifier-branch" alt="test 가 쓰는 여섯 입력을 실제 검증 메서드에 넣어 돌아온 메시지와, 각 입력이 다섯 검사 중 어디에 걸리는지 전부 표시한 표. 웹 토큰 분기에만 걸리는 합성 값, 밑줄로 시작하는 값, 실제 크기의 토큰. 그리고 무작위 입력 20만 개로 옮겨 적은 정규식과 실제 메시지를 대조하고 각 검사에서 갈린 수를 함께 센 터미널 기록." caption="메일·전화 입력이 받는 메시지는 문자 클래스 메시지 · 웹 토큰 test 입력은 접두 검사에도 걸리고 실제 크기 토큰은 문자 클래스에서 먼저 걸린다 · 무작위 20만 개에서 메일·전화 검사에서 갈린 입력은 0 — 27줄 · exit 0" zoom="true" :::

입력             길이 걸리는 검사 전부                      | requireIdentifier 가 낸 메시지
test: 메일         18   문자클래스 메일           | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator
test: 전화         13   문자클래스        전화    | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator
test: eyj        20                   접두 | identifier must not contain authentication material
test: bearer     19                   접두 | identifier must not contain authentication material
test: JWT        57            JWT    접두 | identifier must not contain a JSON web token
test: 129자       129  문자클래스              | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator

메일과 전화 입력은 전용 검사 앞에서 이미 걸린다. 문자 클래스가 골뱅이와 더하기를 빼 놓았으니 그 둘에 도달할 값 자체가 없다.

웹 토큰 입력은 전용 검사에 닿는다. 다만 같은 값이 다음 검사에도 걸린다 — eyJ 로 시작하기 때문이다.

웹 토큰 분기가 혼자 잡는 것

합성 JWT 26자       26            JWT       | identifier must not contain a JSON web token
_로 시작            26   문자클래스    JWT       | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator
실제 크기 JWT        238  문자클래스    JWT    접두 | identifier must be 1..128 characters of [A-Za-z0-9._~-] and must not contain a key separator

혼자 걸리려면 26자에서 128자 사이이고 영숫자로 시작하며 eyJ 로도 bearer 로도 시작하지 않아야 한다. 실제 토큰은 헤더가 {" 로 시작해 base64url 로 늘 eyJ 가 되고, 길이도 128자를 넘기 일쑤다. 그러니 이 분기가 혼자 잡는 것은 토큰을 닮은 합성 값이다.

어느 검사를 지우면 test 가 빨개지는가

웹 토큰 분기는 일을 하지만 붙들고 있는 test 가 없다. 저 test 입력은 분기를 지워도 접두 검사가 같은 타입의 예외를 낸다.

문자 클래스 검사와 접두 검사는 다르다. 129자 입력과 1:2 는 문자 클래스 검사가 없으면 아무 검사에도 안 걸리고, eyJhbGciOiJIUzI1NiJ9bearer-abcdefabcdef 는 접두 검사 말고 걸리는 데가 없다.

왜 test 가 이것을 못 잡는가

assertThatThrownBy(() -> new RedisKeyName("user", "person@example.com"))
    .isInstanceOf(IllegalArgumentException.class);

단언 대상이 예외 타입뿐이다. 어느 검사가 던졌는지 보지 않는다.

    sdk/api/key/RedisKeyRules.java:67:      throw new IllegalArgumentException("identifier must not contain a mail address");
    sdk/api/key/RedisKeyRules.java:70:      throw new IllegalArgumentException("identifier must not contain a JSON web token");
    sdk/api/key/RedisKeyRules.java:73:      throw new IllegalArgumentException("identifier must not contain a phone number");
    sdk/api/key/RedisKeyRules.java:76:      throw new IllegalArgumentException("identifier must not contain authentication material");

저장소 전체를 파일 형식 제한 없이 훑어도 네 메시지가 나오는 곳은 던지는 자리 넷뿐이다.

옮겨 적은 정규식을 믿어도 되는가

표의 「걸리는 검사」 열은 소스에서 옮겨 적은 정규식으로 계산한다. 그 사본이 실제와 같은지 무작위 입력으로 대조했다.

  200000 / 200000 일치
    문자클래스      에서 갈린 입력 144166
    메일         에서 갈린 입력 0
    JWT        에서 갈린 입력 23462
    전화         에서 갈린 입력 0
    접두         에서 갈린 입력 30843
    통과         1529

문자 클래스와 웹 토큰과 접두는 각각 수만 번씩 갈렸다. 메일과 전화는 0 인데, 그게 이 기록의 결론이다. 도달할 값이 없으니 대조할 방법도 없다.

잃는 것

거부는 그대로다. 세 형태 모두 거부된다.

운영자가 보는 메시지가 달라진다. 메일 주소를 담지 말라는 문장 대신 문자 클래스 문장이 온다.

그리고 세 분기가 검증을 다섯 겹처럼 보이게 만든다.

확인하지 못한 것

세 분기를 실제로 지운 빌드로 전체 test 를 돌리지 않았다. 소스를 고치지 않는 것이 이 작업의 조건이다. 대신 각 입력이 걸리는 검사를 전부 표시해, 지운 뒤에도 같은 타입의 예외를 낼 검사가 남는지 확인했다.

분기와 함께 쓰이지 않게 되는 패턴 필드까지 지운 빌드가 경고 없이 컴파일되는지도 확인하지 못했다.