Spring Boot Executable JAR에서 JSP 접근 불가 문제 해결

2025. 12. 9. 18:41·Java&Spring/차세대를 하면서...

Spring Boot Executable JAR에서 JSP 접근 불가 문제 해결

배경

Spring Framework 2.5.6 기반의 레거시 시스템을 Spring Boot 2.7.18로 업그레이드하는 작업을 진행했다. 기존 시스템은 EAR 파일로 패키징하여 JavaEE 스펙인 외부 JEUS에 배포하는 방식이었는데, Spring Boot로 전환하면서 Executable JAR(bootJar) 형태로 패키징해야 했다.

Executable JAR를 선택한 이유

  • 외부 WAS 의존성 제거 (내장 Tomcat 사용)
  • 배포 단순화 (java -jar로 실행)
  • 컨테이너 환경(Docker/Kubernetes) 대응 용이
  • 운영 환경 표준화

그러나 레거시 시스템에는 수백 개의 JSP 파일이 존재했고, 이를 당장 Thymeleaf 등으로 마이그레이션하는 것은 현실적으로 불가능했다. JSP를 유지하면서 Executable JAR로 패키징해야 하는 상황이었다.

문제 상황

Spring Boot 애플리케이션을 bootJar로 패키징한 후 실행했더니 JSP 페이지에 접근할 수 없었다. 분명 src/main/webapp 디렉토리에 JSP 파일들이 존재하는데, 404 에러가 발생했다.

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
...
There was an unexpected error (type=Not Found, status=404).

처음에는 ViewResolver 설정 문제인 줄 알았다. application.properties에서 prefix/suffix 설정을 확인해봤지만 정상이었다.

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

IDE에서 실행할 때는 잘 되는데, JAR로 패키징하면 안 되는 상황이었다. "그러면 Gradle에서 webapp 파일을 JAR에 포함시키면 되지 않을까?" 생각했다.

원인 분석

Executable JAR의 구조적 한계

Spring Boot의 Executable JAR는 일반적인 WAR 파일과 구조가 다르다.

WAR 파일 구조:

myapp.war
├── WEB-INF/
│   ├── classes/
│   ├── lib/
│   └── views/
│       └── index.jsp
├── META-INF/
└── index.jsp

Spring Boot Executable JAR 구조:

myapp.jar
├── BOOT-INF/
│   ├── classes/
│   │   └── (컴파일된 클래스들)
│   └── lib/
│       └── (의존성 JAR들)
├── META-INF/
│   └── MANIFEST.MF
└── org/springframework/boot/loader/
    └── (Spring Boot Loader 클래스들)

문제의 핵심은 src/main/webapp 디렉토리가 Executable JAR에 포함되지 않는다는 것이다.

Gradle의 기본 동작

Gradle의 bootJar 태스크는 다음 위치만 JAR에 포함한다:

소스 위치 JAR 내 위치 포함 여부
src/main/java BOOT-INF/classes O
src/main/resources BOOT-INF/classes O
src/main/webapp - X

webapp 디렉토리는 WAR 패키징 전용이다. JAR 패키징 시에는 완전히 무시된다.

Gradle 복사만으로 해결되지 않는 이유

"그러면 Gradle에서 src/main/webapp을 META-INF/resources로 복사하면 되지 않을까?" 생각했다.

task copyWebappToResources(type: Copy) {
    from 'src/main/webapp'
    into 'build/resources/main/META-INF/resources'
}

이렇게 하면 JAR 내부에 JSP 파일이 포함된다:

myapp.jar
└── BOOT-INF/
    └── classes/
        └── META-INF/
            └── resources/
                └── WEB-INF/
                    └── views/
                        └── index.jsp

그런데 이것만으로는 동작하지 않았다. 이유는 다음과 같다.

1. Servlet 스펙의 제약

Servlet 스펙에 따르면 META-INF/resources는 WEB-INF/lib에 패키징된 JAR에서만 로드되어야 한다. 주 아카이브(메인 JAR) 내의 META-INF/resources에서는 로드되면 안 된다.

Spring Boot 1.4.3부터 이 스펙을 엄격히 준수하기 시작했다. 따라서 BOOT-INF/classes/META-INF/resources에 있는 리소스는 Tomcat이 자동으로 인식하지 않는다.

2. 경로 불일치

