--- kind: CASE slug: a06-f018-changestreams-false title: 플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다 topic: assembly-ownership project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a06-f018-changestreams-false evidenceCapturedOn: 2026-09-02 body: case-a06-f018-changestreams-false.body.md assets: - key: a06-f018-changestreams-false file: ../../../final/evidence/rendered/a06-f018-changestreams-false.svg - key: a06-f018-changestreams-false-wiring file: ../../../final/evidence/rendered/a06-f018-changestreams-false-wiring.svg evidence: - ../../../final/evidence/raw/a06-f018-changestreams-false.txt - ../../../final/evidence/raw/a06-f018-changestreams-false-wiring.txt source: - 원본 분석 절은 final/document.md#a06#L1069 이다. 등급은 P2 다. 주석의 전제가 더 이상 사실이 아니라는 판정, 형제 불리언과의 대비표, 검증기 분기가 도달 불가라는 사실, 그리고 실패가 장애 조치 런북으로 분류된다는 서술이 그 절에 있다. - 같은 문서 `#L1015` 는 소비자가 도는 조건을 포크가 다섯을 공급하는 경우로 한정한다. 이 저장소 안에서는 그 다섯의 구현이 전부 시험 픽스처다. - 같은 문서 `#L156` 은 같은 코드를 P3 으로 판정하면서 변경 스트림 실행체가 애초에 출하되지 않는다는 것을 근거로 든다. 이 리비전에서 그 근거가 성립하지 않으므로 그 절에 붙은 "현재 잘못된 동작을 만들지는 않는다"는 유지될 수 없고, 실질 등급은 이 절의 P2 로 흡수된다. - 설정 빈이 속성을 받고도 거짓을 보고한다는 것, 그 두 경우의 빈 집합이 같다는 것, 검사 빈 자체에 조건이 있다는 것, 그리고 런북 전체에 단독 서버·오플로그·해당 오류 코드가 없다는 것은 이 기록에서 확인했다. --- # 플래그는 고정 거짓이라 능력 검사를 끄지만, 조립 조건이 아니라서 소비자 빈은 그대로 생성된다 설정 주석은 드라이버 쪽 구현이 출하되지 않아 빈이 0 이라는 것을 근거로 플래그를 고정 거짓으로 만든다. 이 리비전에서 그 구현에는 빈 선언이 있고, 배포가 다섯을 공급하면 소비자도 선다. 조립 조건 어디에도 그 플래그는 없다. ## 관계 - **거부라고 적힌 처리가 폐기이고, 그 값을 읽는 시작 검사는 켤 방법이 없다** 같은 플래그를 다른 절에서 다룬 기록이다. 거부와 폐기의 차이는 그 기록이 다룬다. - **하위 시스템 전체가 미배선인데 그것을 켜는 플래그는 시작 검사를 수행한다** 형제 불리언 쪽 기록이다. 트랜잭션 계수와 그 결과는 그 기록이 다룬다. - **@Bean이 있다는 것은 조립 증거가 아니다** 플래그와 조립의 관계를 확인하는 규칙이다. ## 문제 시작 검증기에는 인접한 두 분기가 있다. 하나는 트랜잭션이 켜져 있는데 토폴로지가 지원하지 않으면 던지고, 다른 하나는 변경 스트림에 대해 같은 일을 한다. 두 좌항은 같은 설정 타입의 형제 불리언이고 자동 구성의 인접한 두 줄이 넘긴다. 두 불리언은 같은 검증기에서 서로 다르게 끝난다. 앞의 값은 배포가 넣은 대로 도착하고, 뒤의 값은 컴팩트 생성자가 이미 거짓으로 바꾼 뒤다. ## 결론 플래그가 고정 거짓이므로 검증기의 변경 스트림 분기는 실행되지 않는다. 조립은 그와 무관하게 진행된다. 소비자 빈의 조건은 배포가 공급해야 하는 타입 다섯이고, 그 목록에 이 플래그는 없다. 자동 구성 파일 전체에서 그 이름이 나오는 줄은 검증기 인자 하나뿐이다. 전체 자동 구성을 올린 스프링 컨텍스트로 확인했다. 설정 빈은 change-streams=true 를 받고도 거짓을 보고하고, 그 두 경우의 빈 집합이 같다. 모듈 opt-in 을 켜고 리액티브 템플릿이 있으면 드라이버 쪽 구현 빈은 만들어지고 소비자 빈은 만들어지지 않는다. 다섯을 함께 넣으면 소비자 빈도 만들어진다. opt-in 을 켜지 않으면 셋 다 없다. 주석은 이 코드가 있기 전 상태를 서술한다. 빈이 0 이고 스레드가 0 이라는 근거는 이 리비전에서 성립하지 않는다. 남는 것은 능력 검사만 꺼진 상태다. 다만 그 검사가 열리는 조건이 따로 있다. 검사 빈은 토폴로지 프로브를 조건으로 걸고, 보안 프로파일과 관리 자격 참조와 스키마 버전 범위 중 하나라도 없으면 부분 검증 대신 예외로 닫는다. 그리고 검증기는 선언 토폴로지와 실제를 능력 검사보다 먼저 대조한다. 변경 스트림 분기는 그 둘을 통과한 배포에서만 차례를 얻는데, 그 차례가 와도 좌항이 거짓이다. 그 다음 실패는 커서를 여는 시점의 드라이버 오류다. 복구 정책은 서버 코드가 이력 소실이 아니고 재개 가능 라벨도 아니면 실패로 확정하며 장애 조치 런북을 붙인다. 런북 어디를 봐도 그 세 낱말이 없다. 이 연쇄는 형제 기록이 오플로그 없는 서버에서 실행으로 확인했다. 수정은 셋 중 하나다. 플래그를 되살려 조립 조건으로 쓰거나, 소비자 빈이 설 때 능력을 기동에서 확인하거나, 최소한 주석을 현재 사실로 고치는 것이다. ## 검증 환경 OpenJDK : 21.0.12 확인 방식 : 전체 자동 구성을 올린 스프링 컨텍스트에서 빈 집합과 바인딩 값 비교, 코드베이스 정적 검색 소스 수정 : x ## 재현 조건 1. 시작 검증기의 인접한 두 분기와 그 좌항을 넘기는 두 줄을 읽는다. 2. 그 검사를 만드는 빈의 조건과 입력이 빠졌을 때의 처리를 읽는다. 3. 컴팩트 생성자의 플래그 강제와 그 주석을 읽는다. 4. 소비자 빈에 붙은 조건과 자동 구성 파일에서 그 플래그가 나오는 줄 수를 확인한다. 5. MongoPlatformAutoConfiguration 전체와 블로킹·리액티브 템플릿 빈을 등록한 컨텍스트를 ca-skeleton.persistence-mongo.enabled=true 로 띄우고 두 빈과 설정 빈의 유무, 그리고 바인딩된 플래그 값을 읽는다. 6. 조건 다섯을 함께 넣고 같은 것을 읽는다. 7. 두 경우를 change-streams=true 를 넣은 상태에서 반복한다. 8. opt-in 을 켜지 않은 경우와 리액티브 템플릿이 없는 경우를 각각 읽는다. 9. 복구 정책의 분류와 그것이 붙이는 런북 전체를 읽는다. ## 본문 시작 검증기에는 능력을 요구하는 분기가 둘 있고, 두 좌항은 같은 설정 타입의 형제 불리언이다. ## 두 분기가 읽는 값이 오는 자리 :::evidence key="a06-f018-changestreams-false" alt="시작 검증기의 인접한 두 능력 분기와 그 좌항을 넘기는 자동 구성의 두 줄, 그 검사를 만드는 빈의 조건과 입력이 빠졌을 때 닫는 처리, 컴팩트 생성자의 플래그 강제와 그 주석, 자동 구성 파일에서 그 플래그가 나오는 줄 수, 소비자 빈에 붙은 조건 전체, 복구 정책의 분류 메서드, 그리고 그것이 붙이는 런북의 증상 절 전체와 그 런북에서 단독 서버·오플로그·해당 오류 코드가 나오는 줄 수를 출력한 터미널 기록." caption="두 분기의 좌항은 형제 불리언이고 인접한 두 줄이 넘김 · 검사 빈은 토폴로지 프로브 조건이고 입력이 빠지면 닫음 · 변경 스트림은 생성자에서 고정 거짓이고 자동 구성 파일에 그 이름이 나오는 줄은 1 · 소비자 조건은 @ConditionalOnMissingBean 과 타입 다섯 · 실패는 FAILED 와 장애 조치 런북 · 그 런북에 단독 서버·오플로그·40573 은 0줄 — 91줄 · exit 0" zoom="true" ::: ```java if (transactionsEnabled && !capabilities.isStable(MongoCapability.TRANSACTION)) { ... if (changeStreamsEnabled && !capabilities.isStable(MongoCapability.CHANGE_STREAM)) { ``` 자동 구성의 인접한 두 줄이 그 좌항을 넘긴다. ```java properties.transactions(), properties.changeStreams(), ``` 앞의 값은 배포가 넣은 대로 도착한다. 그 값에서 무슨 일이 벌어지는지는 형제 기록이 다룬다. 여기서는 뒤의 값이 이미 거짓이라는 것과, 그런데도 조립은 진행된다는 것만 본다. ## 조립 조건 주석이 근거로 든 것은 빈이 0 이라는 사실이다. ```java // Experimental, and therefore not a switch (MNG-INT-003). The driver-side source — watch, // resumeAfter/startAfter, cursor lifetime, reconnection — is not shipped; what exists is policy // and value objects that do not add up to a running consumer. Accepting the flag and ignoring … ... changeStreams = false; ``` 소비자 빈에 붙은 조건은 `@ConditionalOnMissingBean` 과 타입 다섯의 `@ConditionalOnBean` 이다. ```java @org.springframework.boot.autoconfigure.condition.ConditionalOnBean({ dev.caskeleton.adapter.outbound.mongo.changestream.MongoChangeStreamSubscription.class, dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeCheckpointStore.class, dev.caskeleton.adapter.outbound.mongo.changestream.MongoResumeTokenCodec.class, dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeProjector.class, dev.caskeleton.adapter.outbound.mongo.changestream.projector.MongoChangeDeduplicationStore .class }) ``` 다섯 다 배포가 공급해야 하는 타입이다. 이 플래그는 목록에 없고, 자동 구성 파일 전체에서 그 이름이 나오는 줄은 검증기 인자 하나뿐이다. ## 컨텍스트를 띄운 결과 :::evidence key="a06-f018-changestreams-false-wiring" alt="전체 자동 구성을 등록한 스프링 컨텍스트를 모듈 opt-in 없이, opt-in 과 리액티브 템플릿만으로, opt-in 과 배포가 공급해야 하는 다섯을 함께, 그리고 opt-in 과 리액티브 템플릿 없이 각각 띄워 드라이버 쪽 구현 빈과 소비자 빈과 설정 빈의 유무, 그리고 바인딩된 플래그 값을 change-streams 를 넣지 않은 경우와 넣은 경우에 대해 읽은 터미널 기록." caption="opt-in 없으면 셋 다 없음 · opt-in 과 템플릿이면 드라이버 쪽만 섬 · 다섯을 넣으면 소비자도 섬 · 설정 빈은 change-streams=true 를 받고도 changeStreams()=false · 두 경우의 빈 집합이 같음 — 13줄 · exit 0" zoom="true" ::: `MongoPlatformAutoConfiguration` 전체를 등록하고 블로킹·리액티브 템플릿을 넣어 띄웠다. ```text [opt-in, 리액티브 템플릿만] 기본 드라이버쪽=있음 소비자=없음 설정빈=있음 changeStreams()=false change-streams=true 드라이버쪽=있음 소비자=없음 설정빈=있음 changeStreams()=false [opt-in, 배포가 공급해야 하는 다섯을 함께] 기본 드라이버쪽=있음 소비자=있음 설정빈=있음 changeStreams()=false change-streams=true 드라이버쪽=있음 소비자=있음 설정빈=있음 changeStreams()=false ``` 설정 빈은 컨텍스트에 있고 속성을 받는다. 받고도 거짓을 보고하므로 조립 조건에 닿기 전에 이미 값이 정해져 있다. 소비자가 서는 조건은 다섯을 공급했는지 하나다. 모듈 opt-in 이 없으면 세 빈이 모두 만들어지지 않고, opt-in 이 있어도 리액티브 템플릿이 없으면 두 빈이 만들어지지 않는다. ```text [모듈 opt-in 없이] 기본 드라이버쪽=없음 소비자=없음 설정빈=없음 [opt-in, 리액티브 템플릿 없이] 기본 드라이버쪽=없음 소비자=없음 설정빈=있음 changeStreams()=false ``` ## 능력 검사가 열리는 조건 검사가 꺼진 것과 검사가 애초에 만들어지지 않는 것은 다르다. 검사 빈부터 조건이 있다. ```java @Bean @ConditionalOnBean(MongoTopologyProbe.class) public InitializingBean mongoPlatformStartupCheck( ... if (security == null || admin == null || versions == null) { // Fail closed rather than validate a subset. A partial startup check reports success for // the parts nobody supplied, which is the shape the missing wiring already had. ``` 토폴로지 프로브가 있어야 만들어지고, 보안 프로파일과 관리 자격 참조와 스키마 버전 범위가 다 있어야 돈다. 그리고 검증기는 능력 검사보다 먼저 선언 토폴로지와 실제를 대조한다. 그 둘을 통과한 배포에서만 변경 스트림 분기가 자기 차례를 얻고, 그 차례에서 좌항이 거짓이다. ## 그 다음 실패가 가는 곳 ```java if (failure.hasLabel("ResumableChangeStreamError")) { return MongoChangeStreamRecoveryDecision.resume(); } return MongoChangeStreamRecoveryDecision.halt(MongoChangeStreamState.FAILED, FAILURE_RUNBOOK); ... private static boolean isHistoryLost(int serverCode) { return serverCode == 286 || serverCode == 280; ``` 토폴로지가 복제 셋이 아니라는 오류는 286 도 280 도 아니고 재개 가능 라벨도 없으므로 셋째 갈래다. 붙는 런북의 증상 절은 네 항목이고 전부 프라이머리 선출과 서버 선택 지연이다. ```text - `MongoServerSelectionException` / `MongoConnectionException` spike, then recovery within seconds. - `MongoSdamObservationListener` reports a topology change (primary removed, new primary elected). - `MongoPoolObservationListener` shows checkout wait times rising while server-side command duration stays flat — the wait is topology, not query cost. - Latency spike on writes with no corresponding rise in read latency. ``` 증상 절뿐 아니라 그 런북 전체에서 단독 서버도 오플로그도 해당 오류 코드도 나오지 않는다. 이 연쇄를 실제 서버에서 이은 것은 형제 기록이다. ## 확인하지 못한 것 다섯을 공급한 포크의 배포를 오플로그 없는 토폴로지에 올려 기동 통과와 커서 열기 실패를 이어서 재현하지는 않았다. 그 연쇄는 형제 기록이 단독 서버에서 실행으로 확인했다. 여기서는 조립 조건과 검사가 열리는 조건까지 확인했다.