Notice
Recent Posts
Recent Comments
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

Grobble

Classpath Index 본문

공부

Classpath Index

smilu97 2023. 7. 29. 20:14

org.springframework.boot:spring-boot-maven-plugin 을 사용하면 Spring Boot Executable Jar를 생성할 수 있다. 이 Jar내부에는 BOOT-INF/classpath.idx 파일이 존재하게 되는데, 내용은 대략 아래와 같은 모양을 가진다

- "BOOT-INF/lib/pinpoint-web-2.6.0-SNAPSHOT.jar"
- "BOOT-INF/lib/pinpoint-commons-2.6.0-SNAPSHOT.jar"
- "BOOT-INF/lib/pinpoint-commons-server-2.6.0-SNAPSHOT.jar"
- "BOOT-INF/lib/pinpoint-commons-profiler-2.6.0-SNAPSHOT.jar"
- "BOOT-INF/lib/pinpoint-plugins-loader-2.6.0-SNAPSHOT.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-2.7.13.jar"
- "BOOT-INF/lib/jakarta.validation-api-2.0.2.jar"
- "BOOT-INF/lib/pinpoint-commons-server-cluster-2.6.0-SNAPSHOT.jar"
- "BOOT-INF/lib/curator-client-4.2.0.jar"

이 내용은 해당 프로젝트 디렉토리에서 mvn dependency:tree를 실행했을 때의 결과에서 depth 정보를 뺀 것과 같다. 예를 들어, 위의 classpath.idx 내용이 나오는 프로젝트에서 mvn dependency:tree를 실행하면 아래와 같이 나온다.

[INFO] com.navercorp.pinpoint:pinpoint-web-starter:jar:2.6.0-SNAPSHOT
[INFO] +- com.navercorp.pinpoint:pinpoint-web:jar:2.6.0-SNAPSHOT:compile
[INFO] | +- com.navercorp.pinpoint:pinpoint-commons:jar:2.6.0-SNAPSHOT:compile
[INFO] | +- com.navercorp.pinpoint:pinpoint-commons-server:jar:2.6.0-SNAPSHOT:compile
[INFO] | | +- com.navercorp.pinpoint:pinpoint-commons-profiler:jar:2.6.0-SNAPSHOT:compile
[INFO] | | +- com.navercorp.pinpoint:pinpoint-plugins-loader:jar:2.6.0-SNAPSHOT:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.13:compile
[INFO] | | \- jakarta.validation:jakarta.validation-api:jar:2.0.2:compile
[INFO] | +- com.navercorp.pinpoint:pinpoint-commons-server-cluster:jar:2.6.0-SNAPSHOT:compile
[INFO] | | +- org.apache.curator:curator-client:jar:4.2.0:compile

보면, 첫줄은 자기 자신이므로 제외되고, 나머지는 순서가 그대로인 것을 볼 수 있다.

이걸 어디에 쓰나

JVM은 classpath를 여러 개 가질 수 있다. 그러다보니 동일한 classpath를 가지는 리소스가 복수 존재할 수 있는데, 이 리소스간의 우선순위는 classpath가 정의된 순서에 따라(먼저 등장한 쪽이 승리) 결정된다. 예를 들어 다음과 같이 java를 실행했다면,

java -cp "A.jar:B.jar" com.example.MainClass

classpath:application.yml 리소스를 로드했을 때, A.jar, B.jar 양쪽에 application.yml이 존재하더라도 A.jarapplication.yml 만이 효력을 가진다.

spring boot 에서도 classpath.idx에 등장하는 순서대로 classpath를 설정하기 때문에, pom.xml에 먼저 등장하는 dependency project가 우선권을 가지게 된다.

소스코드 검증

classpath.idx가 작성되는 코드를 살펴보기 위한 첫 진입점은 아래인 것으로 보인다.

Map<String, Library> write(AbstractJarWriter writer) throws IOException {
    Map<String, Library> writtenLibraries = new LinkedHashMap<>();
    for (Entry<String, Library> entry : this.libraries.entrySet()) {
        String path = entry.getKey();
        Library library = entry.getValue();
        if (library.isIncluded()) {
            String location = path.substring(0, path.lastIndexOf('/') + 1);
            writer.writeNestedLibrary(location, library);
            writtenLibraries.put(path, library);
        }
    }
    writeClasspathIndexIfNecessary(writtenLibraries.keySet(), getLayout(), writer);
    return writtenLibraries;
}

private void writeClasspathIndexIfNecessary(Collection<String> paths, Layout layout, AbstractJarWriter writer)
        throws IOException {
    if (layout.getClasspathIndexFileLocation() != null) {
        List<String> names = paths.stream().map((path) -> "- \"" + path + "\"").toList();
        writer.writeIndexFile(layout.getClasspathIndexFileLocation(), names); // arg0이 보통 "BOOT-INF/classpath.idx"임
    }
}

여기서 LinkedHashMap은 엔트리가 들어온 순서를 유지하기 때문에, 순서는 this.libraries 맵에 들어온 순서로 결정된다. 그리고 this.libraries도 마찬가지로 LinkedHashMap인 걸 따라가서 확인했다. 계속 따라가면(꽤 많이 따라가야 해서 자세한 내용은 생략) spring-boot 프로젝트를 벗어나서 MavenProject 라는 객체가 외부에서 주입되는 것을 볼 수 있는데 이것이 순서 정보를 가지고 있다는 것을 확인할 수 있다. SpringBoot는 Maven이 정해주는 순서를 그대로 따르는 것 같다.

Maven 프로젝트로 넘어가서 대략 어디서 MavenProject를 만드는지 찾아보면 DefaultProjectBuilder라는 builder가 만든다는 것을 볼 수 있는데, 이 과정에서 DefaultProjectBuilder::resolveDependencies가 있는 것을 볼 수 있다. 여기서 호출하는 Util 메소드인 RepositoryUtils::toArtifacts를 보면 아래와 같은데

public static void toArtifacts(
            Collection<org.apache.maven.artifact.Artifact> artifacts,
            Collection<? extends DependencyNode> nodes,
            List<String> trail,
            DependencyFilter filter) {
    for (DependencyNode node : nodes) {
        org.apache.maven.artifact.Artifact artifact = toArtifact(node.getDependency());

        List<String> nodeTrail = new ArrayList<>(trail.size() + 1);
        nodeTrail.addAll(trail);
        nodeTrail.add(artifact.getId());

        if (filter == null || filter.accept(node, Collections.emptyList())) {
            artifact.setDependencyTrail(nodeTrail);
            artifacts.add(artifact);
        }

        // Recursive
        toArtifacts(artifacts, node.getChildren(), nodeTrail, filter);
    }
}

전형적인 깊이 우선 탐색 순회코드인 것을 볼 수 있다.