왜 Java를 쓰는가
Java가 지금도 널리 쓰이는 이유와, 문법보다 플랫폼과 생태계 관점에서 어떤 강점을 가지는지 정리합니다.
Category
Preparing references and filters for this topic. 이 주제의 레퍼런스와 필터를 준비하고 있습니다.
Java가 지금도 널리 쓰이는 이유와, 문법보다 플랫폼과 생태계 관점에서 어떤 강점을 가지는지 정리합니다.
Java 프로그램의 기본 단위인 package, import, 클래스 파일 구조, `main` 메서드 의미를 정리합니다.
package com.example.app;
import java.util.List;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, Java");
}
}Java의 primitive type, 참조 타입, 변수 선언과 초기화 규칙, 자동 형변환 감각을 정리합니다.
// 정수
byte b = 127; // 1바이트, -128 ~ 127
short s = 32_000; // 2바이트
int i = 2_147_483_647; // 4바이트 — 기본 정수 타입
long l = 10_000_000_000L; // 8바이트, 리터럴에 L 필요
// 실수
float f = 3.14f; // 4바이트, 리터럴에 f 필요
double d = 3.14; // 8바이트 — 기본 실수 타입
// 기타
char c = 'A'; // UTF-16 code unit 하나
boolean active = true; // true / false
// 참조 타입
String name = "RefDock"; // 객체, null 가능
// var — 타입 추론 (Java 10+)
var count = 42; // int로 추론
var items = new ArrayList<String>(); // ArrayList<String>으로 추론
// 상수 — final
final int MAX_RETRY = 3;`if`와 `switch`를 언제 선택하는지, 현대 Java의 switch expression이 어떻게 fall-through 버그와 보일러플레이트를 없애는지 정리합니다.
// 단순 조건
if (score >= 60) {
System.out.println("합격");
}
// if-else if-else — 범위 조건
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else {
grade = "C";
}
// 삼항 연산자 — 한 줄 조건 대입
String result = score >= 60 ? "합격" : "불합격";
// switch expression — 값 종류가 명확히 갈릴 때
String label = switch (status) {
case ACTIVE -> "활성";
case INACTIVE -> "비활성";
case PENDING -> "대기";
};`for`, `for-each`, `while`을 어떤 기준으로 고르는지, `break`와 `continue`가 실제로 어떻게 흐름을 바꾸는지 정리합니다.
// 인덱스가 필요한 반복
for (int i = 0; i < names.length; i++) {
System.out.println(i + ": " + names[i]);
}
// 컬렉션 순회 — 인덱스 불필요
for (String name : names) {
System.out.println(name);
}
// 종료 시점이 동적
while (!queue.isEmpty()) {
process(queue.poll());
}Java 배열이 고정 길이인 이유, `length` 필드와 `Arrays` 유틸리티 사용법, 배열 대신 컬렉션을 써야 하는 시점을 정리합니다.
// 배열 선언과 초기화
int[] numbers = {10, 20, 30, 40, 50};
String[] names = new String[3]; // 기본값 null로 초기화
// 길이 확인 — length 필드 (메서드 아님)
System.out.println(numbers.length); // 5
// 인덱스로 접근
System.out.println(numbers[0]); // 10
numbers[1] = 99; // 수정 가능`String`이 불변 객체인 이유와 그 결과로 `==`가 아닌 `equals()`를 써야 하는 이유, 반복 연결에서 `StringBuilder`가 필요한 시점을 정리합니다.
String name = "Kim";
// 값 비교는 반드시 equals()
String a = new String("hello");
String b = new String("hello");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false — 참조가 다름
// 자주 쓰는 메서드
System.out.println(name.length()); // 3
System.out.println(name.toUpperCase()); // "KIM"
System.out.println(" hello ".strip()); // "hello"
System.out.println(name.contains("im")); // true`String.format`, `formatted()`, `printf`의 차이, 텍스트 블록으로 여러 줄 문자열을 다루는 방법, 포맷 지정자 선택 기준을 정리합니다.
String name = "Kim";
int score = 95;
// String.format — 새 문자열 반환
String msg = String.format("이름: %s, 점수: %d", name, score);
// formatted() — Java 15+, 동일한 결과
String msg2 = "이름: %s, 점수: %d".formatted(name, score);
// 표준 출력에 바로 포매팅
System.out.printf("이름: %s, 점수: %d%n", name, score);기본 타입이 컬렉션에 들어갈 수 없는 이유, 오토박싱이 자동으로 일어나는 시점, 예상치 못한 `NullPointerException`과 `==` 비교 함정을 정리합니다.
// 기본 타입 ↔ 래퍼 클래스
int primitive = 42;
Integer boxed = primitive; // 오토박싱 — 컴파일러가 Integer.valueOf(42)로 변환
int unboxed = boxed; // 언박싱 — 컴파일러가 boxed.intValue()로 변환
// 컬렉션에는 래퍼 클래스 필요
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // 오토박싱
int n = numbers.get(0); // 언박싱`java.time`의 `LocalDate`, `LocalDateTime`, `ZonedDateTime`을 어떤 기준으로 고르는지, 불변 설계가 날짜 연산을 어떻게 안전하게 만드는지 정리합니다.
// 오늘 날짜
LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(1990, 3, 15);
// 날짜 연산 — 새 객체 반환 (불변)
LocalDate nextWeek = today.plusDays(7);
LocalDate lastMonth = today.minusMonths(1);
// 포매팅
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuu-MM-dd");
String formatted = today.format(fmt); // "2026-03-27"Java 객체지향의 출발점인 class, field, method, constructor, instance 개념을 한 흐름으로 정리합니다.
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
User user = new User("Kim");
System.out.println(user.getName());Java의 네 가지 접근 제한자가 어떤 범위를 열고 닫는지, `package-private`이 왜 중요한지, 캡슐화 설계 기준을 정리합니다.
public class BankAccount {
private double balance; // 외부 직접 접근 차단
private String ownerId;
public void deposit(double amount) { // 공개 API
if (amount > 0) balance += amount;
}
public double getBalance() {
return balance;
}
private boolean isValid(double amount) { // 내부 구현
return amount > 0 && amount <= balance;
}
}상속이 코드 재사용을 넘어서 타입 관계를 어떻게 만들고, 다형성이 왜 중요한지 정리합니다.
class Animal {
void speak() {
System.out.println("...");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("woof");
}
}공통 계약을 표현하는 interface와 부분 구현을 제공하는 abstract class를 어떻게 구분할지 정리합니다.
interface Payable {
int pay();
}
abstract class Employee implements Payable {
protected String name;
}객체 비교에서 참조 비교와 값 비교가 어떻게 다르고, equals/hashCode/Comparable 계약을 어떻게 맞춰야 하는지 정리합니다.
public final class User implements Comparable<User> {
private final long id;
private final String email;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User other)) return false;
return id == other.id && Objects.equals(email, other.email);
}
@Override
public int hashCode() {
return Objects.hash(id, email);
}
@Override
public int compareTo(User other) {
int byId = Long.compare(this.id, other.id);
return byId != 0 ? byId : this.email.compareTo(other.email);
}
}`enum`이 문자열 상수보다 안전한 이유, 메서드와 필드를 추가하는 방법, switch expression과 함께 모든 케이스를 컴파일 타임에 강제하는 패턴을 정리합니다.
enum Status {
ACTIVE, INACTIVE, PENDING
}
Status s = Status.ACTIVE;
// switch expression과 함께 — 모든 케이스 처리 강제
String label = switch (s) {
case ACTIVE -> "활성";
case INACTIVE -> "비활성";
case PENDING -> "대기";
};Java annotation이 단순 주석이 아니라 메타데이터라는 점과 `@Retention`, `@Target`으로 어디까지 남고 어디에 붙는지 정하는 방법을 정리합니다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresRole {
String value();
}
class UserService {
@RequiresRole("admin")
void deleteUser(long userId) {
// ...
}
}
Method method = UserService.class.getDeclaredMethod("deleteUser", long.class);
RequiresRole role = method.getAnnotation(RequiresRole.class);
if (role != null && role.value().equals("admin")) {
System.out.println("관리자 권한 필요");
}Java 제네릭이 왜 필요한지, `List<String>` 같은 타입 매개변수가 컴파일 타임 안전성을 어떻게 높이는지 정리합니다.
List<String> names = new ArrayList<>();
names.add("Kim");
String first = names.get(0);`<T extends Comparable<T>>`로 타입에 제약을 거는 방법, `? extends`와 `? super`가 어떤 문제를 해결하는지, 타입 소거가 런타임에 미치는 영향을 정리합니다.
// bounded type — T에 제약 추가
public static <T extends Comparable<? super T>> T max(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
// wildcard — 읽기 전용 컬렉션 수용
public static double sum(List<? extends Number> numbers) {
return numbers.stream().mapToDouble(Number::doubleValue).sum();
}
max(List.of(3, 1, 4)); // T = Integer
sum(List.of(1, 2.5, 3L)); // Integer, Double, Long 모두 수용Java Collections Framework의 핵심인 `List`, `Set`, `Map`을 어떤 상황에 고르는지 정리합니다.
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String, Integer> map = new HashMap<>();Java에서 수정 가능한 컬렉션과 수정 불가능한 컬렉션이 어떻게 다르고, `List.of`, `Set.of`, `Map.of`를 어떤 관점으로 써야 하는지 정리합니다.
List<String> roles = List.of("ADMIN", "EDITOR", "VIEWER");
Set<Integer> levels = Set.of(1, 2, 3);List와 Map을 어떻게 순회하고, 정렬 기준을 Comparable과 Comparator 중 어디에 둘지 정리합니다.
users.sort(Comparator
.comparing(User::getAge)
.thenComparing(User::getName));
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}`Map`에서 존재 여부를 먼저 검사한 뒤 `put`하는 코드를 `getOrDefault`, `computeIfAbsent`, `merge`로 어떻게 줄이는지 정리합니다.
Map<String, Integer> counts = new HashMap<>();
counts.put("java", counts.getOrDefault("java", 0) + 1);
counts.merge("java", 1, Integer::sum);
Map<Long, List<Order>> ordersByUser = new HashMap<>();
ordersByUser
.computeIfAbsent(userId, id -> new ArrayList<>())
.add(order);Java 예외 모델의 핵심인 checked exception과 unchecked exception 차이, `try-catch-throws` 흐름을 정리합니다.
try {
Files.readString(path);
} catch (IOException e) {
System.out.println("읽기 실패: " + e.getMessage());
}`try-with-resources`가 왜 중요한지와 `Files`, `Path`를 이용한 기본 파일 처리 흐름을 정리합니다.
Path path = Path.of("notes.txt");
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
}현대 Java 파일 I/O의 핵심인 `Path`와 `Files`를 사용해 경로 조합, 파일 읽기/쓰기, 디렉터리 작업을 어떻게 읽어야 하는지 정리합니다.
Path path = Path.of("data", "users.txt");
if (Files.exists(path)) {
List<String> lines = Files.readAllLines(path);
}Java의 lambda expression과 method reference가 어떤 문제를 줄이고, 함수형 스타일과 어떻게 연결되는지 정리합니다.
names.sort((a, b) -> a.compareToIgnoreCase(b));
names.forEach(System.out::println);Java Stream이 컬렉션과 어떻게 다르고, pipeline, intermediate, terminal operation을 어떻게 읽어야 하는지 정리합니다.
List<String> result = names.stream()
.filter(name -> name.length() >= 3)
.map(String::toUpperCase)
.toList();Stream의 마지막 단계에서 결과를 어떤 컬렉션 모양으로 모을지 결정하는 `Collectors`와 `groupingBy`, `toMap` 패턴을 정리합니다.
Map<String, List<Student>> byDepartment =
students.stream()
.collect(Collectors.groupingBy(Student::getDepartment));`Optional`이 null 자체를 없애는 도구가 아니라, 값이 없을 수 있음을 더 명시적으로 표현하는 방법이라는 점을 정리합니다.
Optional<User> user = findUser(id);
String name = user.map(User::getName)
.orElse("anonymous");`record`가 어떻게 값 객체 보일러플레이트를 없애는지, `instanceof`의 패턴 매칭이 cast 코드를 어떻게 줄이는지 정리합니다.
// record — 한 줄로 불변 값 객체
record Point(int x, int y) {}
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 — 접근자 자동 생성
System.out.println(p); // Point[x=3, y=4] — toString 자동
// instanceof 패턴 매칭 — 타입 검사 + 변수 선언 한 번에
Object obj = "hello";
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // cast 없이 바로 사용
}`sealed`가 상속 가능한 타입 범위를 어떻게 제한하고, `permits`, `final`, `sealed`, `non-sealed` 조합으로 도메인 모델을 어떻게 닫는지 정리합니다.
sealed interface PaymentResult
permits Approved, Declined, PendingReview {
}
record Approved(String transactionId) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
final class PendingReview implements PaymentResult {
private final String ticketId;
PendingReview(String ticketId) {
this.ticketId = ticketId;
}
String ticketId() {
return ticketId;
}
}
String message = switch (result) {
case Approved approved -> "승인: " + approved.transactionId();
case Declined declined -> "거절: " + declined.reason();
case PendingReview review -> "검토 필요: " + review.ticketId();
};Java 스레드가 무엇인지, Thread와 Runnable의 역할이 어떻게 다르고 lifecycle을 어떻게 읽어야 하는지 정리합니다.
Runnable task = () -> {
System.out.println("work on " + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "worker-1");
thread.start();
thread.join();직접 Thread를 만드는 방식과 thread pool 기반 실행 방식이 어떻게 다르고, Callable/Future를 왜 함께 쓰는지 정리합니다.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> 40 + 2);
int value = future.get();
pool.shutdown();비동기 작업의 결과를 기다리고 이어 붙이는 `CompletableFuture`를 `Future`보다 어떤 관점에서 이해하면 좋은지 정리합니다.
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(String::toUpperCase);
String result = future.join();여러 thread가 같은 상태를 건드릴 때 왜 문제가 생기고, synchronized와 lock을 어떤 기준으로 고를지 정리합니다.
public class Counter {
private int value;
public synchronized void increment() {
value++;
}
public synchronized int getValue() {
return value;
}
}Java 21의 virtual thread를 언제 쓰고, `Thread.ofVirtual()`과 `Executors.newVirtualThreadPerTaskExecutor()`를 어떤 기준으로 선택하는지 정리합니다.
// 짧은 단발 작업
Thread thread = Thread.ofVirtual()
.name("fetch-user")
.start(() -> fetchUser(userId));
thread.join();
// 요청 처리 안에서 여러 I/O 작업을 동시에 fan-out
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<User> user = executor.submit(() -> fetchUser(userId));
Future<List<Order>> orders = executor.submit(() -> fetchOrders(userId));
return new Profile(user.get(), orders.get());
}Java 테스트에서 JUnit이 어떤 역할을 하고, 테스트 메서드, assertion, fixture를 어떻게 조직하는지 정리합니다.
class CalculatorTest {
@Test
void add_returnsSum() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
}입력 케이스를 여러 번 돌리는 parameterized test와 테스트 준비/정리 단계의 lifecycle을 JUnit 5 기준으로 정리합니다.
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 10})
void isNonNegative_returnsTrue(int value) {
assertTrue(value >= 0);
}Java 프로젝트에서 Maven과 Gradle이 무엇을 해 주는지, dependency 관리와 build 흐름을 어떤 관점으로 이해하면 좋은지 정리합니다.
src/
main/
java/
test/
java/
pom.xml # Maven
build.gradle # Gradle