JAVA 만을 사용해서 웹 서버를 구성하던 중 정적 파일을 읽어 응답해주어야하는 경우가 있었다.
String path = "src/main/resources/static/";
File file = new File(path + uri);
위와 같이 프로젝트의 path 를 사용해서 정적 파일을 읽었다.
하지만 저 코드는 "src/main/resource/" 경로가 유지되어야한다.
굳이 말하자면 WEB-INF 나 webapp 같은 파일로 정적 파일을 관리한다면 해당 코드는 사용하지 못한다.
그리고 그런 것 보다는 기본 Path 정도는 시스템에 넣어서 쓰는 편이 좋지 않을까 싶어서 고치고자 한다.
1. Class Loader
결론적으로는 Resource 파일을 가져오기 위해서는 Class Loader 를 사용한다.
Class Loader 에 대한 내용은 다음 글을 참고하자.
https://happyzodiac.tistory.com/48
Class Loader 는 한 마디로 프로젝트 폴더에서 만든 모든 파일을 가져와준다.
여기에는 당연히 정적 파일들도 포함된다.
(1). ClassLoader.getResource( ... )
ClassLoader classLoader = getClass().getClassLoader();
어느 클래스든 클래스 로더를 통해서 로딩되었기 때문에 현재 위치의 클래스의 클래스 로더를 가져올 수 있다.
public URL getResource(String name) {
Objects.requireNonNull(name);
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = BootLoader.findResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
getResource 는 리소스가 위치한 URL 을 반환해주는 메서드이다.
실제 디버깅을 진행하면 Application Class Loader 에서 Platform Class Loader 로 올라간다.
가장 먼저 BootLoader.findResource 를 진행하고
이후에 PlatformClassLoader 가 findResource 를 진행한다.
JVM 에서 살펴봤던 대로 가장 마지막으로 Application Class Loader 가 findResource 를 진행하며 이곳이 우리가 정적파일을 찾는 메서드 위치이다.
(2). URLClassPath.findResource( ... )
결국 Application Class Loader 가 정적 파일을 찾기 위해 실행하는 메서드는 URLClassPath 의 findResource 이다.
public URL findResource(String name, boolean check) {
Loader loader;
for (int i = 0; (loader = getLoader(i)) != null; i++) {
URL url = loader.findResource(name, check);
if (url != null) {
return url;
}
}
return null;
}
for 문을 돌면서 ClassPath 에 등록되어있는 모든 path 를 돌면서 원하는 파일을 하나하나 찾아나간다.
여기 중에서 눈에 띄는 것이 바로 "/Users/admin/Desktop/be-was-2024/out/production/resources" 이다.
실제 내가 사용하고 있는 프로젝트 디렉토리 안에 out 파일에 존재하는 것이다.
즉, 현재 Java 프로젝트를 build 하고 존재하는 파일들의 위치를 뒤져본다는 것이다.
static/index.html 파일은 예상하던 path 에서 찾을 수 있었고, base 와 합친 full-path 를 반환하게 된다.
결론은 빌드할 때 정적파일들을 모두 out/production/resources 로 가져온다는 뜻이고 빌드 시 "src/main/resources/" 파일을 가져가라고 지정하면 완벽하게 해결될 것 같다.
(3). build.gradle
물론 "src/main/resources" 경로는 현재 default 경로이기 때문에 자동으로 빌드 시 이동되는 것 같다.
눈으로 확인해보기 위해 현재 resources 디렉토리의 이름을 "abc" 로 변경해보자.
sourceSets {
main {
resources {
srcDirs "src/main/abc"
}
}
}
build.gradle 에 바꾼 디렉토리를 등록해놓자.
build.gradle 에 리소스 디렉토리를 등록하지 않았을 때에는 abc 라는 파일에 존재하는 static/index.html 을 읽어내지 못한다.
out/resources 폴더에 아무것도 존재하지 않는 것을 확인할 수 있다.
build.gradle 에 디렉토리를 등록하는 순간 abc 에 있는 파일도 읽어낼 수 있게 된다.
또한 out 폴더를 봤을 때에도 static/index.html 이 존재하는 것을 확인할 수 있다.
- 결론적으로 build.gradle 을 통해서 내 프로젝트 파일에 존재하는 파일들을 resources 디렉토리로 지정할 수 있다.
- 지정된 파일들은 빌드 시에 out/resources 에 등록된다.
- ClassLoader 는 CLASS_PATH(out/resources) 에 존재하는 파일들을 읽을 수 있다.
2. Spring 에서의 Resources 파일 읽기
ClassPathResource classPathResource = new ClassPathResource("static/index.html");
Spring 에서는 ClassPathResource 클래스를 사용해서 CLASS_PATH 에 등록된 파일들을 읽어낸다.
public class ClassPathResource extends AbstractFileResolvingResource {
private final String path;
private final String absolutePath;
@Nullable
private final ClassLoader classLoader;
@Nullable
private final Class<?> clazz;
public ClassPathResource(String path) {
this(path, (ClassLoader)null);
}
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
Assert.notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
this.path = pathToUse;
this.absolutePath = pathToUse;
this.classLoader = classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader();
this.clazz = null;
}
...
}
해당 클래스의 내부를 살펴보면 결국 path 를 가지고 ClassLoader 를 사용해 리소스를 읽어낸다.
Spring 에서도 결국 동일한 방법을 사용하고 있다는 뜻이다.
3. JAVA Reflection
Reflection 이란 런타임에 클래스에 대한 정보를 조사하고 조작할 수 있는 기능이다. 이때에도 실제 ClassLoader 를 사용하고 있기 때문에 간단하게 비교만 해보자.
Class<?> clazz = Class.forName("java.lang.String");
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method.getName());
}
아주 대표적이고 간단한 Reflection 의 예시이다. 런타임 중에 String 클래스에 대한 Class 객체를 가져오게되고 내부에 메서드 정보를 얻어내는 코드이다.
여기서 Class 객체를 가져오는 forName() 메서드를 한 번 살펴보자.
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
여기서 호출하는 Class 객체와 함께 ClassLoader 를 가져온다. 이후 과정은 네이티브 메서드(JNI)로 C/C++ 로 구현되어있다. 결국 메서드가 하는 일은 ClassLoader 를 통해서 해당 클래스 파일을 찾고 로드하게 된다. 이후 JVM 내부에서 로드된 클래스 파일을 통해서 Class 객체를 생성하게 되는 것이다.
여기서 호출부를 함께 전달하는 이유는 접근 권한 등을 확인해 허가되지 않은 클래스 로딩이나 접근을 방지하기 위함이다.
Reflection 은 유연성과 일반화된 코드 실행 능력을 제공한다. 객체의 타입을 정확히 알지 못하더라도 객체의 메소드를 호출할 수 있다. 실제로 Spring 에서도 의존성 주입 시에 Reflection 을 사용해서 의존성 주입을 구현한다. @Autowired 어노테이션을 사용하면 스프링 컨테이너가 Reflection 을 사용해서 필요한 객체들을 클래스에 자동으로 주입해준다. 이외에도 하이버네이트는 객체와 데이터베이스 테이블 간의 매핑을 동적으로 처리하기도 한다.
하지만 Reflection 은 런타임에 클래스를 분석하기 때문에 성능이 느리고 컴파일 타임에 타입 체크 등이 불가능하다는 단점이 있다. 또한 private 메서드나 필드에도 접근할 수 있기 때문에 보안상 큰 취약점이 될 수 있기에 주의해야한다.
4. 결론
- 경로 상수를 사용하지 말고 ClassLoader 를 사용해서 CLASS_PATH 에 존재하는 파일을 그대로 읽어오자!
- Java Reflection 도 ClassLoader 를 사용한다!
'Programming > JAVA' 카테고리의 다른 글
부동소수점? BigDecimal 을 통한 정확한 계산 (7) | 2024.11.11 |
---|---|
[Java] Volatile? Synchronized? Concurrent? 자바의 동기화 방식 (4) | 2024.11.09 |
[JAVA] JVM 구조 총정리 (0) | 2024.07.10 |
[JAVA] Spring 없이 웹 서버 구축! (4) 라우팅을 위한 Mapper (0) | 2024.07.09 |
[JAVA] Spring 없이 웹 서버 구축! (3) HTTP Request, Response 객체 (0) | 2024.07.09 |