μ‹€ν–‰ κ°€λŠ₯ν•œ JAR(Executable JAR) 파일둜 μ‹€ν–‰λœ μŠ€ν”„λ§ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ ν”„λ¦¬λ§ˆμ»€ ν…œν”Œλ¦ΏμœΌλ‘œ μ •μ˜λœ 이메일 ν…œν”Œλ¦Ώμ„ λ¦¬μ†ŒμŠ€λ‘œ κ°€μ Έμ˜€μ§€ λͺ»ν•˜λŠ” 였λ₯˜κ°€ λ°œμƒν–ˆλ‹€. μ²˜μŒμ—λŠ” μƒˆλ‘­κ²Œ Amazon ECS으둜 κ΅¬μ„±λœ 배포 ν™˜κ²½μ—μ„œ μ‚¬μš©λ  λΉŒλ“œλœ JAR νŒŒμΌμ— ν…œν”Œλ¦Ώ 파일이 ν¬ν•¨λ˜μ§€ μ•Šμ•˜λ‹€κ³  μƒκ°ν–ˆμ§€λ§Œ μ  ν‚¨μŠ€ λΉŒλ“œ κ²°κ³Όλ₯Ό λ³΄λ‹ˆ νŒ¨ν‚€μ§•λœ JAR νŒŒμΌμ—λŠ” μ •μƒμ μœΌλ‘œ λ¦¬μ†ŒμŠ€ ν΄λ”μ˜ νŒŒμΌλ“€μ΄ ν¬ν•¨λ˜μ–΄ μžˆμŒμ„ 확인할 수 μžˆμ—ˆλ‹€. ν•œκ°€μ§€ μ€‘μš”ν•œ 뢀뢄은 λ¬Έμ œκ°€ λ°œμƒν•œ μ½”λ“œλ“€μ€ ParallelStream 으둜 인해 ForkJoinPool.commonPool 의 μ›Œμ»€ μŠ€λ ˆλ“œλ‘œ κΈ°λ‘λ˜μ–΄μžˆλ‹€λŠ” 점이닀. 그리고 λΉ„λ°€λ²ˆν˜Έ μ°ΎκΈ° μš”μ²­μ— μ˜ν•œ 이메일 λ°œμ†‘μ— λŒ€ν•œ ν…œν”Œλ¦Ώμ€ μ •μƒμ μœΌλ‘œ κ°€μ Έμ™€μ„œ λ°œμ†‘λ˜κ³  μžˆμŒλ„ ν™•μΈν•˜μ˜€λ‹€. κ·Έλ ‡λ‹€λ©΄, κ³Όμ—° ForkJoinPool.commonPool μŠ€λ ˆλ“œ μ•ˆμ—μ„œλŠ” 무슨일이 λ°œμƒν•˜λŠ” κ²ƒμΌκΉŒ κΆκΈˆν•˜λ‹€.

ForkJoinPool.commonPool

2025-06-27 04:31:00 [ForkJoinPool.commonPool-worker-5] ... class path resource [...] cannot be opened because it does not exist

였λ₯˜μ— λŒ€ν•œ 원인에 λŒ€ν•΄μ„œ 찾던 쀑 spring-boot#15737 μ΄μŠˆμ—μ„œ 힌트λ₯Ό 얻을 수 μžˆμ—ˆλ‹€. JDK 9 μ—μ„œλΆ€ν„° JDK-8172726 에 μ˜ν•΄ ForkJoinPool.commonPool μ—μ„œλŠ” μƒμœ„ μŠ€λ ˆλ“œκ°€ μ•„λ‹Œ μ‹œμŠ€ν…œ ν΄λž˜μŠ€λ‘œλ”κ°€ μ‚¬μš©λœλ‹€λŠ” 것이 μ€‘μš”ν•œ 정보이닀. 그리고 ForkJoinPool μ—μ„œ LaunchedClassLoader λ₯Ό μ‚¬μš©ν•  수 있게 μ„€μ •ν•˜μžλŠ” spring-boot#39843 티켓도 μžˆλ‹€.