┌─────────────────────────────────────────────────────────────┐
│ Gradle 복사 결과                                             │
│ → BOOT-INF/classes/META-INF/resources/WEB-INF/views/*.jsp  │
└─────────────────────────────────────────────────────────────┘
                              ≠
┌─────────────────────────────────────────────────────────────┐
│ Tomcat이 탐색하는 위치 (Servlet 스펙 기준)                   │
│ → WEB-INF/lib/*.jar 내부의 META-INF/resources              │
└─────────────────────────────────────────────────────────────┘

Tomcat은 BOOT-INF/classes 안에 있는 META-INF/resources를 웹 리소스로 인식하지 않는다.

3. ClassLoader 계층 구조 문제

Spring Boot의 Executable JAR는 특수한 ClassLoader 구조를 사용한다:

┌─────────────────────────────────────────┐
│ AppClassLoader                          │
│ URL: file:/path/to/myapp.jar            │
│ (fat jar 자체만 포함)                    │
└─────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────┐
│ LaunchedURLClassLoader                  │
│ URL: jar:file:/path/to/myapp.jar!/      │
│      BOOT-INF/classes/                  │
│      BOOT-INF/lib/*.jar                 │
└─────────────────────────────────────────┘

AbstractEmbeddedServletContainerFactory.getUrlsOfJarsWithMetaInfResources() 메서드는 ClassLoader의 URL을 순회하며 META-INF/resources가 있는 JAR를 찾는다. 그러나:

  • AppClassLoader는 fat jar URL만 반환한다
  • LaunchedURLClassLoader는 BOOT-INF 내부 경로만 포함한다
  • 이 메서드는 부모 ClassLoader를 탐색하지 않는다

결과적으로 BOOT-INF/classes/META-INF/resources는 Tomcat에 등록되지 않는다.

4. Spring Boot 팀의 공식 입장

이 문제는 GitHub Issue #8324에서 논의되었다. Spring Boot 팀의 Andy Wilkinson은 다음과 같이 설명했다:

"Servlet 스펙은 META-INF/resources의 리소스가 WEB-INF/lib에 패키징된 JAR에서만 로드되어야 하며, 주 아카이브 내의 META-INF/resources에서는 로드되지 않아야 한다고 명시하고 있습니다."

이 이슈는 "status: declined"로 종료되었다. Spring Boot 팀은 BOOT-INF/classes/META-INF/resources 지원을 공식적으로 거부했다.

해결/대응 전략

Gradle 복사만으로는 부족하다. 두 가지 전략을 함께 적용해야 한다.

전략 1: META-INF/resources로 복사 (필요조건)

먼저 JSP 파일을 JAR 내부에 포함시켜야 한다.

// webapp 디렉토리를 META-INF/resources로 복사
task copyWebappToResources(type: Copy) {
    from 'src/main/webapp'
    into 'build/resources/main/META-INF/resources'
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

// processResources가 webapp 복사 이후에 실행되도록
processResources {
    dependsOn copyWebappToResources
}

// bootJar 설정
bootJar {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

주의: 이것만으로는 부족하다! 위에서 설명한 대로 Tomcat이 이 경로를 자동으로 인식하지 않기 때문이다.

전략 2: TomcatServletWebServerFactory 커스터마이징 (충분조건)

Tomcat에 명시적으로 리소스 경로를 등록해야 한다.

@Configuration
public class JspConfiguration {

    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
        return factory -> factory.addContextCustomizers(context -> {
            // JAR 스캔 필터 설정
            context.getJarScanner().setJarScanFilter((jarScanType, jarName) -> true);

            WebResourceRoot resources = new StandardRoot(context);

            try {
                URL resourceUrl = getClass().getClassLoader()
                    .getResource("META-INF/resources");

                if (resourceUrl != null) {
                    String protocol = resourceUrl.getProtocol();

                    if ("jar".equals(protocol)) {
                        // JAR 내부 리소스 등록
                        String jarPath = resourceUrl.toString();
                        int separatorIndex = jarPath.indexOf("!/");
                        if (separatorIndex > 0) {
                            // jar:file:/path/to/file.jar!/META-INF/resources
                            String jarFileUrl = jarPath.substring(4, separatorIndex);
                            jarFileUrl = jarFileUrl.substring(5); // "file:" 제거

                            resources.addPreResources(
                                new JarResourceSet(resources, "/",
                                    jarFileUrl, "/META-INF/resources")
                            );
                        }
                    } else {
                        // IDE 실행 시 (파일 시스템)
                        File resourceFile = new File(resourceUrl.toURI());
                        resources.addPreResources(
                            new DirResourceSet(resources, "/",
                                resourceFile.getAbsolutePath(), "/")
                        );
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(
                    "Failed to configure JSP resources", e);
            }

            context.setResources(resources);
        });
    }
}

이 설정이 하는 일:

┌─────────────────────────────────────────────────────────────┐
│ 요청: /WEB-INF/views/index.jsp                              │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ Tomcat WebResourceRoot                                      │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PreResources (우선 탐색)                                 │ │
│ │ → JarResourceSet: JAR!/META-INF/resources/              │ │
│ │   또는                                                   │ │
│ │ → DirResourceSet: 파일시스템 경로                        │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│ 매핑: / → META-INF/resources/                               │
│ 결과: META-INF/resources/WEB-INF/views/index.jsp 찾음       │
└─────────────────────────────────────────────────────────────┘

두 전략이 모두 필요한 이유

전략 역할 단독 적용 시
전략 1 (Gradle 복사) JAR 내부에 JSP 파일 포함 Tomcat이 인식 못함
전략 2 (Tomcat 설정) Tomcat에 리소스 경로 등록 파일이 없어서 404
  • 전략 1은 필요조건이다. JSP 파일이 JAR 안에 있어야 한다.
  • 전략 2는 충분조건이다. Tomcat이 해당 경로를 인식하도록 명시적으로 등록해야 한다.
  • 둘 다 있어야 동작한다.

전략 선택 가이드

JSP를 Executable JAR에서 사용해야 하는가?
    │
    ├─ YES → 전략 1 + 전략 2 모두 적용
    │
    └─ NO → WAR 패키징 또는 Thymeleaf 등 템플릿 엔진 사용 권장

주의: Spring Boot 공식 문서에서는 Executable JAR + JSP 조합을 권장하지 않는다. 레거시 시스템이 아니라면 Thymeleaf 등 템플릿 엔진으로 마이그레이션하는 것이 장기적으로 유리하다.

결론

Spring Boot Executable JAR에서 JSP가 동작하지 않는 이유는 Servlet 스펙과 Spring Boot의 중첩 JAR 구조 때문이다.

핵심 인사이트:

  • Gradle로 META-INF/resources에 복사하는 것만으로는 부족하다
  • Servlet 스펙상 BOOT-INF/classes/META-INF/resources는 웹 리소스로 인식되지 않는다
  • Spring Boot 팀은 이 동작을 의도적으로 설계했으며, 지원 요청을 거부했다
  • JarResourceSet으로 Tomcat에 명시적으로 리소스 경로를 등록해야 한다
  • Gradle 복사(필요조건) + Tomcat 설정(충분조건) 두 가지를 함께 적용해야 동작한다

참고 자료

  • Spring Boot Reference - JSP Limitations
  • Apache Tomcat - WebResourceRoot
  • Gradle - Copy Task
  • GitHub Issue #9525 - Static resources from META-INF/resources not made available to Tomcat
  • GitHub Issue #8324 - Configure embedded containers to load static resources from fat jar's BOOT-INF/classes/META-INF/resources
저작자표시 비영리 변경금지 (새창열림)

'Java&Spring > 차세대를 하면서...' 카테고리의 다른 글

Spring Boot 애플리케이션 실행 시 JAR 스캔 경고 문제 분석  (0) 2025.12.08
'Java&Spring/차세대를 하면서...' 카테고리의 다른 글
  • Spring Boot 애플리케이션 실행 시 JAR 스캔 경고 문제 분석
무토(MUTO)
무토(MUTO)
무식하게 공부하자. 토 나올 때 까지.
  • 무토(MUTO)
    MUTO 와 함께 개발을
    무토(MUTO)
  • 전체
    오늘
    어제
    • 분류 전체보기 (32)
      • 우아한유스방 (0)
      • Node (1)
      • 알고리즘 (3)
      • Java&Spring (21)
        • STUDY HALLE (15)
        • 차세대를 하면서... (2)
        • Java (2)
        • Spring (2)
      • 실패록 (5)
        • 오늘의 실패록 (2)
        • 회고록 (3)
      • 책읽기 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Comparable
    comparator
    유닛테스트
    이진탐색트리
    백준
    실패록
    intellij
    java8
    java error
    enum
    취준생
    jvm
    Java
    operator
    오브젝트
    415
    연결리스트
    bytecode
    whiteship
    github
    자바
    선형자료구조
    이진트리
    더블 디스패치
    junit5
    Java Exception
    study halle
    조영호
    @RequestMapping
    객체지향
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
무토(MUTO)
Spring Boot Executable JAR에서 JSP 접근 불가 문제 해결
상단으로

티스토리툴바