![[백엔드 언어 01] Java - JDK와 첫 실행 구조](https://img.thenullpage.com/posts/8225/8225_1_911dfb.webp)
자바는 서버 쪽에서 오래 버틴 언어다. 은행 시스템도, 쇼핑몰 결제 서버도, 안드로이드 앱도 여전히 자바 위에서 돌아간다. 한 번 짠 코드가 리눅스에서도 윈도우에서도 그대로 돌아가고, 문법이 크게 흔들리지 않아 오래된 코드도 대체로 그대로 읽힌다. 이 시리즈는 자바 문법을 실행 가능한 코드로 하나씩 짚어 나간다. 첫 편은 자바가 어떤 구조로 돌아가는지, 첫 프로그램을 어떻게 쓰고 실행하는지부터 잡는다. 파이썬을 먼저 배운 사람이라면 자바 문법이 유난히 장황하게 느껴질 수 있는데, 그 장황함의 대가로 무엇을 얻는지도 같이 짚는다.
JDK, JRE, JVM. JDK는 컴파일러와 디버거를 포함한 개발 도구 패키지다. JRE는 개발 도구를 빼고 실행에만 필요한 JVM과 표준 라이브러리를 담는다. JVM은 자바 가상 머신, 바이트코드를 읽어 실제로 실행하는 프로그램이다. 요즘은 JRE만 따로 설치하는 경우가 줄었고 대부분 JDK 하나로 개발과 실행을 함께 처리한다. 오라클 배포판과 오픈JDK 계열 중 어느 쪽을 받아도 같은 명세를 따르므로 문법이나 표준 라이브러리 동작에는 차이가 없다. 파이썬은 소스 코드를 그 자리에서 한 줄씩 해석하며 실행하지만, 자바는 중간 단계가 하나 더 있다. 소스 파일을 컴파일러가 바이트코드로 먼저 바꾸고, 그 바이트코드를 JVM이 실행한다. 이 중간 형태 덕분에 같은 바이트코드 파일이 윈도우에서도 맥에서도 리눅스에서도 똑같이 실행된다. 운영체제 차이는 JVM이 흡수하고 개발자가 짠 코드는 손댈 필요가 없다.
첫 프로그램. 자바는 파일 이름이 그 안에 있는 공개 클래스 이름과 반드시 같아야 한다. Hello 클래스를 만들면 파일 이름도 Hello.java여야 한다.
public class Hello {
public static void main(String[] args) {
System.out.println("안녕하세요, 자바");
}
}
컴파일하고 실행하는 명령은 이렇다.
javac Hello.java
java Hello
// 출력: 안녕하세요, 자바
javac는 Hello.class라는 바이트코드 파일을 만들고, java 명령은 그 파일을 읽어 JVM 위에서 돌린다. 확장자 없이 클래스 이름만 적어야 하며, java Hello.class라고 적으면 실행이 안 된다. 이 서명 한 줄에 자바의 실행 모델이 다 들어 있다. public은 JVM이 클래스 바깥에서 이 메서드를 불러야 하니 붙는다. static은 객체를 만들지 않고도 클래스 자체에서 바로 부를 수 있다는 뜻이고, JVM은 프로그램을 시작할 때 아직 어떤 인스턴스도 만들지 않은 상태라 static이 아니면 부를 방법이 없다. void는 돌려주는 값이 없다는 뜻이고, String[] args는 명령행에서 넘어오는 문자열 배열을 받는 자리다. 파이썬의 if __name__ == "__main__": 도 진입점을 표시하지만 관례일 뿐 강제는 아니다. 자바는 이 서명을 컴파일러가 강제한다. 자바에는 최상위 함수라는 개념도 없어서, 출력문 하나를 실행하려 해도 반드시 클래스 안에 담아야 한다.
출력과 정수 나눗셈, 파이썬과 비교. println은 값을 찍고 줄을 바꾸고, print는 줄을 바꾸지 않는다.
System.out.println("hello");
// 파이썬
print("hello")
자바는 문장 끝에 세미콜론을 붙이고 블록은 중괄호로 묶는다. 파이썬은 세미콜론이 없고 들여쓰기가 블록의 경계다. 자바는 컴파일 시점에 타입이 확정되는 정적 타입 언어라 변수를 쓰기 전에 타입을 적어야 하고, 파이썬은 실행하며 타입이 정해지는 동적 타입이라 타입 표시가 없다. 파이썬은 빨리 짤 수 있지만 타입을 잘못 넘긴 실수가 실행 중에야 드러나고, 자바는 코드가 조금 길어지는 대신 컴파일러가 그런 실수를 미리 잡아 준다. 값을 나눌 때도 차이가 난다.
int a = 7;
int b = 2;
System.out.println(a / b);
double c = 7.0 / 2;
System.out.println(c);
// 출력:
// 3
// 3.5
정수끼리 나누면 소수점 아래가 그냥 버려진다. 파이썬 3은 / 연산자가 항상 실수를 돌려주고 정수 나눗셈은 //로 따로 쓴다. 이 규칙을 모르고 평균을 구하면 소수점이 통째로 사라지는 버그를 만난다. 한쪽만 double로 바꿔도 계산 전체가 실수로 돈다.
반복문도 결이 다르다.
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
}
System.out.println(sum);
// 출력: 15
// 파이썬
total = 0
for i in range(1, 6):
total += i
print(total)
자바는 초기값과 조건, 증가식을 다 적어야 하는 전통적인 for문을 쓴다. 파이썬은 range로 범위를 만들어 그대로 순회하니 증가식을 따로 적지 않는다. 자바에도 향상된 for문이 있어서 배열이나 컬렉션을 인덱스 없이 값만 꺼내며 순회할 수 있다.
int[] nums = {10, 20, 30};
int total2 = 0;
for (int n : nums) {
total2 += n;
}
System.out.println(total2);
// 출력: 60
이 형태가 파이썬의 for i in nums와 느낌이 가장 비슷하다.
JVM이 하는 일. JVM은 바이트코드를 읽어 기계어로 바꿔 실행한다. 매번 한 줄씩 해석만 하면 느려서, 자주 실행되는 부분은 JIT 컴파일러가 통째로 기계어로 미리 번역해 속도를 끌어올린다. 메모리 관리도 JVM 몫이라, 더 이상 아무도 참조하지 않는 객체는 가비지 컬렉터가 알아서 찾아 회수한다. C처럼 확보한 메모리를 직접 반납할 필요가 없지만, GC가 도는 순간 프로그램이 잠깐 멈추는 일시정지가 생길 수 있다. 이 특성 때문에 자바는 짧게 끝나는 스크립트보다 오래 켜 두고 요청을 계속 받는 서버 프로그램에 더 잘 맞는다.
클래스 이름과 파일 이름을 다르게 저장하거나 세미콜론을 빠뜨리는 실수는 자바를 처음 만지는 사람이 가장 자주 걸리는 자리다. 오류 메시지는 대개 줄 번호와 함께 무엇이 빠졌는지 알려주니, 당황하지 말고 그 줄부터 확인하면 된다.
이번 편에서 손에 쥐어야 할 감각은 세 가지다. 소스는 컴파일러가 바이트코드로 바꾸고 그것을 JVM이 실행한다는 것, public static void main이 프로그램의 유일한 시작점이라는 것, 그리고 정수 나눗셈처럼 자바가 파이썬과 갈라지는 지점이 어디인지. 문법을 외우려 하기보다 짧은 코드를 직접 쳐서 컴파일하고 돌려 보는 편이 훨씬 빨리 는다.