본문 바로가기
빌드노트 검색

빌드노트

Maven filtering 뒤 바이너리 파일의 해시가 달라지는 이유

Maven 리소스 필터링은 기본 제외 목록에 없는 바이너리도 문자열 치환 대상으로 삼을 수 있다. 합성 .bin의 21→11바이트 변경과 PNG 보존, 두 수정안의 원본 해시 보존을 비교하고 폴더 분리와 nonFilteredFileExtensions의 선택 기준을 설명한다.

소스의 바이너리 파일은 정상인데 Maven으로 복사한 파일이나 JAR 안의 파일만 해시가 다르다면, 그 파일을 포함하는 <resource>에 <filtering>true</filtering>가 있는지 확인한다. Filtering은 리소스를 그대로 복사하는 데 그치지 않고 ${...} 같은 문자열을 값으로 바꾼다. 바이트를 보존해야 하는 파일을 문자열 치환 대상으로 넣었는지가 먼저 확인할 부분이다.

다만 이미지 확장자를 모두 같은 조건으로 보면 진단이 어긋난다. Maven Resources Plugin의 바이너리 필터링 문서 (새 탭에서 열림)는 jpg, jpeg, gif, bmp, png를 기본 제외 대상으로 명시한다. 아래 별도 실험에서도 PNG는 그대로였고, 기본 제외 목록에 없는 합성 .bin 파일만 바뀌었다. 실제 이미지 장애를 겪었다는 기록이 아니라, 복사 중 바이트가 달라지는 조건을 확인한 결과다.

빌드 성공과 바이트 보존은 다른 결과였다

Windows 11, Maven 3.9.16, JDK 17.0.2에서 새 최소 프로젝트를 만들었다. Resources Plugin은 3.5.0, JAR Plugin은 3.5.1로 고정하고 리소스 복사와 JAR 생성 goal만 실행했다. 문자 인코딩은 UTF-8이었다.

텍스트 config.txt에는 label=${asset.name} 한 줄과 LF 줄바꿈을 넣고, POM의 asset.name 값을 LAB으로 정했다. sample.bin은 직접 만든 작은 바이너리 포맷이다. 앞에 BLOB 네 바이트, 그다음에 본문 길이를 나타내는 4바이트 정수, 마지막에 ${asset.name}의 13바이트를 넣었다. 전체 크기는 21바이트다. 비교용으로 69바이트짜리 1×1 PNG도 생성했다.

같은 세 파일을 네 가지 설정으로 복사하고 각각 JAR에 담았다.

  • 전부 filtering=false: 세 파일 모두 원본과 같았다. 텍스트의 ${asset.name}도 남았다.
  • 전부 filtering=true: 텍스트는 label=LAB으로 바뀌었다. .bin도 21바이트에서 11바이트로 줄었고, PNG는 그대로였다.
  • filtering=true에 bin 확장자 제외 추가: 텍스트만 바뀌고 .bin과 PNG는 원본과 같았다.
  • 텍스트만 별도 필터링 폴더에 배치: 텍스트만 바뀌고 .bin과 PNG는 원본과 같았다.

전체 필터링 조건의 바이너리 SHA-256은 다음과 같았다.

src/main/resources/sample.bin   21 bytes
5fa9ac48a96eeab905b4837e0d3321f9349bb252e5c58b3f3a4ca014527007b3
target/classes/sample.bin      11 bytes
41a03693d0a94d7edac8e739b5d122fc9dca092ecf22de899f8f55feeed7ae13

변경된 파일을 읽어 보니 ${asset.name} 자리가 LAB으로 치환돼 있었다. 길이 필드는 여전히 13을 가리키지만 실제 본문은 3바이트였다. 이 포맷의 길이 검사에는 실패했는데 Maven 로그에는 BUILD SUCCESS가 남았다. 입력 바이트가 UTF-8로 읽힐 수 있도록 만든 예제여서, 문자 읽기 오류 없이 바이너리 구조만 깨지는 경우를 관찰할 수 있었다.

모든 조건에서 target/classes의 파일과 JAR에서 읽은 같은 항목의 내용은 일치했다. 여기서 비교한 것은 JAR 전체 해시나 압축된 바이트가 아니라 압축을 푼 개별 항목의 바이트다. 이 실험의 변경 지점은 JAR 압축 단계에 앞선 리소스 복사였다. resources goal (새 탭에서 열림)은 설정된 리소스를 출력 디렉터리로 복사하고, jar goal (새 탭에서 열림)은 기본적으로 그 출력 디렉터리의 파일을 담는다.

내 파일도 복사 단계에서 바뀌는지 확인한다

먼저 자신의 POM과 적용 중인 프로필에서 문제 파일을 포함하는 <resources> 설정을 찾는다. 기본 출력 위치를 쓰는 프로젝트라면 소스 파일과 target/classes의 대응 파일을 비교할 수 있다. 다음은 Maven과 JDK가 설치된 Windows의 PowerShell 7에서, 해당 POM이 있는 디렉터리에서 실행하는 명령이다. 경로는 자신의 문제 파일에 맞게 바꾼다.

