빌드노트
Maven package 후에도 삭제한 리소스가 JAR에 남는 이유
소스에서 지운 텍스트 리소스가 target/classes와 JAR에 남는 조건을 별도 Maven 프로젝트로 확인했다. 소스·중간 출력·JAR을 비교하고 clean package 결과로 이전 복사본과 현재 빌드 입력을 구별한다.
src/main/resources에서 텍스트 파일을 지우고 mvn package를 다시 실행했는데, JAR에서는 여전히 그 파일이 읽힐 수 있다. 소스에서 지운 파일의 복사본이 target/classes에 남아 있으면, 다음 JAR에도 포함될 수 있기 때문이다. 먼저 현재 소스, 빌드 출력 디렉터리, 최종 JAR의 목록을 따로 확인한다.
별도 최소 프로젝트에서 이 조건을 재현했다. 삭제 후의 package는 성공했지만 예전 리소스가 남았다. 다른 파일을 바꿔 JAR이 실제로 갱신된 뒤에도 마찬가지였고, clean package로 빌드 출력을 비운 뒤에야 사라졌다.
JAR에 들어가는 파일은 어디서 오는가
Resources Plugin 3.3.1의 resources 목표 (새 탭에서 열림)는 설정된 리소스를 ${project.build.outputDirectory}로 복사한다. JAR Plugin 3.4.2의 classesDirectory (새 탭에서 열림)도 기본값이 같은 디렉터리다. 기본 디렉터리 구성을 쓰면 이 위치가 target/classes다. JAR을 만들 때마다 src/main/resources만 새로 훑어 담는 구조가 아니다.
이번 관찰에서는 소스 목록에서 빠진 파일이 복사 대상에서 제외됐어도, 이미 만들어 둔 출력 파일은 삭제되지 않았다. 이 버전·설정의 리소스 복사를 소스와 출력의 파일 목록까지 일치시키는 동기화로 가정하면 오판한다.
package 앞에 자동으로 clean이 붙는 것도 아니다. Maven의 빌드 생명주기 설명 (새 탭에서 열림)은 default와 clean을 별도 생명주기로 구분한다. package만 호출하면 기존 빌드 출력을 먼저 지우는 단계는 요청하지 않은 것이다.
파일을 지운 뒤에도 예전 내용이 읽혔다
Windows 11, Maven 3.9.16, OpenJDK 17.0.2에서 의존성 없는 일반 JAR 프로젝트를 만들었다. Resources 3.3.1, JAR 3.4.2, Clean 3.4.1, Compiler 3.14.1, Surefire 3.5.4로 플러그인 버전을 고정했다. 필터링이나 추가 리소스 경로는 설정하지 않았고 테스트 소스도 없었다.
입력은 두 파일이었다. src/main/resources/obsolete.txt에는 retired-setting=on, keep.txt에는 generation=1을 넣었고, 둘 다 마지막에 LF 줄바꿈을 붙였다. 같은 프로젝트에서 다음 순서로 실행했다. 네 번의 빌드는 모두 종료 코드 0이었다.
- 두 파일을 둔 채
mvn.cmd package: 소스·target/classes·JAR 모두에obsolete.txt가 있었다. - 소스의
obsolete.txt만 삭제하고mvn.cmd package: 소스에는 없고target/classes와 JAR에는 남았다. keep.txt를generation=2로 바꾸고mvn.cmd package: 새 값이 JAR에 반영됐지만obsolete.txt도 함께 남았다.- 같은 소스에서
mvn.cmd clean package:obsolete.txt가target/classes와 JAR 양쪽에서 사라졌다.
두 번째 실행에서는 JAR 전체의 SHA-256이 첫 번째와 같았고, 로그에도 Building jar:가 새로 나오지 않았다. JAR Plugin은 입력 변경이 없다고 판단하면 생성을 생략할 수 있다. 하지만 그것만으로 세 번째 결과를 설명할 수는 없다. 세 번째 실행에서는 Building jar:가 나왔고, JAR 전체 해시도 65f30102b5b4…에서 f0c866bc4f09…로 바뀌었다. 새 JAR 안에 바뀐 keep.txt와 오래된 obsolete.txt가 함께 들어 있었다. 해시는 이 실험의 산출물을 구별하는 값이지 다른 환경의 기대값은 아니다.
이름만 남은 빈 항목도 아니었다. java -cp target/stale-resource-lab-1.0.jar Probe로 해당 JAR만 클래스패스에 올리고, getResourceAsStream("/obsolete.txt")으로 읽었다. 삭제 후 두 번의 package에서 모두 retired-setting=on과 줄바꿈을 읽었다. 원래 소스, 남은 복사본, JAR 항목의 19바이트 SHA-256은 모두 같았다.
20bb3cb61ed92f6ef27ffe5ab7c3f4576c278d3cbf2245a2ed218aba5f4b37fe
clean package 뒤에는 같은 읽기 코드가 스트림을 얻지 못했고 obsolete.txt=MISSING을 출력했다. 이 비교에서는 내용 변형이 아니라 삭제가 빌드 출력에 반영되지 않은 것이 문제였다.
자기 프로젝트에서는 세 위치를 먼저 비교한다
다음은 JDK의 jar 명령을 쓸 수 있는 Windows PowerShell에서, 문제 JAR을 만든 모듈의 pom.xml 디렉터리에서 실행하는 검사다. $entry는 지운 리소스의 상대 경로, $jarPath는 실제 확인할 최종 JAR로 바꾼다. 아래 파일명은 실험에서 사용한 값이다. JAR 항목 경로는 /로 구분한다.
$ErrorActionPreference = 'Stop'
$entry = 'obsolete.txt'
$jarPath = './target/stale-resource-lab-1.0.jar'
$entries = @(jar tf $jarPath)
if ($LASTEXITCODE -ne 0) { throw 'JAR list failed' }
'source: ' + (Test-Path -LiteralPath "./src/main/resources/$entry" -PathType Leaf)
'classes: ' + (Test-Path -LiteralPath "./target/classes/$entry" -PathType Leaf)
'jar: ' + ($entries -ccontains $entry)
삭제 후 package를 실행한 두 조건에서는 같은 출력이 나왔다.
source: False
classes: True
jar: True
이 조합이면 소스 삭제 여부와 별개로 이전 복사본이 남아 있다는 근거가 된다. 다른 리소스 디렉터리나 생성 단계가 같은 경로에 파일을 쓰는지도 POM에서 함께 확인한다. 출력 경로를 바꾼 프로젝트는 target/classes 대신 실제 outputDirectory를 검사한다. Spring Boot 재패키징 JAR은 이 일반 JAR과 항목 위치가 다를 수 있으므로 jar tf의 실제 목록에서 대응 경로를 찾아야 한다.
반대로 classes: False인데 jar: True라면, 이 실험과 남아 있는 위치가 다르다. 잘못 고른 JAR, 재사용된 이전 JAR, 이후의 재패키징·추가 복사 단계를 확인한다. 둘 다 False인데 실행 중에는 읽힌다면, 실행한 JAR과 클래스패스에 다른 복사본이 있는지 확인한다. 목록을 읽는 데 실패한 경우는 파일이 없다는 결과로 취급하지 않는다.
출력을 비운 뒤 같은 검사를 반복한다
기존 산출물을 비교용으로 보관한 뒤, 빌드를 재현할 수 있는 별도 작업 사본에서 mvn.cmd clean package를 실행한다. POM과 설정, 프로필·속성 인자는 문제를 만든 빌드와 같게 유지한다. 명령이 실패했다면 새 산출물로 판단하지 말고 실패 로그부터 확인한다.
Clean Plugin 3.4.1 문서 (새 탭에서 열림)는 기본 삭제 대상에 빌드 디렉터리와 클래스 출력 디렉터리를 포함한다. 이번 실험에서는 전용 프로젝트의 target을 지운 뒤 현재 남아 있는 keep.txt만 다시 복사했다. 같은 검사 결과는 source: False, classes: False, jar: False였다.
깨끗한 빌드에서만 파일이 사라졌다면, 배포용 JAR을 기존 target에 의존하지 않는 빌드에서 만들고 삭제 예정 경로의 부재도 산출물 검사에 넣을 근거가 된다. JAR을 열어 항목 하나만 수동 삭제하면 남아 있는 target/classes 복사본 때문에 다음 패키징에서 다시 들어갈 수 있다.
깨끗한 빌드에도 남는다면, 현재 빌드가 다른 경로에서 다시 공급하는 파일인지부터 조사한다. clean이 소스 트리 안의 생성 파일까지 모두 지워 준다고 가정하지 않는다. 이번에 검증한 범위는 위 버전의 기본 리소스 경로를 쓰는 일반 JAR 한 개다. 다른 플러그인 버전, 사용자 정의 정리 설정, Spring Boot·Shade 재패키징과 실제 운영 배포는 실행하지 않았다.