ClassLoader in ClassPathResource

μŠ€ν”„λ§ 기반 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ λΉŒλ“œ λ‹¨κ³„μ—μ„œ λ¦¬μ†ŒμŠ€(resources) 폴더λ₯Ό ν΄λž˜μŠ€νŒ¨μŠ€μ— 포함될 수 μžˆλ„λ‘ WAR λ˜λŠ” JAR 파일둜 νŒ¨ν‚€μ§•λœλ‹€. μ΄λ ‡κ²Œ ν¬ν•¨λœ λ¦¬μ†ŒμŠ€λŠ” 정적 λ¦¬μ†ŒμŠ€λ‘œ μ‚¬μš©μžμ—κ²Œ 전달할 μˆ˜λ„ μžˆμ§€λ§Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‚΄λΆ€μ μœΌλ‘œ λ‘œλ“œλ˜μ–΄ ν™œμš©λ  수 μžˆλ‹€. ν΄λž˜μŠ€νŒ¨μŠ€μ— μ‘΄μž¬ν•˜λŠ” λ¦¬μ†ŒμŠ€λ₯Ό μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆλ„λ‘ μ§€μ›ν•˜λŠ”κ²Œ μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬μ—μ„œ μ œκ³΅ν•˜λŠ” ClassPathResource ν΄λž˜μŠ€λ‹€. ClassPathResource의 μƒμ„±μžμ—μ„œ ν΄λž˜μŠ€λ‘œλ”κ°€ μ§€μ •λ˜μ§€ μ•ŠμœΌλ©΄ ClassUtils.getDefaultClassLoader 의 ν΄λž˜μŠ€λ‘œλ”λ₯Ό μ‚¬μš©ν•˜μ—¬ λ¦¬μ†ŒμŠ€λ₯Ό μ°Έμ‘°ν•˜κ²Œ λœλ‹€. μ½”λ“œ κ΅¬ν˜„μ„ μ‚΄νŽ΄λ³΄λ©΄ ν˜„μž¬ μŠ€λ ˆμŠ€μ— μ„€μ •λœ μ»¨ν…μŠ€νŠΈ ν΄λž˜μŠ€λ‘œλ”λ₯Ό λ¨Όμ € μ°Ύκ³  λ§ˆμ§€λ§‰μœΌλ‘œλŠ” μ‹œμŠ€ν…œ ν΄λž˜μŠ€λ‘œλ”λ₯Ό 톡해 λΆ€νŠΈμŠ€νŠΈλž© ν΄λž˜μŠ€λ‘œλ”λ‘œ μœ„μž„ν•˜μ—¬ λ¦¬μ†ŒμŠ€λ₯Ό κ°€μ Έμ˜¬ 수 μžˆμŒμ„ 이해할 수 μžˆλ‹€. 일반적인 ν΄λž˜μŠ€λ‘œλ”μ™€ μŠ€λ ˆλ“œ μ»¨ν…μŠ€νŠΈ ν΄λž˜μŠ€λ‘œλ”μ˜ 차이λ₯Ό 보면 ForkJoinPool.commonPool λ‚΄μ˜ ContextClassLoaderκ°€ λ‹€λ₯Ό 수 μžˆμŒμ„ μ•Œ 수 μžˆλŠ”λ° μ•žμ„œ JDK-8172726 λ₯Ό 톡해 JDK 9 λΆ€ν„°λŠ” ContextClassLoaderκ°€ μ‹œμŠ€ν…œ ν΄λž˜μŠ€λ‘œλ”λ‘œ μ„€μ •λœλ‹€λŠ” 것을 ν™•μΈν–ˆμ—ˆλ‹€.