아래 명령은 실험과 같은 Resources Plugin 3.5.0을 명시한다. 자기 빌드의 문제를 진단할 때는 실제 사용 중인 버전으로 맞춘다. 이 goal은 리소스 출력 파일을 다시 만들 수 있으므로 생성물을 보관해야 한다면 먼저 복사해 둔다.

$ErrorActionPreference = 'Stop'
mvn.cmd -B -ntp org.apache.maven.plugins:maven-resources-plugin:3.5.0:resources
if ($LASTEXITCODE -ne 0) { throw '리소스 복사가 실패했습니다.' }
Get-FileHash -Algorithm SHA256 -LiteralPath 'src/main/resources/sample.bin', 'target/classes/sample.bin'

위의 전체 필터링 조건에서는 두 줄의 해시가 각각 5FA9AC48…, 41A03693…로 달랐다. 실제 파일도 이 단계에서 달라지면, JAR을 만들기 전부터 복사 설정을 조사할 근거가 생긴다. 파일이 없으면 해시 불일치와는 다른 문제다. includes, excludes, 리소스 디렉터리와 출력 경로부터 확인한다.

반대로 소스와 복사본은 같은데 최종 JAR의 해당 항목만 다르면, 이 실험과 변경 지점이 다르다. 이후 실행되는 복사·가공·재패키징 단계와 비교 대상 JAR이 맞는지 확인한다. 파일이 바뀐 단계가 아직 밝혀지지 않았는데 인코딩 값부터 바꿔서는 원인을 가리기 쉽다.

치환할 텍스트만 별도 폴더에 둔다

필터링이 필요한 설정 파일이 일부라면 폴더를 나누는 편이 관리하기 쉽다. 바이너리와 그대로 복사할 파일은 src/main/resources에 두고, 치환할 텍스트만 src/main/resources-filtered로 옮긴다. 공식 필터링 예제 (새 탭에서 열림)도 텍스트와 바이너리가 섞였을 때 이 구성을 권한다.

기존 <build> 안의 리소스 목록을 다음처럼 구성한다. 같은 디렉터리를 다시 필터링하는 기존 항목이 남지 않도록 목록 전체를 함께 확인한다.

<resources>
  <resource>
    <directory>src/main/resources</directory>
    <filtering>false</filtering>
  </resource>
  <resource>
    <directory>src/main/resources-filtered</directory>
    <filtering>true</filtering>
  </resource>
</resources>

실험에서는 config.txt만 옮겼다. 복사 결과는 target/classes/config.txt, 바이너리는 target/classes/sample.bin이었다. 소스 폴더 이름인 resources-filtered가 출력 경로 앞에 붙지는 않았다. 실제 프로젝트에서도 폴더 아래의 상대 경로를 유지하고, 두 소스 폴더에 같은 상대 경로의 파일을 중복해서 두지 않는다.

이 구성에서는 새 바이너리를 필터링하지 않는 폴더에 넣으면 된다. 확장자가 없거나 새로운 형식이 계속 들어오는 프로젝트라면, 제외 확장자 목록을 매번 보완하는 것보다 치환할 파일의 위치를 명시하는 쪽을 선택할 수 있다.

폴더를 유지한다면 확장자를 명시적으로 제외한다

폴더를 옮기기 어렵고 바이너리 확장자가 일정하다면 nonFilteredFileExtensions를 추가하는 방법도 있다. 아래는 <build><plugins> 안의 Resources Plugin 설정이다. 이미 선언이 있다면 그 선언에 설정을 합친다.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>3.5.0</version>
  <configuration>
    <nonFilteredFileExtensions>
      <nonFilteredFileExtension>bin</nonFilteredFileExtension>
    </nonFilteredFileExtensions>
  </configuration>
</plugin>

값은 .bin이나 *.bin이 아니라 bin이다. 이 설정은 파일을 복사 대상에서 빼는 excludes와 다르다. 실험에서는 sample.bin을 원래 출력 경로에 복사하면서 치환만 건너뛰었고, 텍스트의 치환은 계속 이뤄졌다. 기본 PNG 제외도 유지됐다. 이 결과는 추가 제외 확장자 설정 (새 탭에서 열림)의 설명과 일치한다.

치환할 파일이 소수이고 바이너리 종류가 늘어난다면 폴더를 분리한다. 기존 디렉터리를 유지해야 하고 모든 제외 확장자를 관리할 수 있다면 확장자 설정을 쓴다. 수정 뒤에는 텍스트의 치환 결과와 바이너리의 원본 해시 보존을 함께 확인한다. 전부 filtering=false로 돌리면 바이너리는 보존해도 필요한 설정 치환까지 사라질 수 있다.

이번 관찰은 합성 바이너리의 길이 검사와 파일·JAR 항목의 바이트 비교까지다. 실제 이미지 뷰어의 오류, 다른 바이너리 형식, Spring Boot 재패키징은 검증하지 않았다. 특히 기본 설정의 .png가 바뀌는 문제라면 이 .bin 결과를 그대로 원인으로 삼지 말고, 실제 플러그인 버전과 그 파일을 다시 쓰는 다른 단계를 확인해야 한다.

← 전체 글 보기