서버 없이 브라우저에서 Java를 실행하기 위해 OpenJDK를 Alpine Linux와 QEMU를 거쳐 WebAssembly로 감싼 JavaBox 프로젝트의 구조, 최적화(스냅샷·지속 JVM), 성능 한계, 그리고 배운 점을 소개합니다.
a java russian nesting doll
서버 없이 브라우저에서 Java 코드를 실행할 수 있을지 궁금했던 적 있나요? Java applet이 그리운가요? 아주 적은 보상을 위해 비교적 긴 시간을 기다리는 걸 좋아하나요?
이쯤 되면 이 글을 읽는 건 미친 사람뿐일 거라 생각하니, 모자 하나 사서 단단히 붙잡고 있으세요.
자, 당신을 위한 프로젝트가 있습니다, buckaroo! 이건 JavaBox입니다. 서버도 없고, JVM 백엔드도 없습니다. 그저 순수하고, 날것 그대로의(사실 unadulterated의 반대말은 뭐죠?) OpenJDK를 Alpine Linux에 쑤셔 넣고, 그걸 WASM 덩어리에 쑤셔 넣고, 그걸 브라우저에 쑤셔 넣었습니다. 비효율의 러시아 인형 같은 거죠.
그래도 좀 멋지지 않나요? 이건 누군가가 “그래, 네가 한번 해봐”라고 말했고 내가 “맥주 좀 들어줘”라고 답한 뒤, 다시 그 사람에게 돌아가 “기술적으로는 됩니다”라고 말한 결과입니다. 실제로 되긴 하는데, 여기서 ‘기술적으로’라는 단어를 정말로 강조해야 할 것 같습니다. 그래도 법 조문 그대로, 저는 실제로 이걸 동작하게 만들었습니다.
브라우저 탭에서 JavaBox를 열면 무슨 일이 일어나는지 같이 보죠. 브라우저가 Cloudflare Worker를 로드합니다. 그 Worker가 227MB짜리 WebAssembly 덩어리를 서빙합니다. 그 덩어리에는 Emscripten으로 컴파일된 QEMU 빌드가 들어 있습니다. QEMU가 Linux 커널을 부팅합니다. Linux 커널이 Alpine Linux 3.21을 부팅합니다. Alpine Linux에는 OpenJDK 21이 깔려 있습니다. 그리고 OpenJDK가 당신의 Java 코드를 컴파일하고 실행합니다.
세어 보면, 브라우저와 System.out.println("Hello World") 사이에 추상화 레이어가 여섯 겹입니다. CPU는 QEMU의 Tiny Code Generator를 통해 x86_64 명령을 에뮬레이션하고, 그 QEMU 자체가 WebAssembly로 컴파일되어 있으며, 그 WebAssembly는 브라우저의 JavaScript 엔진이 JIT 컴파일로 네이티브 머신 코드로 내립니다. 아래로 내려갈수록 에뮬레이션입니다.
이런 건 컴퓨터 과학자들을 울게 만들기도 하고, 또 저로 하여금 계속 밀고 나가고 싶게 만들기도 합니다.
브라우저에서 Java를 실행하는 가장 뻔한 접근은 그냥 컨테이너를 부팅하고, 셸 프롬프트를 기다린 다음, 터미널로 javac Main.java를 보내고, 기다리고, 그다음 java Main을 보내고, 또 기다린 뒤, 출력만 수집하는 겁니다. 저는 이걸 해봤습니다. 하지만 javac를 한 번 호출할 때마다 새로운 JVM을 매번 새로 띄워야 합니다. QEMU TCG WebAssembly 에뮬레이션 아래에서 JVM 시작만 해도 12분이 넘게 걸립니다.
누구도 자신의 for 루프에 세미콜론이 제대로 찍혔는지 보자고 12분을 기다리진 않죠. 그래서 창의적으로 접근해야 했습니다.
해결책은 제가 CompileServer라고 부르는 지속 JVM 데몬이었습니다. 이건 한 번 부팅해서 javax.tools.JavaCompiler로 컴파일러 API를 로드한 뒤, stdin으로 작업이 들어오길 기다리는 Java 프로그램입니다. 소스 코드를 보내면 이미 로드된 컴파일러로 프로세스 안에서(in-process) 컴파일합니다. 컴파일된 클래스를 실행하고 싶으면 URLClassLoader로 로드해서 실행합니다. 같은 JVM입니다. 재시작 없음. 12분 세금 없음.
진짜 재미있는 부분은 이게 스냅샷을 어떻게 버티느냐입니다. container2wasm에는 컨테이너가 부팅을 끝낸 뒤 QEMU 스냅샷을 캡처하는 기능이 있습니다. 브라우저가 WASM 덩어리를 로드하면 처음부터 부팅하는 대신 그 스냅샷에서 복원합니다. 그래서 CompileServer는 제 머신의 빌드 단계에서 시작되고, 스냅샷 속에 얼려졌다가, 당신의 브라우저 탭 안에서 JVM이 이미 달궈진 상태로 다시 깨어납니다.
프로토콜은 엄청 단순합니다. 브라우저가 터미널을 통해 JBOX_PING을 보냅니다. CompileServer가 JBOX_PONG으로 응답합니다. SDK는 이걸로 JVM이 살아있는지 확인합니다. 그리고 컴파일+실행을 위해 JBOX_COMPILE ClassName을 보내고, 그 뒤에 소스 코드를 보내고, JBOX_END를 보냅니다. CompileServer는 컴파일하고, 실행하고, 출력을 찍고, 마지막에 JBOX_EXIT:0(또는 실제 종료 코드)을 출력합니다. 전체가 BufferedReader 기반인데, 더 고급스러운 것(예: JLine)은 스냅샷 복원 이후에 깨지기 때문입니다.
분명히 해두자면, 이건 우아한 엔지니어링이 아닙니다. 이건 덕트 테이프와 기도입니다. 하지만 컴파일+실행 시간을 12분에서 약 35초로 줄였고, 이건 20배 개선이라 저는 자랑스럽게 생각하기로 했습니다.
성능 이야기를 해봅시다. 이게 어디쯤 서 있는지 솔직해지는 게 중요하다고 생각합니다.
페이지 로드부터 CompileServer 준비 완료까지 부팅 시간은 약 20초입니다. QEMU 스냅샷 복원과 Linux 기동 시간입니다. HelloWorld를 처음 컴파일하고 실행하는 데는 그 위에 약 35초가 더 걸립니다. 그러니 페이지를 여는 순간부터 터미널에 “Hello World”를 보기까지 대략 55초쯤 걸린다고 보면 됩니다.
빠른가요? 아니요. 프로덕션 IDE로 쓸 만한가요? 절대 아니요. 12분짜리 대안보다 빠른가요? 훨씬요.
게스트는 RAM 128MB를 받고, JVM 힙은 -Xmx64m로 64MB로 제한되어 있습니다. JIT 컴파일은 -Xint로 꺼놨습니다. 소프트웨어 CPU 에뮬레이터 위에서 이미 돌아가고 있는데 Java 바이트코드를 JIT로 컴파일해봐야 의미가 없으니까요. JIT 출력물은 QEMU가 에뮬레이트해야 할 x86 명령이 더 늘어날 뿐입니다. 거북이가 거북이를 컴파일하는 꼴입니다.
우리는 Java 문서 사이트에서 JavaBox를 써보는 걸 탐색 중입니다. 예를 들어 HashMap이나 제네릭 같은 Java 개념을 읽고 있을 때, 페이지 안에 “Try It” 버튼이 바로 있는 겁니다. 클릭합니다. 예제 코드를 수정합니다. 실행을 누릅니다. 출력을 봅니다. JDK 설치 없음. IDE 설정 없음. 어디선가 서버가 당신의 코드를 처리하는 것도 없음.
이 55초짜리 첫 실행 시간은 문제입니다. 하지만 누군가 문서 페이지에 들어와서 읽기 시작하면, 설명을 읽는 동안 백그라운드에서 컨테이너를 부팅할 수 있습니다. 실제로 “Try It”을 클릭하고 뭔가 타이핑할 즈음엔 CompileServer가 이미 달궈져 있을지도 모릅니다. 아마도요. 이론은 그렇습니다. 실제가 어떻게 될지는 봐야죠.
또 다른 아이디어는 공유 가능한 스니펫입니다. Java를 좀 작성하고 URL을 받아서 누군가에게 보냅니다. 상대가 열면 그 코드가 브라우저에서 실행됩니다. 유지할 백엔드가 없습니다. 사용자 수에 따라 늘어나는 서버 비용도 없습니다. 각 사용자의 브라우저가 자기 계산을 직접 합니다. 가장 문자 그대로이면서도 가장 터무니없는 의미에서의 serverless죠.
지금 바로 라이브 데모가 떠 있습니다. javabox-demo.brian-fec.workers.dev로 가서 기다리세요. 그리고 조금 더 기다리세요. 그러면 터미널이 보일 겁니다. Java를 좀 입력하고 컴파일해보세요. 아니면 말고요. 제가 당신 상사는 아니니까요.
코드는 GitHub의 github.com/bmarti44/javabox에 있습니다. README에 직접 전체 과정을 밟아보고 싶은 사람을 위한 빌드 안내가 있는데, Rancher Desktop, Colima(Apple Silicon이라면), container2wasm, 그리고 제가 원래는 갖고 있지 않았지만 이후에 개발하게 된 수준의 인내심이 필요합니다.
기술적으로는 인상적이지만 실용적으로는 쓸모없는 걸 만드는 건, 무언가를 배우는 과소평가된 방법입니다. 저는 이제 QEMU, WebAssembly 메모리 모델, cross-origin isolation 헤더, Java compiler API에 대해 예상보다 훨씬 더 많이 이해하게 됐습니다. SharedArrayBuffer가 대부분의 개발 서버가 설정하지 않는 특정 HTTP 헤더를 요구한다는 것도 배웠습니다. container2wasm의 스냅샷 기능이 진짜로 영리하다는 것도, c2w-net 네트워킹 프록시가 브라우저에서 가능하다고 생각하지 못했던 일을 한다는 것도 배웠습니다. JVM이 콜드 스타트에는 믿을 수 없을 만큼 무겁지만, 그냥 계속 돌려두면 놀랄 만큼 협조적이라는 것도 배웠습니다.
또, 227MB는 브라우저로 보내기엔 엄청난 데이터라는 것도 배웠습니다. 진짜로, 엄청 엄청요. 더 압축할 수도 있고, 더 똑똑하게 청킹할 수도 있습니다. 하지만 어느 순간엔, 브라우저 탭 안에 운영체제 전체를 넣어버렸다는 사실을 받아들이고 인생을 계속 살아가야 합니다.
JavaBox가 서버 사이드 Java 컴파일을 대체할까요? 아니요. 실제 서버를 쓰는 온라인 IDE와 경쟁할까요? 아니요. 2026년에 브라우저가 할 수 있는 일을 진짜 흥미롭게 보여주는 재미있는 proof of concept인가요? 저는 그렇다고 생각합니다.
그리고 다른 게 없더라도, 다음에 누가 “이제 브라우저에서 Java는 못 돌리잖아”라고 말하면, 저는 링크를 보내줄 수 있습니다. 55초를 기다려야 하긴 하겠지만, 결국엔 도착할 겁니다. 언젠가는요.
Brian Martin은 Oracle의 Senior Principal Developer입니다. 코드는 github.com/bmarti44/javabox에 있습니다. 라이브 데모는 javabox-demo.brian-fec.workers.dev에 있습니다. 이런 종류의 것에 관심이 있다면 구독해 주세요. 제가 만드는 이상한 것들에 대해 계속 쓸 예정입니다.