2025-06-29T04:03:25.603Z  INFO 1 --- [springboot] [   scheduling-1] kr.kdev.demo.Application                 : jar:nested:/app.jar/!BOOT-INF/classes/!/test.txt, true, org.springframework.boot.loader.launch.LaunchedClassLoader@36baf30c, org.springframework.boot.loader.launch.LaunchedClassLoader@36baf30c
2025-06-29T04:03:25.602Z  INFO 1 --- [springboot] [onPool-worker-2] kr.kdev.demo.Application                 : jar:nested:/app.jar/!BOOT-INF/classes/!/test.txt, true, org.springframework.boot.loader.launch.LaunchedClassLoader@36baf30c, jdk.internal.loader.ClassLoaders$AppClassLoader@502f3a78
2025-06-29T04:03:25.602Z  INFO 1 --- [springboot] [onPool-worker-1] kr.kdev.demo.Application                 : jar:nested:/app.jar/!BOOT-INF/classes/!/test.txt, true, org.springframework.boot.loader.launch.LaunchedClassLoader@36baf30c, jdk.internal.loader.ClassLoaders$AppClassLoader@502f3a78

μœ„μ™€ 같이 ParellelStream λ‚΄μ—μ„œλŠ” ForkjoinPool.commonPool 인 경우 LaunchedClassLoaderκ°€ μ•„λ‹Œ ClassLoaders$AppClassLoader κ°€ 좜λ ₯λ˜μ—ˆμŒμ„ μ•Œ 수 μžˆμ—ˆλ‹€. JDK 버전과 관계없이 ν•΄λ‹Ή λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλ‹€λŠ” μ΄μ•ΌκΈ°λ‘œ ForkJoinPool.commonPool 의 μŠ€λ ˆλ“œ λ‚΄μ—μ„œ ClassPathResourceλ₯Ό μ‚¬μš©ν•  λ•Œμ—λŠ” μ˜¬λ°”λ₯Έ ν΄λž˜μŠ€λ‘œλ”κ°€ μ‚¬μš©ν•  수 μžˆλ„λ‘ 전달해야함을 μ•Œ 수 μžˆλ‹€. 그런데, 아직 또 λ‹€λ₯Έ λ¬Έμ œκ°€ λ‚¨μ•„μžˆλ‹€. ClasspathResourceλ₯Ό 직접 μ‚¬μš©ν•˜λŠ” μ½”λ“œλΌλ©΄ λΆ€λͺ¨ μŠ€λ ˆλ“œμ˜ ν΄λž˜μŠ€λ‘œλ”λ₯Ό λͺ…μ‹œμ μœΌλ‘œ μ „λ‹¬ν•˜λ©΄ 해결이 λœλ‹€. κ·ΈλŸ¬λ‚˜, 이메일 λ°œμ†‘μ„ μœ„ν•΄ μ •μ˜λœ 이메일 ν…œν”Œλ¦Ώμ„ κ°€μ Έμ˜¬ λ•Œμ—λŠ” ν”„λ¦¬λ§ˆμ»€ ν…œν”Œλ¦Ώ μ—”μ§„μ˜ ν…œν”Œλ¦Ώ λ‘œλ”λ‘œ μœ„μž„ν•˜κ²Œ λœλ‹€. 더 λ‚˜μ•„κ°€μ„œλŠ” Nested ν˜•νƒœλ‘œ κ³΅ν†΅μ μœΌλ‘œ μ •μ˜λœ 뢀뢄이 ν¬ν•¨λœ 이메일 ν…œν”Œλ¦ΏμœΌλ‘œ μ •μ˜λ˜μ–΄ 미리 ν…œν”Œλ¦Ώμ„ λΆˆλŸ¬μ™€μ„œ μΊμ‹œν•  μˆ˜λ„ μ—†λŠ” μƒν™©μœΌλ‘œ ν™•μΈλœλ‹€.

일단, FreeMarkerConfigurationFactory.setResourceLoader을 μ΄μš©ν•΄λ³΄μ•„μ•Όν•  것 κ°™λ‹€.