자바는 소스를 기계어로 바로 굽지 않는다. C나 러스트는 소스를 CPU가 이해하는 네이티브 코드로 곧장 번역해서, 나온 실행 파일이 그 CPU와 운영체제에서만 돈다. 자바는 중간에 한 단계를 더 둔다. javac가 소스를 바이트코드라는 중립 명령어로 바꾸고, JVM(자바 가상 머신)이 실행 시점에 그 바이트코드를 진짜 기계어로 번역해 돌린다. 그래서 같은 .class 파일이 윈도우, 리눅스, 맥 어디서든 JVM만 깔려 있으면 똑같이 돈다. 이걸 "한 번 작성해 어디서나 실행"이라 부르고, 자바가 20년 넘게 기업 시장을 잡고 있는 이유의 절반이 이 구조다.


JDK, JRE, JVM은 껍질이 겹친 구조다. 가장 안쪽에 JVM이 있고, 거기에 표준 라이브러리를 얹은 것이 JRE(실행 환경), 다시 javac와 디버깅 도구까지 얹은 것이 JDK(개발 키트)다. 개발자는 JDK만 깔면 안에 JRE와 JVM이 다 들어 있다. 벤더도 오라클 JDK 말고 Temurin, Amazon Corretto, Azul Zulu 같은 무료 오픈JDK 배포판이 많고 기능은 사실상 같다.


2026년 실무 기준선은 21이나 25다. 자바는 2017년부터 6개월마다 새 버전을 내고, 그중 몇 개를 장기 지원(LTS)으로 지정한다. 8, 11, 17, 21에 이어 최신 LTS는 2025년 9월의 JDK 25다. 최신 버전은 2026년 3월의 JDK 26까지 나왔지만, 회사 서비스는 검증 비용 때문에 보통 LTS에 머문다. 이 시리즈 코드는 record, switch 패턴 매칭, 가상 스레드 같은 21 이상 문법을 쓰고, 버전에 민감한 기능은 몇부터 되는지 표시한다.


가장 작은 프로그램에도 클래스가 있어야 한다. 파이썬이면 print("hi") 한 줄로 끝날 일이 자바에서는 이렇게 된다.


public class Hello {
  public static void main(String[] args) {
    System.out.println("안녕, 자바");
  }
}


이 짧은 코드에 규칙이 다 담겨 있다. 첫째, 모든 코드는 클래스 안에 있어야 한다. 자바에는 클래스 밖에 떠 있는 함수가 없다. 둘째, 시작점은 public static void main(String[] args)라는 정해진 서명의 메서드다. JVM이 프로그램을 켤 때 이 모양의 메서드를 찾아 첫 줄부터 실행한다. public은 밖에서 부를 수 있다는 뜻, static은 객체를 만들지 않고도 부른다는 뜻, void는 돌려주는 값이 없다는 뜻, String[] args는 실행할 때 넘어온 명령행 인자를 담는 배열이다. 지금은 "약속된 진입점"이라고만 알아도 된다.


파일 이름과 클래스 이름은 맞춰야 한다. public으로 선언한 클래스 이름은 파일 이름과 같아야 한다. 위 코드는 Hello.java에 저장한다. 안 맞추면 컴파일이 아예 안 되고, 처음 자바를 만지는 사람이 가장 많이 걸리는 벽이다. 관례상 클래스는 대문자로 시작하는 파스칼 표기(Hello, UserService), 메서드와 변수는 소문자로 시작하는 카멜 표기(getName, userCount)를 쓴다. 문법이 강제하진 않지만 온 세상 자바 코드가 이 관례를 따른다.


컴파일과 실행은 두 단계다. 먼저 컴파일한다.


javac Hello.java


같은 폴더에 바이트코드인 Hello.class가 생긴다. 그다음 실행한다.


java Hello


이때 확장자를 붙이지 않는다. java Hello.class가 아니라 java Hello이고, java 명령이 그 클래스의 main을 찾아 돌린다. 참고로 JDK 11부터는 단일 파일이면 java Hello.java처럼 컴파일을 건너뛰고 바로 실행하는 것도 된다. 학습용에는 편하지만 실제 프로젝트는 빌드 도구가 컴파일과 패키징을 맡는다.


