Java
고친 사람 github-actions[bot]
Java 는 한 번 짜 둔 프로그램을 운영체제를 가리지 않고 돌려 주는 프로그래밍 언어입니다. 백엔드에서는 서버 애플리케이션을 짜는 데 널리 쓰입니다. 다 쓴 메모리는 코드를 돌리는 실행 프로그램이 알아서 치웁니다.
쉽고 빠른 이해
Java 는 한 번 컴파일한 파일을 리눅스에서도 윈도우에서도 고치지 않고 돌리게 해 주는 언어입니다. 백엔드에서는 웹 서버 애플리케이션을 짜는 데 흔히 씁니다.
기계마다 알아듣는 명령이 다릅니다. 프로그램이 그 차이를 떠안으면 운영체제마다 빌드를 따로 해야 합니다. Java 는 그 차이를 실행 프로그램 하나에 몰아 두었습니다.
어떻게 도는가:
- 소스 파일을 컴파일해 어느 기계에서나 같은 중간 파일로 바꿉니다
- 기계마다 깔린 실행 프로그램이 그 파일을 읽어 돌립니다
- 다 쓴 메모리는 실행 프로그램이 알아서 치웁니다
대가도 있습니다. 실행 프로그램이 먼저 떠야 해서 켜는 데 시간이 걸립니다. 변수마다 타입을 적어야 해서 코드가 길어집니다. 그래서 몇 초 만에 끝나는 작은 스크립트에는 잘 안 씁니다.
상세
Java 는 프로그래밍 언어입니다. 이 절은 Java 코드가 어떤 길을 거쳐 실행되는지부터 봅니다. 그다음 짧은 코드 조각으로 클래스·타입·예외·메모리를 하나씩 봅니다. 마지막에 언제 Java 를 고르고 언제 안 고르는지를 적습니다.
소스에서 실행까지
Java 코드는 .java 로 끝나는 소스 파일에 적습니다. 이 파일은 바로 실행되지 않습니다. 먼저
javac 라는 컴파일러가 소스를 읽어 클래스 파일로 바꿉니다. 앞에서 말한 중간 파일이 이
클래스 파일입니다.
클래스 파일 안에는 바이트코드가 들어 있습니다. 바이트코드는 실제 칩이 아니라 가상의 컴퓨터가 알아듣도록 정한 명령입니다. 그 가상의 컴퓨터를 JVM(Java Virtual Machine, 자바 가상 머신)이라고 부릅니다. 앞에서 기계마다 깔린다고 한 실행 프로그램이 바로 JVM 입니다.
JVM 은 운영체제마다 따로 만들어져 있습니다. 반면 클래스 파일은 어디서 만들었든 같은 바이트입니다. 그래서 한 번 컴파일한 파일을 리눅스의 JVM 에도, 윈도우의 JVM 에도 넣을 수 있습니다.
flowchart TD
A["소스 파일 · Hello.java"] --> B["javac · 컴파일러"]
B --> C["클래스 파일 · Hello.class"]
C --> D["리눅스의 JVM"]
C --> E["윈도우의 JVM"]
C --> F["맥의 JVM"]
컴파일은 한 번뿐입니다. 기계마다 갈리는 것은 JVM 쪽입니다.
JVM 은 처음에는 바이트코드를 한 명령씩 해석하며 돌립니다. 그러다 자주 도는 메서드를 골라 칩이 바로 알아듣는 기계어로 번역해 둡니다. 이것을 JIT(Just In Time) 컴파일이라고 부릅니다. 그래서 Java 서버는 뜬 직후보다 한동안 요청을 받은 뒤에 응답 시간이 더 짧아집니다.
설치하는 꾸러미와 Java 라는 이름
Java 로 개발하려면 JDK(Java Development Kit, 자바 개발 도구 모음)를 설치합니다. 그 안에 javac 같은 개발 도구와 JVM 이 함께 들어 있습니다.
개발은 하지 않고 이미 만든 클래스 파일을 돌리기만 하는 기계도 있습니다. 거기에 필요한 것만 추린 묶음을 JRE(Java Runtime Environment, 자바 런타임 환경)라고 부릅니다. JRE 에는 JVM 은 들어 있지만 javac 는 없습니다.
설치 꾸러미와 별개로, 「Java」라는 이름 자체도 여러 뜻으로 쓰입니다. 언어·JVM·표준 라이브러리 셋을 한꺼번에 가리킬 때가 많습니다. 대화에서 무엇을 말하는지는 문맥으로 가립니다.
| 가리키는 것 | 무엇인가 |
|---|---|
| 언어 | 소스 코드를 적는 문법과 규칙 |
| JVM | 클래스 파일을 받아 실행하는 가상의 컴퓨터 |
| 표준 라이브러리 | 문자열·컬렉션·파일·네트워크를 다루는 기본 클래스 묶음 |
언어와 JVM 은 서로 떨어져 있습니다. JVM 은 클래스 파일만 읽습니다. 소스가 무슨 언어였는지는 보지 않습니다. 그래서 Kotlin 이나 Scala 로 짠 코드도 클래스 파일로 컴파일하면 JVM 위에서 돕니다.
모든 코드는 클래스 안에
Java 코드는 전부 클래스 안에 들어갑니다. 클래스는 데이터와 그 데이터를 다루는 함수를 한 단위로 묶은 설계도입니다.
클래스 안에 든 함수는 메서드라고 부릅니다. 아래에 나오는 main 도 메서드 하나입니다.
클래스로 찍어 낸 실물 하나를 객체라고 합니다. 예를 들어 Order 클래스는 주문 한 건이 무엇을
갖는지 정합니다. 주문 한 건 한 건이 그 클래스의 객체입니다. 이렇게 객체를 중심으로 짜는 방식을
객체 지향 프로그래밍이라고 부릅니다.
프로그램이 시작하는 곳도 정해져 있습니다. 클래스 안에 main 이라는 메서드를 두면 JVM 이
거기서부터 실행합니다. 아래 코드를 실행하면 화면에 hi 가 찍힙니다.
public class Hello {
public static void main(String[] args) {
System.out.println("hi"); // hi
}
}
main 앞에 붙은 낱말 셋은 이 메서드를 JVM 이 부를 수 있게 하는 표시입니다.
| 낱말 | 뜻 |
|---|---|
public |
클래스 밖에서도 부를 수 있다 |
static |
객체를 만들지 않고 클래스에서 바로 부를 수 있다 |
void |
돌려주는 값이 없다 |
JVM 이 막 시작한 때에는 아직 아무 객체도 없습니다. 그래서 main 에는 static 이 붙어야 합니다.
타입을 미리 적는 언어
Java 는 변수를 만들 때 그 변수에 담을 값의 종류를 적습니다. 이 종류를 타입이라고 부릅니다.
정수를 담을 변수에는 int 를, 문자열을 담을 변수에는 String 을 붙입니다.
int count = 3;
String name = "java";
System.out.println(name + count); // java3
위 코드는 타입이 맞아서 그대로 돕니다. 이번에는 정수 변수에 문자열을 넣어 봅니다.
int count = 3;
count = "three"; // 컴파일 오류
이 코드는 실행까지 가지 못합니다. 컴파일러가 실행 전에 타입을 검사해 여기서 막습니다. 이렇게 실행 전에 타입을 확인하는 방식을 정적 타입이라고 부릅니다.
이 검사가 있는 까닭은 타입 실수를 서버에 올리기 전에 잡으려는 것입니다. Python 처럼 실행 중에 타입이 정해지는 언어에서는 같은 실수가 그 줄이 실행되는 순간에야 드러납니다. 대가는 적을 것이 늘어난다는 점입니다.
기본 타입과 참조 타입
Java 의 타입은 두 갈래입니다. int·long·boolean 같은 기본 타입 변수에는 값 자체가 들어
있습니다. 그 밖의 타입은 전부 참조 타입입니다.
참조 타입 변수에는 객체 자체가 아니라 그 객체를 가리키는 값이 들어 있습니다. 이 차이는 변수를 다른
변수에 넣을 때 드러납니다. 아래에서 int[] 는 정수를 담는 배열입니다. 배열은 참조 타입입니다.
int a = 1;
int b = a;
b = 2;
System.out.println(a); // 1
int[] x = {1};
int[] y = x;
y[0] = 2;
System.out.println(x[0]); // 2
b 를 바꿔도 a 는 1 입니다. 값이 복사됐기 때문입니다. 반면 y 를 고치자 x 쪽에서도 2 가
보입니다. x 와 y 가 같은 배열 하나를 가리키고 있어서입니다.
flowchart TD
subgraph V["변수"]
X["x"]
Y["y"]
end
X --> ARR["배열 하나 · 첫 칸 2"]
Y --> ARR
참조 타입 변수는 아무 객체도 가리키지 않을 수도 있습니다. 그 상태를 null 이라고 부릅니다.
변수에 null 을 넣으면 이 상태가 됩니다.
null 인 변수로 메서드를 부르면 실행 중에 NullPointerException 이라는 오류가 납니다. 메서드를 실행할 객체가 없기 때문입니다.
String s = null;
s.length(); // NullPointerException
이 코드는 컴파일을 통과합니다. 변수가 null 인지는 대개 실행해 봐야 알기 때문입니다.
예외와 throws
Java 는 실패를 예외라는 객체로 알립니다. 예외가 나면 그 줄에서 실행이 멈춥니다. 예외는 메서드를
부른 쪽으로 거슬러 올라갑니다. 그러다 try·catch 로 받아 둔 곳에서 처리됩니다.
Java 에는 다른 언어에 드문 규칙이 하나 있습니다. 파일 열기처럼 실패가 예상되는 일은
검사 예외를 던집니다. 검사 예외를 던지는 메서드를 부르는 쪽은 catch 로 받거나, 메서드 머리에
throws 로 적어 떠넘겨야 합니다.
아래 코드의 new 는 클래스로 객체를 만드는 낱말입니다. new FileReader("a.txt") 는 파일을 읽는
객체를 만듭니다. 파일이 없으면 검사 예외인 IOException 의 한 종류를 던집니다.
void open() throws IOException {
new FileReader("a.txt");
}
여기서 throws IOException 을 지우면 이 메서드는 컴파일되지 않습니다. 파일이 없을 수 있다는 사실을
모른 채 지나가지 못하게 컴파일러가 막는 것입니다. 대가는 받을 생각이 없는 코드에도 이 표시를 적어야
한다는 점입니다.
앞의 NullPointerException 은 검사 예외가 아닙니다. 그래서 throws 로 적지 않아도 컴파일됩니다.
메모리를 치우는 가비지 컬렉션
new 로 객체를 만들면 JVM 이 힙이라는 메모리 구역에서 공간을 내줍니다. 힙은 객체들이 사는
곳입니다. 객체가 늘어날수록 힙에서 쓰는 공간도 늘어납니다.
그런데 Java 코드에는 그 공간을 돌려주는 명령이 없습니다. 어떤 변수도 가리키지 않게 된 객체는 JVM 이 찾아서 치웁니다. 이 일을 가비지 컬렉션이라고 부릅니다.
이렇게 만든 까닭은 메모리를 손으로 반납하다 생기는 실수를 없애려는 것입니다. C 같은 언어에서는 반납을 잊으면 메모리가 계속 쌓입니다. 이미 반납한 메모리를 다시 쓰면 프로그램이 엉뚱하게 동작합니다.
대가도 있습니다. 치우는 때를 JVM 이 정하므로 그 순간 프로그램이 잠깐 멈출 수 있습니다. 또 누군가 아직 가리키고 있는 객체는 치우지 않습니다. 그래서 다 쓴 객체를 목록에 계속 담아 두면 Java 에서도 메모리 누수가 생깁니다.
Java 가 맞는 곳과 안 맞는 곳
Java 는 오래 떠 있는 서버 프로그램과 잘 맞습니다. JVM 을 켜는 비용은 한 번만 냅니다. 오래 돌수록 JIT 컴파일이 자주 도는 코드를 기계어로 바꿔 둡니다.
쓰는 사람이 많아 서버 개발에 필요한 도구가 이미 갖춰져 있습니다. Spring 같은 웹 프레임워크, 데이터베이스에 붙는 JDBC(Java Database Connectivity, 자바 데이터베이스 연결), 빌드 도구 Gradle 이 그 예입니다. 부하 테스트 도구 Gatling 도 시나리오를 Java 코드로 적을 수 있습니다.
몇 초 만에 끝나는 명령줄 도구에는 잘 안 맞습니다. JIT 컴파일이 붙기 전에 일이 끝나서 JVM 을 켜는 시간만 떠안습니다. 메모리가 적은 기기에서도 JVM 이 차지하는 몫이 부담이 됩니다.
관련 항목
Java 코드가 실행되기까지 거치는 구성 요소
JVM · javac · 클래스 파일 · 바이트코드 · JIT 컴파일 · 클래스패스 · JAR
Java 를 담아 배포하는 설치 꾸러미
JDK · JRE · OpenJDK · Java SE · 표준 라이브러리
Java 언어를 이루는 문법 요소
클래스 · 객체 · 메서드 · 타입 · 기본 타입 · 참조 타입 · 제네릭 · 어노테이션 · 람다 표현식
Java 프로그램에서 자주 나는 예외와 오류
예외 · 검사 예외 · null · NullPointerException · OutOfMemoryError · StackOverflowError · ClassNotFoundException
Java 가 메모리를 관리하는 장치
Java 서버 개발에 쓰이는 프레임워크와 빌드 도구
Spring · Spring Boot · JDBC · Hibernate · Maven · Gradle
Java 코드를 시험하는 테스트 도구
JVM 위에서 함께 도는 프로그래밍 언어
Kotlin · Scala · Groovy · Clojure
서버 개발을 두고 Java 와 겨루는 언어
Python · Go · C++ · JavaScript · Rust
Java 가 속하는 상위 분류
다른 이름: 자바 · 자바 언어