Java 시작하기 | JDK 설치부터 Hello World까지
이 글의 핵심
Java를 처음 설치하면 JDK와 JRE 중 무엇이 필요한지, PATH와 JAVA_HOME은 왜 잡아야 하는지부터 헷갈립니다. 운영체제별 설치와 명령줄 컴파일·실행을 직접 해 보며 한 번 작성해 어디서나 실행된다는 구조를 이해하고, IDE 설정 후 기본 문법과 클래스까지 첫 프로그램을 완성하는 흐름으로 구성했습니다.
시리즈 안내
#01 | 📋 전체 목차 | 다음: #02 변수와 타입
들어가며
Java는 1995년 Sun Microsystems(현 Oracle)가 내놓은 객체지향(데이터와 동작을 객체로 묶어 설계하는 방식) 언어입니다. Write Once, Run Anywhere(WORA)는 같은 바이트코드를 JVM(자바 바이트코드를 실행하는 가상 머신)이 있는 환경에서 실행한다는 뜻입니다. 소스 코드를 곧바로 기계어로 바꾸는 C/C++과 달리, javac는 특정 CPU에 묶이지 않은 중간 형태(바이트코드)를 만들고, 실제 기계어 변환은 각 운영체제용 JVM이 실행 시점에 맡습니다. 그래서 개발자는 운영체제별 빌드를 따로 만들 필요가 없지만, 대신 실행하는 쪽에 맞는 버전의 JVM이 설치되어 있어야 한다는 조건이 생깁니다. 입문 단계에서 겪는 오류 상당수가 이 “JVM 버전·경로” 문제에서 나옵니다.
클래스는 객체의 설계도, 인스턴스는 그 설계도로 만든 실체라는 관점이 이후 글 전체와 연결됩니다.
Java의 핵심 특징:
- 플랫폼 독립적: 소스를 바이트코드로 컴파일해 JVM(Java Virtual Machine)에서 실행
- 객체지향: 클래스 기반 (단,
int·boolean같은 기본 타입은 객체가 아님) - 가비지 컬렉션: 더 이상 참조되지 않는 객체를 런타임이 회수
- 정적 타입: 타입 오류 상당수를 컴파일 타임에 잡음
- 표준 라이브러리: 컬렉션, I/O, 네트워크, 동시성 API가 기본 제공
- 멀티스레딩: 언어·라이브러리 차원의 스레드 지원
Java가 사용되는 곳: Spring 기반의 엔터프라이즈 서버, Android 앱, Servlet·Spring Boot 웹 애플리케이션, Hadoop·Spark 같은 빅데이터 도구, 그리고 Minecraft(Java Edition) 같은 게임까지 넓게 쓰입니다.
JDK·JRE·JVM 차이와 OS별 JDK 설치
JDK vs JRE vs JVM
| 구분 | 설명 | 포함 |
|---|---|---|
| JVM | Java Virtual Machine (실행 엔진) | 바이트코드 실행 |
| JRE | Java Runtime Environment (실행 환경) | JVM + 라이브러리 |
| JDK | Java Development Kit (개발 도구) | JRE + 컴파일러(javac) + 디버거 |
개발자는 JDK 설치 필수!
예전에는 “실행만 할 사람은 JRE, 개발자는 JDK”로 나눠 배포했지만, Java 11부터 Oracle과 대부분의 배포판이 별도의 JRE 설치 파일을 더 이상 기본으로 제공하지 않습니다. 배포할 때는 jlink로 애플리케이션에 필요한 모듈만 담은 런타임을 만들거나 JDK를 그대로 쓰는 방식이 일반적이라, 지금 입문하는 입장에서는 “JDK 하나만 설치하면 된다”고 생각해도 충분합니다.
JDK 배포판도 여러 가지입니다. Oracle JDK, Eclipse Temurin(Adoptium), Amazon Corretto, Azul Zulu, Microsoft Build of OpenJDK 등은 모두 같은 OpenJDK 소스에서 빌드되므로 언어 기능과 표준 라이브러리는 동일하고, 차이는 라이선스 조건과 보안 패치 제공 기간에 있습니다. Oracle JDK는 버전마다 라이선스 조건이 바뀌어 온 이력이 있어 회사에서 쓸 때는 해당 버전의 조건을 확인해야 하므로, 학습용·개인 프로젝트라면 Temurin 같은 무료 배포판을 고르는 것이 가장 부담이 적습니다.
Windows 설치
1. JDK 다운로드:
- Oracle JDK (버전별 라이선스 조건 확인 필요)
- Eclipse Temurin (OpenJDK) (무료, 권장)
2. 설치:
# 다운로드한 설치 프로그램 실행
# 설치 경로: C:\Program Files\Java\jdk-21
# 환경 변수 자동 설정 (또는 수동)
JAVA_HOME=C:\Program Files\Java\jdk-21
PATH=%JAVA_HOME%\bin;%PATH%
3. 설치 확인:
java -version
# 출력:
# openjdk version "21.0.1" 2023-10-17
# OpenJDK Runtime Environment (build 21.0.1+12-29)
# OpenJDK 64-Bit Server VM (build 21.0.1+12-29, mixed mode, sharing)
javac -version
# 출력:
# javac 21.0.1
java -version은 되는데 javac -version에서 'javac'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.가 나온다면, PATH에 JDK가 아니라 다른 경로의 java.exe만 잡혀 있는 경우가 많습니다. 일부 설치 프로그램은 C:\Program Files\Common Files\Oracle\Java\javapath처럼 java만 들어 있는 폴더를 PATH 앞쪽에 추가하기 때문입니다. where java와 where javac로 실제 어느 파일이 실행되는지 확인하고, %JAVA_HOME%\bin이 PATH에서 더 앞에 오도록 순서를 조정하면 됩니다. 환경 변수를 바꾼 뒤에는 이미 열려 있던 터미널에는 반영되지 않으므로 새 터미널을 열어야 한다는 점도 자주 놓칩니다. JAVA_HOME은 java 명령 자체에는 필요 없지만 Maven, Gradle, IDE, Tomcat 같은 도구가 JDK 위치를 찾을 때 이 변수를 읽으므로 처음부터 잡아 두는 것이 좋습니다.
macOS 설치
# Homebrew 사용
brew install openjdk@21
# 심볼릭 링크 생성
sudo ln -sfn /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-21.jdk
# 확인
java -version
Homebrew의 openjdk@21은 macOS 기본 Java와 충돌하지 않도록 “keg-only”로 설치되어, 위의 심볼릭 링크를 만들기 전에는 java -version이 Unable to locate a Java Runtime. 같은 메시지를 낼 수 있습니다. 링크를 만들면 macOS의 /usr/libexec/java_home 도구가 이 JDK를 인식하므로, export JAVA_HOME=$(/usr/libexec/java_home -v 21)처럼 버전을 지정해 여러 JDK를 오갈 수 있습니다. Intel Mac은 Homebrew 경로가 /opt/homebrew가 아니라 /usr/local이라는 점도 주의합니다.
Linux 설치
# Ubuntu/Debian
sudo apt update
sudo apt install openjdk-21-jdk
# CentOS/RHEL
sudo yum install java-21-openjdk-devel
# 확인
java -version
javac -version
Ubuntu에서는 openjdk-21-jre와 openjdk-21-jdk 패키지가 나뉘어 있어서, JRE만 설치된 서버에서는 java는 되는데 javac: command not found가 납니다. 여러 버전을 설치했다면 sudo update-alternatives --config java(와 javac)로 기본 버전을 고를 수 있습니다.
Hello World 컴파일하고 실행하기
HelloWorld.java 작성
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
코드 설명:
public class HelloWorld: 공개 클래스 선언 (파일명과 동일해야 함)public static void main(String[] args): 프로그램 진입점public: 어디서나 접근 가능static: 객체 생성 없이 호출 가능void: 반환값 없음String[] args: 명령줄 인자
System.out.println(): 콘솔 출력
main의 시그니처가 이렇게 길고 정해진 모양인 이유는, java 런처가 클래스를 읽은 뒤 정확히 이 이름과 형태의 메서드를 찾아 호출하기 때문입니다. 객체를 만들기 전이므로 static이어야 하고, 런처가 클래스 밖에서 부르므로 public이어야 했습니다. 다만 Java 25(LTS)부터는 JEP 512(간결한 소스 파일과 인스턴스 main 메서드)가 정식 기능이 되어, 입문용으로 void main() { IO.println("Hello, Java!"); }처럼 클래스 선언 없이 쓰는 것도 허용됩니다. 이 글은 여전히 가장 널리 쓰이는 전통적인 형태를 기준으로 설명하지만, 최신 JDK에서는 뒤의 “main 메서드 시그니처 오류” 예제가 오류 없이 실행될 수 있다는 점을 알아 두세요.
컴파일 및 실행
# 1. 컴파일 (javac)
javac HelloWorld.java
# → HelloWorld.class 생성 (바이트코드)
# 2. 실행 (java)
java HelloWorld
# 출력: Hello, Java!
동작 원리:
HelloWorld.java (소스 코드)
↓ javac (컴파일)
HelloWorld.class (바이트코드)
↓ java (실행)
JVM (플랫폼별 실행)
↓
출력: Hello, Java!
실행할 때 java HelloWorld.class처럼 확장자를 붙이면 Error: Could not find or load main class HelloWorld.class가 납니다. java 명령이 받는 것은 파일 경로가 아니라 클래스 이름이기 때문입니다. 또 클래스에 package com.example; 선언이 있다면 .class 파일이 com/example/ 폴더 구조 안에 있어야 하고, 실행도 그 상위 폴더에서 java com.example.HelloWorld로 해야 합니다. 이 규칙을 모르면 파일은 분명히 있는데 클래스를 못 찾는다는 오류를 한참 들여다보게 됩니다.
Java 11부터는 java HelloWorld.java처럼 소스 파일을 바로 넘기면 메모리에서 컴파일해 곧장 실행하는 “단일 파일 소스 실행”도 지원합니다. 짧은 실험에는 편하지만 .class 파일이 남지 않으므로, 컴파일과 실행이 분리된다는 구조를 이해하려면 처음 몇 번은 javac와 java를 따로 써 보는 편이 좋습니다.
명령줄 인자
public class Args {
public static void main(String[] args) {
System.out.println("인자 개수: " + args.length);
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "]: " + args[i]);
}
}
}
javac Args.java
java Args 안녕 자바 123
# 출력:
# 인자 개수: 3
# args[0]: 안녕
# args[1]: 자바
# args[2]: 123
IntelliJ·Eclipse·VS Code 설정
IntelliJ IDEA (권장)
1. 다운로드:
2. 프로젝트 생성:
File → New → Project
- Name: MyFirstJavaProject
- Language: Java
- Build System: IntelliJ
- JDK: 21
3. 실행:
- 단축키:
Shift + F10(Run) - 디버그:
Shift + F9
IDE를 쓰면 컴파일·클래스패스·실행 명령을 전부 대신 처리해 주므로 편하지만, 그 대신 IDE가 쓰는 JDK와 터미널의 JDK가 서로 다를 수 있다는 점을 기억해야 합니다. IntelliJ에서 새 문법이 빨간 줄로 표시된다면 File → Project Structure의 SDK와 Language level이 원하는 버전으로 잡혀 있는지 먼저 확인합니다.
Eclipse
1. 다운로드:
- Eclipse IDE for Java Developers 2. 프로젝트 생성:
File → New → Java Project
- Project name: MyFirstJavaProject
- JRE: Use default JRE
VS Code
1. 확장 설치:
- Extension Pack for Java 2. 프로젝트 생성:
# 명령 팔레트 (Ctrl+Shift+P)
Java: Create Java Project
변수·연산자·조건문·반복문 기본 문법
변수와 타입
public class Variables {
public static void main(String[] args) {
// 기본 타입 (Primitive Types)
byte b = 127; // 8비트 (-128 ~ 127)
short s = 32767; // 16비트
int age = 25; // 32비트 (기본)
long population = 7800000000L; // 64비트 (L 접미사)
float pi = 3.14f; // 32비트 (f 접미사)
double price = 19.99; // 64비트 (기본)
boolean isActive = true; // true/false
char grade = 'A'; // 16비트 유니코드
// 참조 타입 (Reference Types)
String name = "홍길동";
String message = "안녕하세요!";
// 출력
System.out.println("이름: " + name);
System.out.println("나이: " + age);
System.out.println("가격: " + price);
}
}
상수
public class Constants {
public static void main(String[] args) {
// final: 변경 불가
final int MAX_SIZE = 100;
final double PI = 3.14159;
final String APP_NAME = "MyApp";
// MAX_SIZE = 200; // 컴파일 에러: cannot assign a value to final variable MAX_SIZE
System.out.println("최대 크기: " + MAX_SIZE);
}
}
연산자
public class Operators {
public static void main(String[] args) {
// 산술 연산자
int a = 10, b = 3;
System.out.println("더하기: " + (a + b)); // 13
System.out.println("빼기: " + (a - b)); // 7
System.out.println("곱하기: " + (a * b)); // 30
System.out.println("나누기: " + (a / b)); // 3 (정수 나눗셈)
System.out.println("나머지: " + (a % b)); // 1
// 비교 연산자
System.out.println(a > b); // true
System.out.println(a == b); // false
System.out.println(a != b); // true
// 논리 연산자
boolean x = true, y = false;
System.out.println(x && y); // false (AND)
System.out.println(x || y); // true (OR)
System.out.println(!x); // false (NOT)
// 증감 연산자
int count = 0;
count++; // count = count + 1
System.out.println(count); // 1
count--; // count = count - 1
System.out.println(count); // 0
}
}
조건문
public class Conditionals {
public static void main(String[] args) {
int score = 85;
// if-else
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
} else {
System.out.println("F");
}
// switch
String day = "월요일";
switch (day) {
case "월요일":
System.out.println("월요일입니다.");
break;
case "금요일":
System.out.println("금요일입니다!");
break;
default:
System.out.println("평일입니다.");
}
// 삼항 연산자
String result = (score >= 60) ? "합격" : "불합격";
System.out.println(result);
}
}
반복문
public class Loops {
public static void main(String[] args) {
// for 문
for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
}
// while 문
int count = 0;
while (count < 3) {
System.out.println("count = " + count);
count++;
}
// do-while 문
int num = 0;
do {
System.out.println("num = " + num);
num++;
} while (num < 3);
// 배열 순회
int[] numbers = {1, 2, 3, 4, 5};
for (int n : numbers) {
System.out.println(n);
}
}
}
메서드 정의와 가변 인자
메서드 정의
public class Calculator {
// 반환값 있는 메서드
public static int add(int a, int b) {
return a + b;
}
// 반환값 없는 메서드
public static void printSum(int a, int b) {
System.out.println("합계: " + (a + b));
}
// 오버로딩 (같은 이름, 다른 매개변수)
public static int add(int a, int b, int c) {
return a + b + c;
}
public static double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
int result1 = add(10, 20);
System.out.println("결과1: " + result1); // 30
int result2 = add(10, 20, 30);
System.out.println("결과2: " + result2); // 60
double result3 = add(10.5, 20.3);
System.out.println("결과3: " + result3); // 30.8
printSum(5, 7); // 합계: 12
}
}
가변 인자
public class VarArgs {
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(1, 2, 3, 4, 5)); // 15
System.out.println(sum(10)); // 10
}
}
클래스 정의와 객체 생성
클래스 정의
public class Person {
// 필드 (멤버 변수)
private String name;
private int age;
// 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 기본 생성자
public Person() {
this("이름없음", 0);
}
// 메서드
public void introduce() {
System.out.println("안녕하세요, " + name + "입니다.");
System.out.println("나이는 " + age + "세입니다.");
}
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0) {
this.age = age;
}
}
}
객체 생성 및 사용
public class Main {
public static void main(String[] args) {
// 객체 생성
Person person1 = new Person("홍길동", 25);
Person person2 = new Person("김철수", 30);
Person person3 = new Person();
// 메서드 호출
person1.introduce();
// 출력:
// 안녕하세요, 홍길동입니다.
// 나이는 25세입니다.
person2.introduce();
// Getter/Setter 사용
System.out.println("이름: " + person1.getName());
person1.setAge(26);
System.out.println("새 나이: " + person1.getAge());
}
}
배열 선언과 초기화
배열 선언 및 초기화
public class Arrays {
public static void main(String[] args) {
// 배열 선언 방법 1
int[] numbers1 = new int[5];
numbers1[0] = 10;
numbers1[1] = 20;
// 배열 선언 방법 2
int[] numbers2 = {1, 2, 3, 4, 5};
// 배열 선언 방법 3
int[] numbers3 = new int[]{10, 20, 30};
// 배열 길이
System.out.println("길이: " + numbers2.length); // 5
// 배열 순회
for (int i = 0; i < numbers2.length; i++) {
System.out.println("numbers2[" + i + "] = " + numbers2[i]);
}
// 향상된 for 문
for (int num : numbers2) {
System.out.println(num);
}
// 2차원 배열
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
System.out.println(matrix[1][2]); // 6
}
}
배열은 한 번 만들면 크기를 바꿀 수 없고, 범위를 벗어난 인덱스에 접근하면 java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5처럼 실행 중에 예외가 납니다. C/C++처럼 옆 메모리를 조용히 덮어쓰지 않고 JVM이 매 접근마다 경계를 검사해 주는 덕분입니다. 크기가 변하는 목록이 필요하면 컬렉션 편에서 다룰 ArrayList를 씁니다. 또 이 예제의 클래스 이름 Arrays는 표준 라이브러리의 java.util.Arrays와 이름이 겹치므로, 실제 프로젝트에서는 이렇게 짓지 않는 편이 좋습니다.
계산기·학생 관리·은행 계좌 예제
계산기
import java.util.Scanner;
public class Calculator {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("첫 번째 숫자: ");
double num1 = scanner.nextDouble();
System.out.print("연산자 (+, -, *, /): ");
String operator = scanner.next();
System.out.print("두 번째 숫자: ");
double num2 = scanner.nextDouble();
double result = 0;
switch (operator) {
case "+":
result = num1 + num2;
break;
case "-":
result = num1 - num2;
break;
case "*":
result = num1 * num2;
break;
case "/":
if (num2 != 0) {
result = num1 / num2;
} else {
System.out.println("0으로 나눌 수 없습니다.");
return;
}
break;
default:
System.out.println("잘못된 연산자입니다.");
return;
}
System.out.println("결과: " + result);
scanner.close();
}
}
학생 관리
public class Student {
private String name;
private int studentId;
private double gpa;
public Student(String name, int studentId, double gpa) {
this.name = name;
this.studentId = studentId;
this.gpa = gpa;
}
public void printInfo() {
System.out.println("=== 학생 정보 ===");
System.out.println("이름: " + name);
System.out.println("학번: " + studentId);
System.out.println("학점: " + gpa);
}
public String getGrade() {
if (gpa >= 4.0) return "A";
else if (gpa >= 3.0) return "B";
else if (gpa >= 2.0) return "C";
else return "F";
}
public static void main(String[] args) {
Student student1 = new Student("홍길동", 20240001, 3.8);
Student student2 = new Student("김철수", 20240002, 4.2);
student1.printInfo();
System.out.println("등급: " + student1.getGrade());
System.out.println();
student2.printInfo();
System.out.println("등급: " + student2.getGrade());
}
}
은행 계좌
public class BankAccount {
private String accountNumber;
private String owner;
private double balance;
public BankAccount(String accountNumber, String owner) {
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = 0.0;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println(amount + "원 입금 완료");
System.out.println("잔액: " + balance + "원");
} else {
System.out.println("입금액은 0보다 커야 합니다.");
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println(amount + "원 출금 완료");
System.out.println("잔액: " + balance + "원");
} else {
System.out.println("출금 불가 (잔액 부족 또는 잘못된 금액)");
}
}
public void printInfo() {
System.out.println("=== 계좌 정보 ===");
System.out.println("계좌번호: " + accountNumber);
System.out.println("예금주: " + owner);
System.out.println("잔액: " + balance + "원");
}
public static void main(String[] args) {
BankAccount account = new BankAccount("123-456-789", "홍길동");
account.printInfo();
account.deposit(10000);
account.deposit(5000);
account.withdraw(3000);
account.withdraw(20000); // 잔액 부족
account.printInfo();
}
}
파일명 불일치·main 시그니처·버전 불일치 에러
클래스명과 파일명 불일치
문제:
// HelloWorld.java 파일
public class Hello { // 클래스명이 다름
public static void main(String[] args) {
System.out.println("Hello");
}
}
에러:
error: class Hello is public, should be declared in a file named Hello.java
해결: 파일명을 Hello.java로 변경하거나 클래스명을 HelloWorld로 변경
main 메서드 시그니처 오류
문제:
public class Test {
public void main(String[] args) { // static 누락
System.out.println("Test");
}
}
에러:
Error: Main method not found in class Test, please define the main method as:
public static void main(String[] args)
해결: public static void main(String[] args) 정확히 작성
이 오류는 Java 21 이하에서 나는 메시지입니다. Java 25부터는 인스턴스 main 메서드가 허용되어, 인자 없는 생성자가 있는 클래스라면 static이 없어도 런처가 객체를 만든 뒤 main을 호출합니다. 강의 자료나 책과 결과가 다르게 나온다면 java -version으로 실제 버전부터 확인해 보세요.
컴파일한 버전과 실행하는 버전이 다를 때
에러:
Exception in thread "main" java.lang.UnsupportedClassVersionError: HelloWorld has been compiled by a more recent version of the Java Runtime (class file version 65.0), this version of the Java Runtime only recognizes class file versions up to 61.0
javac는 JDK 21(클래스 파일 버전 65)인데 java는 JDK 17(버전 61)이 실행되는 경우입니다. 여러 JDK가 설치된 PC에서 IDE로 컴파일하고 터미널에서 실행하거나, 개발 PC에서 만든 결과물을 더 오래된 JDK가 깔린 서버에 올릴 때 흔히 만납니다. 실행 쪽 JDK를 올리거나, 낮은 버전 대상으로 컴파일하려면 javac --release 17 HelloWorld.java처럼 --release 옵션을 씁니다.
세미콜론 누락
문제:
System.out.println("Hello") // 세미콜론 누락
에러:
error: ';' expected
해결: 모든 문장 끝에 ; 추가
대소문자 구분
문제:
String name = "홍길동";
system.out.println(name); // System이 아닌 system
에러:
error: cannot find symbol
symbol: variable system
해결: Java는 대소문자를 구분합니다. System 정확히 작성
처음 Java를 배울 때 가장 시간을 많이 잡아먹는 것은 문법보다 이런 환경 문제입니다. 제 경험상 “코드는 맞는데 안 된다”는 질문의 대부분은 PATH에 잡힌 JDK가 예상과 다르거나, 클래스 이름과 실행 위치가 어긋난 경우였습니다. 오류 메시지를 복사해 검색하기 전에 java -version, javac -version, 현재 폴더를 먼저 확인하는 습관이 가장 빠른 해결책입니다.
Java 컴파일과 실행 과정
컴파일 과정
1. 소스 코드 작성
HelloWorld.java
2. 컴파일 (javac)
javac HelloWorld.java
→ HelloWorld.class (바이트코드)
3. 실행 (java)
java HelloWorld
→ JVM이 바이트코드 해석 및 실행
4. 출력
Hello, Java!
JVM 동작 원리
HelloWorld.class (바이트코드)
↓
Class Loader (클래스 로딩)
↓
Bytecode Verifier (검증)
↓
JIT Compiler (Just-In-Time 컴파일)
↓
Native Code (기계어)
↓
실행
위 그림은 단순화한 것입니다. HotSpot JVM은 처음에는 바이트코드를 인터프리터로 실행하다가, 자주 호출되는 메서드(“핫스팟”)만 골라 JIT 컴파일러로 기계어를 만듭니다. 그래서 Java 프로그램은 시작 직후가 가장 느리고 실행할수록 빨라지는 특성(워밍업)이 있으며, 아주 짧은 코드로 속도를 재면 실제보다 느리게 나오는 이유가 이것입니다.
플랫폼 독립성:
Windows: HelloWorld.class → Windows JVM → 실행
macOS: HelloWorld.class → macOS JVM → 실행
Linux: HelloWorld.class → Linux JVM → 실행
동일한 .class 파일이 모든 플랫폼에서 실행!
코딩 컨벤션·주석·패키지·디버깅
코딩 컨벤션
// ✅ 좋은 코드
public class MyClass { // 클래스: PascalCase
private static final int MAX_SIZE = 100; // 상수: UPPER_SNAKE_CASE
private String userName; // 변수: camelCase
public String getUserName() { // 메서드: camelCase
return userName;
}
}
// ❌ 나쁜 코드
public class myclass { // 소문자
private int MaxSize = 100; // PascalCase
private String user_name; // snake_case
}
주석
// 한 줄 주석
/*
* 여러 줄 주석
* 설명...
*/
/**
* JavaDoc 주석
* @param name 사용자 이름
* @return 인사 메시지
*/
public String greet(String name) {
return "안녕하세요, " + name + "님!";
}
패키지
// com/example/myapp/Main.java
// 패키지 선언
package com.example.myapp;
import java.util.Scanner;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 코드...
}
}
디버깅
public class Debug {
public static void main(String[] args) {
int a = 10;
int b = 0;
// 중단점 설정 (IDE에서)
int result = a / b; // ArithmeticException
System.out.println(result);
}
}
IntelliJ 디버깅:
- 중단점 설정: 줄 번호 왼쪽 클릭
- 디버그 실행:
Shift + F9 - 단계 실행:
F8(Step Over),F7(Step Into)
Java 버전별 주요 변화
주요 버전 변화
| 버전 | 출시 | 주요 기능 |
|---|---|---|
| Java 8 | 2014 | Lambda, Stream API, Optional |
| Java 11 | 2018 | HTTP Client, 단일 파일 소스 실행, String 메서드 (LTS) |
| Java 17 | 2021 | Sealed Classes, Record, instanceof 패턴 매칭 (LTS) |
| Java 21 | 2023 | Virtual Threads, switch 패턴 매칭, Record 패턴 (LTS) |
| Java 25 | 2025 | 간결한 소스 파일·인스턴스 main, 모듈 import 선언 (LTS) |
LTS (Long-Term Support): 장기 지원 버전 (8, 11, 17, 21, 25). var는 Java 10에서 들어왔습니다.
Java는 6개월마다 새 버전이 나오고 그중 2년마다 하나가 LTS로 지정됩니다. 비-LTS 버전은 다음 버전이 나오면 업데이트가 끊기므로, 학습이든 실무든 가장 최근 LTS 중 하나를 고르는 것이 무난합니다. 회사 프로젝트에 합류한다면 새 기능보다 그 프로젝트가 쓰는 버전에 맞추는 것이 먼저입니다. Spring Boot 3는 Java 17 이상을 요구하므로, 새로 시작한다면 17 미만을 고를 이유는 거의 없습니다.
최신 기능 맛보기
// Java 10: var (타입 추론)
var name = "홍길동"; // String
var age = 25; // int
var list = new ArrayList<String>();
// Java 14: Switch Expression
String result = switch (day) {
case "월", "화", "수", "목", "금" -> "평일";
case "토", "일" -> "주말";
default -> "알 수 없음";
};
// Java 16: Record (불변 데이터 클래스)
record Person(String name, int age) {}
Person person = new Person("홍길동", 25);
System.out.println(person.name()); // 홍길동
첫 편 요약
핵심 요약
- Java: 플랫폼 독립적, 객체지향 언어
- JDK: Java 개발 도구 키트 (컴파일러 포함)
- JVM: Java 가상 머신 (바이트코드 실행)
- 컴파일:
javac HelloWorld.java→.class - 실행:
java HelloWorld - 클래스: 모든 코드는 클래스 안에
- main 메서드: 프로그램 진입점
- 객체지향: 클래스와 객체로 프로그래밍
Java 기본 구조
// 패키지 선언 (선택)
package com.example;
// import 문 (선택)
import java.util.Scanner;
// 클래스 선언 (필수)
public class MyClass {
// 필드 (멤버 변수)
private int value;
// 생성자
public MyClass(int value) {
this.value = value;
}
// 메서드
public void printValue() {
System.out.println(value);
}
// main 메서드 (진입점)
public static void main(String[] args) {
MyClass obj = new MyClass(10);
obj.printValue();
}
}
실전 팁
- IDE 사용: IntelliJ IDEA 또는 Eclipse 권장
- 코딩 컨벤션: 클래스는 PascalCase, 변수/메서드는 camelCase
- 패키지 구조:
com.회사명.프로젝트명.기능 - 주석: JavaDoc 형식으로 문서화
- 예외 처리: try-catch로 에러 처리
- 디버깅: 중단점과 단계 실행 활용
- 버전 선택: LTS 버전 사용 (21 또는 25)
- 빌드 도구: Maven 또는 Gradle 사용
다음 단계
관련 글
- C++ 초보자가 자주 하는 실수 Top 15 | 컴파일 에러부터 런타임 크래시까지
- HTML/CSS 시작하기 | 웹 개발 첫걸음
- Java 변수와 타입 | 기본 타입, 참조 타입, 형변환
- Java 클래스와 객체 | OOP, 상속, 인터페이스
- Java 컬렉션 | ArrayList, HashMap, Set
- Kotlin Spring Boot 입문: 첫 REST API 만들기
자주 묻는 질문 (FAQ)
Q. class Hello is public, should be declared in a file named Hello.java 오류는 왜 나나요?
A. Java는 public 클래스의 이름과 소스 파일 이름이 정확히 같아야 컴파일되는데, 파일명이 다를 때 이 오류가 납니다. 대소문자까지 일치해야 하므로 hello.java처럼 파일명만 소문자여도 같은 오류가 납니다. 파일명을 Hello.java로 바꾸거나 클래스 이름을 파일명에 맞추면 해결됩니다.