com.github.f4b6a3:ulid-creator:5.2.3 → uuid-creator:6.1.1 로 build.gradle 2곳을 바꾸고 cd src && ./gradlew resolveAndLockAll --write-locks (BUILD SUCCESSFUL) 실행 후에도, app-bootstrap/gradle.lockfile 에 낡은 줄이 남음:
즉 uuid-creator 는 resolvable config 들에 잡혔지만 sampleFixture config 태그는 여전히 ulid-creator 를 가리킴.
발생 컨텍스트: 의존 교체 후 STRICT lock 재생성. 발생 시점: 2026-07-08. 재현 가능 여부: always — non-resolvable config 를 통과하는 의존이 버전 변경될 때마다.
재현 절차 / Reproduction
app-bootstrap/build.gradle 처럼 configurations { sampleFixture { canBeConsumed=false; canBeResolved=false } } 를 두고 testCompileClasspath.extendsFrom sampleFixture 로 확장.
sampleFixture project(':sample-portfolio') 가 끌어오는 전이 의존의 버전을 바꿈(여기선 sample-portfolio 의 ulid-creator→uuid-creator).
cd src && ./gradlew resolveAndLockAll --write-locks 실행.
기대: 모든 lock 태그가 새 좌표로 갱신. 실제: =sampleFixture 로만 태그된 낡은 좌표가 lockfile 에 잔존.
근본 원인 / Root cause
직접 원인: resolveAndLockAll 태스크 본문이 configurations.findAll { it.canBeResolved }.each { it.resolve() } — canBeResolved = false 인 sampleFixture 는 필터에서 제외되어 직접 resolve 되지 않음. --write-locks 는 그 실행에서 resolve 된 config 의 lock 항목만 다시 씀. resolve 안 된 config 의 기존 항목은 삭제/갱신되지 않고 보존된다.
근본 원인: STRICT lock 이 실패하지 않는 이유 — sampleFixture 는 어떤 빌드에서도 직접 resolve 되지 않으므로 그 태그의 lock 항목은 검증되지 않는다(확장 대상인 testRuntimeClasspath 등은 새 uuid-creator 로 올바르게 검증됨). 그래서 조용히 통과하지만, 커밋되는 lockfile 에 사라진 의존(ulid-creator)이 남아 "ULID 완전 제거" 계약을 위반.
해결 / Resolution
적용한 조치: lockfile 수동 병합 — 낡은 ulid-creator:5.2.3=sampleFixture 줄을 삭제하고, uuid-creator:6.1.1 줄의 config 목록에 sampleFixture 를 알파벳 위치(runtimeClasspath 다음, sampleOffTestRuntimeClasspath 앞)에 삽입:
(sample-portfolio→uuid-creator 이므로 sampleFixture 가 uuid-creator 를 포함하는 것이 올바른 상태. resolvable config 목록은 건드리지 않아 누락 위험 없음.)
검증 방법: cd src && ./gradlew check (내부에서 verifyDependencyLocks STRICT 재해석) BUILD SUCCESSFUL — lockfile 일관성 확인. 전 lockfile grep 으로 ulid-creator 0건 확인.
잔여 위험 / 후속: 대안 = sampleFixture { canBeResolved = true } 로 일시 전환 후 resolveAndLockAll 재실행하고 원복. 수동 편집보다 재현성은 높으나 build.gradle 변경 위험이 있어 이번엔 타깃 lock 편집을 택함.
회고 / Lessons
빨리 감지하는 신호: 전이 의존 버전을 바꾼 뒤 모든 *.lockfile 에서 OLD 좌표를 grep 한다 — resolveAndLockAll 성공 로그만 믿지 않는다.
예방 체크리스트: canBeResolved=false 인 aggregation/bucket config(예: sampleFixture)는 resolveAndLockAll 의 findAll { it.canBeResolved } 필터에서 빠진다 → 그 태그의 lock 항목은 자동 갱신 안 됨. 버킷을 확장하는 resolvable config 는 갱신되지만 버킷 태그 자체는 stale 로 남는다.
일반화된 교훈: Gradle STRICT locking 에서 "빌드가 통과한다 ≠ lockfile 이 깨끗하다". non-resolvable config 의 lock 항목은 검증 사각지대라, 의존 삭제/교체 시 수동 대조가 필요하다.