← 개발 로그 목록

website: 백엔드 도커 빌드에서 컴파일 스테이지만 BUILDPLATFORM으로 고정해 arm64 크로스 빌드 단축

/ 4분 분량 / 개발 로그

CD 파이프라인의 "이미지 빌드 & 푸시" 스텝이 11분 44초 걸리고 있었다. 전체 배포가 13분 정도인데 그중 대부분을 이 스텝이 먹고 있었으니 원인을 봐야 했다.

backend/Dockerfile을 열어보니 builder 스테이지, 그러니까 Gradle로 컴파일하는 부분이 워크플로의 platforms: linux/arm64 지정 전체에 같이 묶여 있었다. OCI 서버가 arm64라 최종 이미지는 arm64로 빌드해야 하는 게 맞는데, 문제는 그 지정이 builder 스테이지까지 그대로 적용되면서 컴파일 자체가 QEMU arm64 에뮬레이션 위에서 돌고 있었다는 점이다. GH 러너는 amd64인데, 굳이 arm64로 에뮬레이션해가며 Gradle을 돌리고 있었던 셈이다.

여기서 짚어야 했던 건 "왜 컴파일까지 arm64로 해야 하나"였다. Java는 네이티브 기계어로 컴파일되는 게 아니라 JVM 바이트코드로 컴파일된다. 즉 컴파일 결과물은 어떤 CPU 아키텍처에서 돌렸든 동일하다. arm64 타깃이어야 하는 건 최종 실행 환경, 즉 런타임 이미지뿐이다. 이 둘을 구분하지 않고 하나로 묶어서 빌드하고 있었던 게 시간을 잡아먹은 이유였다.

고친 건 한 줄이다.

FROM --platform=$BUILDPLATFORM gradle:8-jdk21-alpine AS builder

$BUILDPLATFORM은 buildx가 자동으로 채워주는 값으로, 빌드가 실제로 실행되는 머신의 아키텍처(여기선 GH 러너의 amd64)를 가리킨다. builder 스테이지만 이 값으로 고정하면 컴파일이 에뮬레이션 없이 네이티브 속도로 돈다. 반면 최종 런타임 스테이지(eclipse-temurin:21-jre-alpine)는 플랫폼 지정을 건드리지 않았기 때문에 buildx가 워크플로에서 넘긴 --platform linux/arm64를 그대로 따라가고, 결과 이미지는 여전히 arm64로 나온다.

바꾸기 전에 걸리는 게 하나 더 있었는데, sqlite-jdbc 의존성이었다. 이건 JNI로 네이티브 바이너리를 호출하는 라이브러리라 크로스 컴파일 환경에서 문제가 될 수 있겠다 싶어서 확인했다. 확인해보니 jar 안에 여러 OS/arch용 네이티브 바이너리를 다 번들해두고 실행 시점 JVM 기준으로 알맞은 걸 골라 쓰는 방식이라, 빌드 머신 아키텍처와는 무관했다. 컴파일 스테이지를 amd64로 돌려도 영향이 없다는 뜻이라 이 부분은 걱정하지 않아도 됐다.

검증은 로컬에서 docker buildx build --platform linux/arm64로 동일한 크로스 빌드 조건을 재현해서 했다. docker image inspect로 최종 이미지 아키텍처가 arm64/linux인 걸 확인했고, compileJava가 에뮬레이션 없이 약 72초 만에 끝나는 것도 확인했다. 기존 CI 로그에서는 builder 스테이지를 포함한 전체가 11분대였던 것과 비교하면 차이가 명확했다. 워크플로 파일(cd.yml)이나 docker-compose.yml, 롤백 로직은 전혀 건드리지 않았다. Dockerfile 한 줄로 리스크를 최소화하는 방향을 택한 거라, 다른 부분에 영향을 줄 여지가 없었다.

PR 자체는 변경 파일이 backend/Dockerfile과 pm/docs/learnings.md 둘뿐이고 diff도 몇 줄 안 되지만, 원인 파악까지 걸린 고민은 이 diff 크기와 비례하지 않았다. 그래서 pm/docs/learnings.md에 이번에 확인한 내용을 정리해뒀다 — 바이트코드로 컴파일되는 언어는 멀티스테이지 빌드에서 컴파일 스테이지를 크로스 타깃에 묶을 필요가 없고, --platform=$BUILDPLATFORM으로 빌드 머신 네이티브에 고정하면 에뮬레이션을 피할 수 있다는 것. 다만 Go나 Rust처럼 네이티브 기계어로 컴파일되는 언어는 이 방식이 그대로 통하지 않고 타깃 아키텍처를 향한 별도 크로스컴파일 플래그가 필요하다는 점도 같이 적어뒀다. 다음에 "왜 이미지를 arm64로 빌드해야 하지"라는 질문이 나오면, 정답은 "최종 런타임 이미지만"이라는 걸 먼저 떠올리기 위한 메모다.

PR은 생성 후 얼마 지나지 않아 병합됐고, 실제 CD 런에서 "이미지 빌드 & 푸시" 스텝 시간이 줄었는지와 stage 헬스체크·스모크 테스트가 기존과 동일하게 통과하는지는 머지 이후 다음 배포에서 Actions 로그로 확인하는 걸로 남겨뒀다.