Maven 부모 POM

Gradle 플러그인의 대응물은 부모 POM spider-silk-maven-parent로, 그 pluginManagement와 프로파일이 같은 패키징을 담고 있다. Maven은 빌드 컨벤션을 플러그인이 아니라 상속으로 공유한다: Maven 플러그인은 골을 실행하며, Gradle 플러그인처럼 다른 플러그인의 설정을 선언할 수 없다.

<parent>
  <groupId>net.benelog.spidersilk</groupId>
  <artifactId>spider-silk-maven-parent</artifactId>
  <version>1.1.0</version>
</parent>

부모 POM은 Maven이 기본적으로 검색하는 Maven Central에 발행(publication)되므로, <parent> 블록 곁에 저장소나 settings.xml 항목이 필요 없다.

선언으로 옵트인하기

모든 플러그인 설정이 부모의 pluginManagement에 들어 있으므로, 각 플러그인은 자식이 선언하기 전까지 비활성 상태로 머문다. Gradle 쪽의 spiderSilk { jte() }가 그렇듯, 그 선언이 옵트인이다. 컨테이너화하고 jte를 미리 컴파일하는 자식은 둘 다 선언하고, 애플리케이션 자신의 것만 설정한다:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifest>
            <mainClass>flashcard.FlashcardApp</mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
    <plugin>
      <groupId>com.google.cloud.tools</groupId>
      <artifactId>jib-maven-plugin</artifactId>
      <configuration>
        <to>
          <image>ghcr.io/benelog/spider-silk-flashcard:${spider-silk.image.tag}</image>
        </to>
      </configuration>
    </plugin>
    <plugin>
      <groupId>gg.jte</groupId>
      <artifactId>jte-maven-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>org.graalvm.buildtools</groupId>
      <artifactId>native-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

메인 클래스는 jar 매니페스트에 한 번만 선언하는데, 실행 가능한 jar라면 어차피 그 자리에 필요하다. Jib은 거기서 엔트리포인트를 추론하고, native-maven-plugin은 바이너리를 위해 같은 항목을 읽는다. Jib과 native-maven-plugin에 따로 설정해도 동작은 하지만, 두 사본이 어긋나기 전까지만이다.

선언한 플러그인은 Gradle 플러그인이 설정하는 컨벤션을 상속한다:

  • Jib은 eclipse-temurin:25-jre 위에 빌드하고 포트 8080을 노출한다. mvn package jib:buildTar는 Docker 데몬 없이 target/jib-image.tar를 써내며, 배포가 설명하는 대로 레이어를 나눈다.

  • maven.compiler.release는 25로 설정되며, 이는 Jib이 베이스 이미지와 대조할 Java 버전을 읽는 자리를 겸한다. 따라서 Gradle 플러그인이 우회하는 targetCompatibility 불일치는 Maven에서는 생기지 않는다.

  • jte는 src/main/resources/jte 아래의 모든 템플릿을 빌드 시점에 미리 컴파일하며, 네이티브 이미지 리소스도 포함해 템플릿이 설명하는 그대로다.

  • 네이티브 바이너리는 <fallback>false</fallback>으로, 대부분 정적으로(--static-nolibc, 그래서 distroless/base 컨테이너는 glibc 외에는 아무것도 빚지지 않는다), 그리고 도달 가능성 메타데이터 저장소를 활성화한 채로 빌드한다. 바이너리 이름은 ${spider-silk.native.image-name}이며, 기본값은 artifactId다.

생성되는 JSON 코덱

부모 POM은 모든 자식의 컴파일러 프로세서 경로에 silk-json-processor를 올리며, 이는 spiderSilk { json() }의 대응물이다. 컴파일러 플러그인은 모든 빌드에서 실행되므로 선언이 필요 없고, @JsonBound가 붙은 타입이 없는 자식에서는 아무 비용도 들지 않는다. jakarta.json.bind 애너테이션을 쓰는 자식은 API를 provided 스코프로 직접 더하는데, 애너테이션은 컴파일 시점에 읽히고 다시는 필요 없기 때문이다:

<dependency>
  <groupId>jakarta.json.bind</groupId>
  <artifactId>jakarta.json.bind-api</artifactId>
  <version>3.0.1</version>
  <scope>provided</scope>
</dependency>

프로세서가 무엇을 생성하는지는 생성되는 JSON 코덱이 설명한다.

native 프로파일

mvn -Pnative package jib:buildTar는 jar 대신 GraalVM 바이너리를 실어 보낸다. 이 프로파일은 native:compile-no-fork를 package에 바인딩하고, Jib의 베이스를 distroless/base로 바꾸며, 같은 바이너리 이름을 가리키는 Jib의 native-image 확장을 붙인다. 네이티브 이미지 장이 그 전환을 훑어본다.

위 자식의 이미지 이름에 있는 ${spider-silk.image.tag} 자리 표시자가 두 이미지를 구분한다: 부모는 기본값을 latest로 두고, 프로파일은 이를 native로 설정하며, 대신 태그를 하드코딩한 자식은 그 구분에서 빠진다.

Dockerfile 워밍업

플랫폼이 Dockerfile을 요구하는 곳에서는 mvn dependency:go-offline이 Gradle 플러그인의 resolveDependencies를 대신한다. 이 명령은 워밍업 레이어를 위해 의존성과 플러그인을 내려받으며, Gradle Dockerfile이 빌드 스크립트를 복사하는 것과 같은 방식으로 pom만 복사한 뒤에 실행된다.

부모 POM이 고정하는 것

부모 POM의 버전은 부모 POM이 관리하는 도구의 버전을 고정한다: jte 3.2.4, Jib 3.5.2(Maven 플러그인은 업스트림에서 Gradle 플러그인의 3.5.4보다 뒤처진다), GraalVM 빌드 도구 1.1.14, 그리고 부모 POM 자신의 버전인 silk-json-processor다. 이 도구들의 버전 올림은 부모 POM 릴리스로 이루어진다. Gradle 쪽과 마찬가지로 여기서 순수한 Maven을 넘어서는 것은 없다: 모든 컨벤션은 부모 POM 자체에서 읽을 수 있으며, 블록을 직접 소유하려는 빌드는 그 블록을 복사해 가고 상속을 끊는다.