Spring Boot 애플리케이션 실행 시 JAR 스캔 경고 문제 분석
문제 상황
Spring Boot 애플리케이션을 gradle bootRun으로 실행하던 중 다음과 같은 경고 로그가 발생했다.
2025-12-08 19:07:45.775 # WARN 40868 --- [main] o.a.t.u.s.StandardJarScanner:
Failed to scan [file:/C:/Users/USER/.m2/repository/com/sun/xml/ws/jaxws-rt/2.3.1/javax.annotation-api.jar] from classloader hierarchy
java.io.IOException: java.lang.reflect.InvocationTargetException
at org.apache.tomcat.util.compat.Jre9Compat.jarFileNewInstance(Jre9Compat.java:209)
at org.apache.tomcat.util.scan.JarFileUrlJar.<init>(JarFileUrlJar.java:65)
at org.apache.tomcat.util.scan.StandardJarScanner.process(StandardJarScanner.java:387)
...
Caused by: java.nio.file.NoSuchFileException:
C:\Users\USER\.m2\repository\com\sun\xml\ws\jaxws-rt\2.3.1\javax.annotation-api.jar
같은 패턴으로 javax.jws-api.jar, jaxb-api.jar 파일도 찾을 수 없다는 경고가 함께 출력됐다.
처음에는 Maven 로컬 저장소가 손상됐거나, 의존성 다운로드가 실패한 줄 알았다. 그래서 해당 디렉토리를 삭제하고 다시 빌드해봤지만 동일한 경고가 계속 발생했다.
원인 분석
문제의 핵심은 JAR 파일이 없는 게 아니라, 잘못된 경로에서 찾고 있다는 것이었다.
로그를 자세히 보면 Tomcat의 StandardJarScanner가 jaxws-rt-2.3.1 디렉토리 안에서 javax.annotation-api.jar를 찾고 있다. 하지만 실제로 이 파일은 Maven 저장소의 완전히 다른 좌표에 존재한다.
# Tomcat이 찾는 경로 (잘못됨)
C:\Users\USER\.m2\repository\com\sun\xml\ws\jaxws-rt\2.3.1\javax.annotation-api.jar
# 실제 파일이 있는 경로
C:\Users\USER\.m2\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar
왜 이런 일이 발생하는 걸까?
MANIFEST.MF의 Class-Path 속성
jaxws-rt-2.3.1.jar 파일의 MANIFEST.MF를 열어보면 다음과 같은 내용이 있다.
Manifest-Version: 1.0
Class-Path: javax.annotation-api.jar javax.jws-api.jar jaxb-api.jar
jaxb-core.jar jaxb-impl.jar stax-ex.jar ...
J2EE 스펙 기반 라이브러리들은 이렇게 Class-Path 속성에 상대 경로로 의존 라이브러리를 명시한다. 이 방식은 전통적인 Java EE 애플리케이션 서버 환경에서는 정상 동작한다. WAS가 공통 라이브러리들을 같은 디렉토리에 배치하기 때문이다.
하지만 gradle bootRun으로 실행하면 상황이 달라진다.
gradle bootRun의 클래스 로딩 방식
bootRun 태스크는 Maven 로컬 저장소에 있는 JAR 파일들을 개별적으로 클래스패스에 추가한다.
클래스패스 구성:
- C:\Users\USER\.m2\repository\com\sun\xml\ws\jaxws-rt\2.3.1\jaxws-rt-2.3.1.jar
- C:\Users\USER\.m2\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar
- C:\Users\USER\.m2\repository\javax\jws\javax.jws-api\1.1\javax.jws-api-1.1.jar
- ...
각 JAR는 자신의 Maven 좌표에 해당하는 디렉토리에 그대로 있다. 이 상태에서 Tomcat의 StandardJarScanner가 jaxws-rt-2.3.1.jar의 MANIFEST.MF를 읽고, Class-Path에 명시된 javax.annotation-api.jar를 동일 디렉토리에서 찾으려고 시도한다.
당연히 그 위치에는 파일이 없으니 NoSuchFileException이 발생하는 것이다.
gradle bootRun 실행 시 클래스 로딩 구조:
Application ClassLoader
└── jaxws-rt-2.3.1.jar (Maven 저장소 경로)
└── MANIFEST.MF Class-Path 해석
└── javax.annotation-api.jar를 동일 디렉토리에서 탐색
└── 파일 없음 → NoSuchFileException (WARN)
java -jar 실행 시에는 왜 문제가 없을까
신기하게도 gradle bootJar로 빌드한 후 java -jar로 실행하면 이 경고가 발생하지 않는다.
Spring Boot의 실행 가능한 JAR(Executable JAR) 내부 구조를 보면 이유를 알 수 있다.
application.jar
├── BOOT-INF/
│ ├── classes/
│ │ └── (애플리케이션 코드)
│ └── lib/
│ ├── jaxws-rt-2.3.1.jar
│ ├── javax.annotation-api-1.3.2.jar
│ ├── javax.jws-api-1.1.jar
│ ├── jaxb-api-2.3.1.jar
│ └── (모든 의존성 라이브러리)
├── META-INF/
│ └── MANIFEST.MF
└── org/springframework/boot/loader/
└── (Spring Boot 로더 클래스들)
Spring Boot는 패키징 과정에서 모든 의존성 라이브러리를 BOOT-INF/lib/ 디렉토리에 평탄화(flatten) 해서 배치한다. Maven 저장소의 복잡한 디렉토리 구조가 사라지고, 모든 JAR가 동일한 계층에 나란히 놓이게 된다.
그리고 java -jar로 실행하면 Spring Boot의 LaunchedURLClassLoader가 동작한다. 이 클래스로더는 개별 JAR의 MANIFEST.MF에 있는 Class-Path 속성을 무시하고, BOOT-INF/lib/ 전체를 하나의 클래스패스로 관리한다.
java -jar 실행 시 클래스 로딩 구조:
LaunchedURLClassLoader (Spring Boot 전용)
└── BOOT-INF/lib/ 전체를 단일 클래스패스로 로딩
├── jaxws-rt-2.3.1.jar
├── javax.annotation-api-1.3.2.jar
├── javax.jws-api-1.1.jar
└── ... (MANIFEST Class-Path 무시)
실행 방식별 비교
| 구분 | gradle bootRun | java -jar |
|---|---|---|
| JAR 파일 위치 | Maven 저장소 (분산된 디렉토리) | BOOT-INF/lib (동일 계층) |
| ClassLoader | 표준 Application ClassLoader | LaunchedURLClassLoader |
| MANIFEST Class-Path | 해석하고 상대경로로 탐색 | 무시됨 |
| J2EE 라이브러리 호환성 | 경고 발생 가능 | 문제없음 |
대응 전략
이 경고는 WARN 레벨이라 애플리케이션 실행 자체에는 영향을 주지 않는다. 실제로 필요한 클래스들은 Maven 의존성으로 이미 클래스패스에 포함되어 있기 때문이다. 하지만 상황에 따라 다양한 대응 방법을 선택할 수 있다.
전략 1: 무시하기 (권장)
적용 상황: 개발 환경에서만 발생하고, 운영 환경에서는 java -jar로 실행하는 경우
개발 환경에서 bootRun으로 실행할 때만 발생하는 경고이므로, 원인을 이해했다면 그냥 두어도 된다. 운영 환경에서는 bootJar로 패키징한 결과물을 사용하므로 문제없다.
# 개발 환경 - 경고 발생하지만 동작에 문제없음
./gradlew bootRun
# 운영 환경 - 경고 없음
./gradlew bootJar
java -jar build/libs/application.jar
전략 2: 로그 레벨 조정
적용 상황: 경고 로그가 너무 많아서 다른 로그를 보기 어려운 경우
application.yml 또는 logback-spring.xml에서 해당 로거의 레벨을 ERROR로 올린다.
# application.yml
logging:
level:
org.apache.tomcat.util.scan.StandardJarScanner: ERROR
<!-- logback-spring.xml -->
<logger name="org.apache.tomcat.util.scan.StandardJarScanner" level="ERROR"/>
주의: 이 방법은 경고를 숨기는 것이지 근본적인 해결은 아니다. 다른 중요한 스캔 관련 경고도 함께 숨겨질 수 있다.
전략 3: Tomcat JAR 스캔 제외 설정
적용 상황: 특정 JAR 패턴을 스캔 대상에서 완전히 제외하고 싶은 경우
application.properties에서 Tomcat의 JAR 스캔 제외 패턴을 설정한다.
# application.properties
server.tomcat.additional-tld-skip-patterns=jaxws-rt*.jar
또는 Java 설정으로 TomcatServletWebServerFactory를 커스터마이징한다.
@Configuration
public class TomcatConfig {
@Bean
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
((StandardJarScanner) context.getJarScanner()).setScanManifest(false);
}
};
}
}
주의: setScanManifest(false)는 모든 MANIFEST.MF 스캔을 비활성화한다. TLD(Tag Library Descriptor) 자동 감지 등에 영향을 줄 수 있으므로 JSP를 사용하지 않는 프로젝트에서만 적용하는 것이 좋다.
전략 4: 라이브러리 버전 업그레이드
적용 상황: 근본적인 해결을 원하고, 라이브러리 버전 변경이 가능한 경우
jaxws-rt 2.3.2 이상 버전에서는 MANIFEST.MF 구조가 개선되어 이 문제가 줄어든다.
// build.gradle
dependencies {
implementation 'com.sun.xml.ws:jaxws-rt:2.3.7'
}
다만 버전 업그레이드 시 호환성 테스트가 필요하다. 특히 SOAP 웹서비스를 사용하는 경우 동작 변경이 있을 수 있다.
전략 5: Jakarta EE로 마이그레이션
적용 상황: 장기적인 관점에서 Java EE에서 Jakarta EE로 전환을 계획하는 경우
Java EE 8 이후로 Jakarta EE로 전환되면서 패키지명과 라이브러리 구조가 변경됐다. Spring Boot 3.x로 업그레이드하면서 함께 마이그레이션하면 이런 레거시 문제들이 자연스럽게 해소된다.
// Jakarta EE 버전 사용
dependencies {
implementation 'com.sun.xml.ws:jaxws-rt:4.0.1' // Jakarta 기반
}
주의: 이 방법은 대규모 마이그레이션이 필요하므로 신중하게 결정해야 한다. javax.* 패키지가 jakarta.*로 변경되어 코드 수정이 필요하다.
전략 선택 가이드
경고 발생
│
├─ 운영 환경에서도 발생하는가?
│ │
│ ├─ Yes → java -jar로 실행 방식 변경 검토
│ │
│ └─ No (개발 환경만) → 전략 1 (무시) 권장
│
├─ 로그가 너무 많아 다른 로그 확인이 어려운가?
│ │
│ └─ Yes → 전략 2 (로그 레벨 조정)
│
├─ JSP를 사용하지 않는 REST API 서버인가?
│ │
│ └─ Yes → 전략 3 (스캔 비활성화) 가능
│
└─ 라이브러리 버전 변경이 가능한가?
│
├─ Yes (단기) → 전략 4 (버전 업그레이드)
│
└─ Yes (장기) → 전략 5 (Jakarta EE 마이그레이션)
결론
이 문제는 Java EE 시대의 라이브러리 배포 방식과 현대적인 빌드 도구 + 임베디드 서버 환경 사이의 간극에서 발생한다.
전통적인 WAS 환경에서는 MANIFEST.MF의 Class-Path가 의미 있게 동작했지만, Gradle이나 Maven으로 직접 실행하는 개발 환경에서는 가정이 맞지 않아 경고가 발생한다. 반면 Spring Boot의 Fat JAR로 패키징하면 모든 라이브러리가 동일 계층에 평탄화되고 전용 클래스로더가 MANIFEST를 무시하므로 문제가 해소된다.
원인을 이해하고 나면 더 이상 당황할 필요가 없다. 상황에 맞는 전략을 선택해서 대응하면 된다.
참고 자료
'Java&Spring > 차세대를 하면서...' 카테고리의 다른 글
| Spring Boot Executable JAR에서 JSP 접근 불가 문제 해결 (0) | 2025.12.09 |
|---|