빌드노트
Maven 빌드는 성공했는데 편집기 JS는 404였다
홈은 열리는데 편집기는 멈췄다. Maven 빌드 앞에서 빠진 Node 단계가 원인이었다. 두 빌드의 차이와 실제 JAR에서 파일을 확인하는 방법.
블로그 배포를 마치고 글을 수정하려는데 본문을 입력할 수 없었다. “문서 편집기를 준비하고 있습니다”라는 문구가 계속 남아 있었고, 저장 버튼도 비활성 상태였다. 홈과 기존 글은 잘 열렸다.
편집 화면이 요청하는 /js/editor.js가 404를 반환하고 있었다. 새로 배포한 JAR에 이 파일이 없었다. Maven으로 JAR을 만들기 전에 실행해야 할 Node 빌드가 빠진 것이 원인이었다.
Java 테스트는 통과했는데 왜 파일이 없었을까
이 프로젝트의 편집기 원본은 frontend/src/editor.js다. Node 빌드가 원본과 의존성을 묶어 src/main/resources/static/js/editor.js를 만든다. Maven은 그 결과물을 Java 코드와 함께 JAR에 넣는다. 이름은 같아도 저장소에서 수정하는 JS와 브라우저가 받아야 하는 JS는 다른 파일이다.
평소 Dockerfile에는 이 순서가 들어 있었다. Node 단계에서 편집기를 빌드하고, 생성된 JS와 라이선스 파일을 Maven 단계로 복사한 뒤 JAR을 만들었다.
이번 배포는 Dockerfile을 처음부터 실행하지 않았다. 별도로 mvn clean verify를 실행하고, 그렇게 만든 JAR로 기존 실행 이미지의 JAR을 교체했다. pom.xml에는 Node 빌드를 호출하는 설정이 없었다.
생성 파일은 .gitignore에 들어 있어 배포용 소스에도 없었다. Node를 실행하지 않으면 Maven이 가져갈 파일 자체가 없는 구조였다. Maven은 정상 종료했고 Java 테스트 733개도 통과했다. 하지만 그 결과가 편집기 JS의 존재까지 보장해 주지는 않았다.
기존 이미지에 정상 파일이 있었던 것도 소용없었다. JS가 들어 있던 JAR을 새 JAR로 통째로 교체했기 때문이다.
같은 소스로 빌드 순서를 비교했다
당시 소스를 새 작업 폴더에 풀고 Maven만으로 패키징했다. 종료 코드는 0이었지만 편집기 JS와 라이선스 파일은 빠져 있었다.
이어서 Node 빌드를 먼저 실행하고 다시 패키징했다. 이번에는 두 파일이 들어갔다. 아래는 두 JAR에 같은 검사 명령을 실행한 출력이다. 장애 당시 HTTP 로그가 아니라, 원인을 확인하기 위해 따로 재현한 결과다.
Maven만 실행한 JAR:
editor.js: False
Node 빌드 후 만든 JAR:
editor.js: True
확인할 때는 생성 파일이 남아 있지 않은 소스로 시작해야 한다. mvn clean은 target을 지우지만 src/main/resources에 남아 있는 예전 JS까지 지우지는 않는다. 그 파일이 남아 있으면 Node 빌드를 생략해도 정상처럼 보일 수 있다.
운영에서는 두 파일을 복원했다
운영 복구 때는 다른 수정 사항을 유지하기 위해 이전 정상 JAR에서 editor.js와 editor.LICENSE.txt만 가져왔다. 두 빌드의 프런트 소스와 lock 파일이 같은지 확인한 뒤, 새 JAR의 복사본에 추가했다. 원래 있던 JAR 항목의 내용이 바뀌지 않았는지도 검사했다.
2026년 10월 20일 10:36 KST의 복구 기록에서 두 주소는 모두 200을 반환했고 파일 해시도 일치했다. CMS 편집기가 열렸고 글 수정과 저장도 확인했다.
여기까지가 운영에 반영된 조치다. Node 빌드와 Maven 패키징을 하나의 배포 경로로 묶는 작업은 남아 있다. 다음 배포에서는 최종 JAR에 두 파일이 없으면 이미지 생성을 중단해야 한다. 홈 화면만 열어 봐서는 편집기가 고장 난 것을 알 수 없었다.
내 배포본에서도 확인하려면
이 사례는 Spring Boot JAR 안에서 JS를 제공하는 구성이다. 개발자 도구의 Network에서 실패한 JS 주소를 확인한 뒤, 실제로 실행 중인 JAR에 대응하는 파일이 있는지 본다. 로컬에서 방금 만든 JAR과 서버의 JAR을 혼동하지 않아야 한다.
Docker를 쓴다면 컨테이너 이름과 JAR 경로를 확인해 docker cp '앱컨테이너:/app/app.jar' ./running.jar로 빈 진단 폴더에 복사할 수 있다. 이름과 경로는 자신의 환경에 맞게 바꾼다. Docker를 쓰지 않으면 Java의 -jar 인자로 지정한 파일을 복사한다. docker cp 사용법 (새 탭에서 열림)
다음은 PowerShell 7과 JDK의 jar 명령을 쓰는 검사다. 이 프로젝트의 편집기 파일 경로를 대상으로 한다.
$jarPath = './running.jar'
$files = @(jar tf $jarPath)
if ($LASTEXITCODE -ne 0) { throw 'JAR을 읽지 못했습니다.' }
'editor.js: ' + ($files -ccontains 'BOOT-INF/classes/static/js/editor.js')False면 JAR을 읽었지만 해당 파일이 없다는 뜻이다. 파일이 있다면 이 사건과 원인이 다를 수 있으므로 재빌드부터 반복할 필요는 없다. 요청 경로와 정적 자원 설정을 확인한다. JAR 안의 위치는 Spring Boot 실행 JAR 구조 (새 탭에서 열림)와 정적 자원 경로 (새 탭에서 열림)에서 확인할 수 있다.
이 저장소에서 파일 누락을 재현할 때 사용한 빌드 순서는 다음과 같다. 프로젝트 루트에서 실행하며, 앞 명령이 실패하면 다음 명령은 실행하지 않는다.
npm.cmd --prefix frontend ci --ignore-scripts &&
npm.cmd --prefix frontend test &&
npm.cmd --prefix frontend run build &&
mvn.cmd -B -q -DskipTests clean package재현 환경은 Windows, OpenJDK 21.0.1, Node.js 22.23.2였고 프런트 테스트 35개가 통과했다. -DskipTests는 파일 포함 여부만 확인한 이 재현의 옵션이다. 정식 배포에서 Java 검사를 생략하라는 뜻은 아니다. --ignore-scripts도 이 저장소의 기존 Dockerfile에 맞춘 값이다.
당시 404 응답의 원문 헤더는 보존하지 못했다. 위 설명은 빌드 경로, 별도 재현 결과와 운영 복구 기록을 대조한 것이다.