JIT 컴파일이 자바를 빠르게 만든다. 초창기에는 바이트코드를 한 줄씩 해석해 돌려 느렸다. 지금은 JVM 안의 JIT(적시) 컴파일러가 실행 중 자주 도는 코드만 골라 네이티브 기계어로 통째로 번역해 캐시한다. 실제로 어떤 값이 자주 들어오는지 런타임 정보까지 보고 최적화해서, 오래 도는 서버에서는 웜업이 끝나면 C에 근접하는 속도를 낸다. 대신 켜자마자 초기 몇 초는 해석 모드라 느리고 메모리도 넉넉히 먹는다. 짧게 켰다 끄는 명령행 도구에 자바가 불리하고 오래 떠 있는 서버에 유리한 이유가 여기 있다.


메모리는 가비지 컬렉터가 치운다. C나 러스트에서 개발자가 직접 하는 메모리 해제를 자바는 가비지 컬렉터(GC)가 대신한다. 더 이상 아무도 참조하지 않는 객체를 JVM이 알아서 회수하므로, 두 번 해제하거나 이미 해제한 곳을 가리키는 사고가 구조적으로 안 난다. 대신 GC가 도는 순간의 짧은 멈춤과 메모리 사용량을 비용으로 치른다. 러스트가 소유권으로 GC 없이 안전을 얻는 방식과 정확히 대비되는 지점이다.


넓은 표준 라이브러리와 생태계가 진짜 강점이다. 문법만 보면 자바는 화려하지 않고 오히려 장황하다. 그런데 문자열, 컬렉션, 날짜, 파일, 네트워크, 동시성까지 표준 라이브러리가 촘촘하고, 그 위에 스프링을 비롯한 방대한 생태계가 쌓여 있다. 카프카, 엘라스틱서치, 하둡 같은 대형 인프라가 JVM 위에서 돈다. 자바를 배워 두면 일자리가 많다는 말은 언어가 세련돼서가 아니라 이미 세상의 많은 시스템이 이 위에 서 있어서다.


jshell로 문법을 즉석에서 실험할 수 있다. JDK 9부터 jshell이라는 대화형 셸이 들어왔다. 터미널에 jshell을 치면 파이썬 인터프리터처럼 한 줄씩 코드를 입력해 결과를 바로 본다. 클래스와 main을 갖춘 파일을 만들지 않아도 1 + 2"abc".toUpperCase() 같은 표현을 던져 확인할 수 있어서, 이 시리즈 문법을 따라가며 손으로 두들겨 보기에 딱 좋다.


첫날 흔한 실수 몇 가지를 미리 짚는다. 세미콜론을 빠뜨리는 것, public 클래스 이름과 파일 이름을 안 맞추는 것, 대소문자를 틀리는 것(Stringstring으로 쓰면 없는 타입이다), main 서명을 조금이라도 바꿔 시작점을 못 찾는 것. 이 넷은 거의 모두가 밟는다. 자바 컴파일러 오류는 대체로 몇 번째 줄에서 뭐가 문제인지 꽤 친절하게 알려 주니, 겁내지 말고 끝까지 읽는 습관을 들여야 한다.


정적 타입이 주는 것과 뺏는 것이 있다. 자바는 변수마다 타입을 미리 정하고 어긋나면 컴파일 단계에서 막는다. 파이썬처럼 실행해 봐야 타입 오류를 아는 게 아니라 돌리기도 전에 잡힌다. 그만큼 타이핑이 늘고 초반이 답답하지만, 리팩터링할 때 타입이 맞지 않는 곳을 컴파일러가 전부 짚어 주는 경험을 하고 나면 초반의 장황함이 왜 필요했는지 납득하게 된다. 이 안전망의 가치는 프로젝트가 커질수록 커진다.


다음 편에서는 타입과 변수를 다룬다. 자바는 값 하나하나에 타입이 붙는 정적 타입 언어이고, 그 타입이 원시 타입과 참조 타입으로 갈린다. intInteger가 어떻게 다른지, var 타입 추론은 언제 쓰고 오토박싱은 왜 조심해야 하는지를 코드로 뜯어 보겠다. 우선 이번 편의 Hello를 직접 컴파일하고 실행해 보는 것으로 손을 풀어 두면 좋다.