Skip to content

필사 모드: 모던 Java 완전 정복: Java 17/21 핵심 문법 가이드

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

모던 Java 완전 정복: Java 17/21 핵심 문법 가이드

Java는 꾸준히 진화하고 있습니다. Java 21 LTS는 Project Loom(Virtual Threads), Pattern Matching, Record, Sealed Classes 등 혁신적인 기능을 포함합니다. 이 가이드에서는 현업에서 반드시 알아야 할 모던 Java 문법을 완전 정복합니다.

1. Java 21 핵심 신기능 (LTS 기준)

1.1 Record 클래스 (Java 16+)

Record는 불변(immutable) 데이터 클래스를 간결하게 선언하는 방법입니다. 컴파일러가 자동으로 생성자, getter, equals, hashCode, toString을 생성합니다.

// 기존 방식 (boilerplate 가득)

public class PersonOld {

private final String name;

private final int age;

public PersonOld(String name, int age) {

this.name = name;

this.age = age;

}

public String getName() { return name; }

public int getAge() { return age; }

@Override

public boolean equals(Object o) { /* ... */ }

@Override

public int hashCode() { /* ... */ }

@Override

public String toString() { /* ... */ }

}

// Record 방식 (한 줄!)

public record Person(String name, int age) {}

// 사용

Person p = new Person("Alice", 30);

System.out.println(p.name()); // "Alice" (getter 이름은 필드명 그대로)

System.out.println(p); // Person[name=Alice, age=30]

// 커스텀 생성자로 유효성 검사

public record Person(String name, int age) {

public Person {

if (name == null || name.isBlank()) throw new IllegalArgumentException("name cannot be blank");

if (age < 0 || age > 150) throw new IllegalArgumentException("invalid age: " + age);

}

// 추가 메서드 정의 가능

public String greeting() {

return "Hello, I'm " + name + " and I'm " + age + " years old.";

}

}

// Record는 인터페이스 구현 가능

public record Point(double x, double y) implements Comparable<Point> {

@Override

public int compareTo(Point other) {

return Double.compare(this.distance(), other.distance());

}

private double distance() {

return Math.sqrt(x * x + y * y);

}

}

Record의 제약사항:

- 상속 불가 (implicitly final)

- 필드는 모두 private final

- 인스턴스 필드 추가 불가 (static 필드는 가능)

1.2 Sealed Classes (Java 17+)

Sealed Class는 어떤 클래스/인터페이스가 해당 타입을 상속/구현할 수 있는지 명시적으로 제한합니다. 이를 통해 타입 계층을 폐쇄적으로 설계할 수 있습니다.

// Shape는 오직 Circle, Rectangle, Triangle만 상속 가능

public sealed class Shape permits Circle, Rectangle, Triangle {}

public final class Circle extends Shape {

private final double radius;

public Circle(double radius) { this.radius = radius; }

public double radius() { return radius; }

}

public final class Rectangle extends Shape {

private final double width, height;

public Rectangle(double width, double height) {

this.width = width;

this.height = height;

}

public double width() { return width; }

public double height() { return height; }

}

public non-sealed class Triangle extends Shape {

// non-sealed: Triangle을 상속한 클래스는 제한 없음

private final double base, height;

public Triangle(double base, double height) {

this.base = base;

this.height = height;

}

}

// 인터페이스에도 sealed 적용 가능

public sealed interface Result<T> permits Result.Success, Result.Failure {

record Success<T>(T value) implements Result<T> {}

record Failure<T>(String error) implements Result<T> {}

}

1.3 Pattern Matching for instanceof (Java 16+)

기존 instanceof 후 명시적 캐스팅 없이 바로 사용 가능합니다.

// 기존 방식

if (obj instanceof String) {

String s = (String) obj;

System.out.println(s.length());

}

// 모던 방식 (Pattern Matching)

if (obj instanceof String s) {

System.out.println(s.length()); // s는 String으로 자동 캐스팅

}

// 조건과 결합

if (obj instanceof String s && s.length() > 5) {

System.out.println("Long string: " + s);

}

// 부정

if (!(obj instanceof String s)) {

System.out.println("Not a string");

return;

}

// 이 시점부터 s 사용 가능

System.out.println(s.toUpperCase());

1.4 Pattern Matching for switch (Java 21)

switch 표현식에서 타입 패턴 매칭을 지원합니다. Sealed Class와 결합하면 강력합니다.

// 타입 기반 switch

public double calculateArea(Shape shape) {

return switch (shape) {

case Circle c -> Math.PI * c.radius() * c.radius();

case Rectangle r -> r.width() * r.height();

case Triangle t -> 0.5 * t.base() * t.height();

// sealed class이므로 default 불필요 (모든 케이스 커버)

};

}

// Guarded Pattern (조건부 패턴)

public String classify(Object obj) {

return switch (obj) {

case Integer i when i < 0 -> "Negative integer: " + i;

case Integer i when i == 0 -> "Zero";

case Integer i -> "Positive integer: " + i;

case String s when s.isEmpty() -> "Empty string";

case String s -> "String: " + s;

case null -> "Null value";

default -> "Other: " + obj.getClass().getSimpleName();

};

}

// Record Pattern

public String describePoint(Object obj) {

return switch (obj) {

case Point(double x, double y) when x == 0 && y == 0 -> "Origin";

case Point(double x, double y) when x == 0 -> "On Y-axis at " + y;

case Point(double x, double y) when y == 0 -> "On X-axis at " + x;

case Point(double x, double y) -> "Point at (" + x + ", " + y + ")";

default -> "Not a point";

};

}

1.5 Text Blocks (Java 15+)

여러 줄 문자열을 가독성 있게 작성합니다.

// 기존 방식

String json = "{\n" +

" \"name\": \"Alice\",\n" +

" \"age\": 30\n" +

"}";

// Text Block

String json = """

{

"name": "Alice",

"age": 30

}

""";

// SQL 예시

String sql = """

SELECT u.id, u.name, o.total

FROM users u

JOIN orders o ON u.id = o.user_id

WHERE u.active = true

ORDER BY o.total DESC

""";

// HTML

String html = """

""";

// 들여쓰기 제어: \s (trailing whitespace 유지), \ (줄바꿈 없애기)

String oneLine = """

Hello \

World\

"""; // "Hello World"

1.6 Virtual Threads (Java 21, Project Loom)

Virtual Thread는 JVM이 관리하는 경량 스레드입니다. OS 스레드와 1:1 매핑 대신 M:N 방식으로 수백만 개의 동시 작업이 가능합니다.

// 기존 플랫폼 스레드

Thread platformThread = new Thread(() -> {

System.out.println("Platform thread: " + Thread.currentThread());

});

platformThread.start();

// Virtual Thread 생성

Thread virtualThread = Thread.ofVirtual()

.name("my-virtual-thread")

.start(() -> {

System.out.println("Virtual thread: " + Thread.currentThread());

});

// ExecutorService로 Virtual Thread 사용

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

List<Future<String>> futures = new ArrayList<>();

for (int i = 0; i < 10_000; i++) {

int taskId = i;

futures.add(executor.submit(() -> {

// I/O 차단 작업도 OK - Virtual Thread는 블로킹 시 마운트 해제

Thread.sleep(100);

return "Task " + taskId + " completed";

}));

}

// 모든 작업 완료 대기

for (Future<String> f : futures) {

System.out.println(f.get());

}

}

// Spring Boot와 함께 사용 (application.properties)

// spring.threads.virtual.enabled=true

// 구조적 동시성 (Structured Concurrency, preview)

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

Future<User> user = scope.fork(() -> fetchUser(userId));

Future<List<Order>> orders = scope.fork(() -> fetchOrders(userId));

scope.join().throwIfFailed();

return new UserProfile(user.resultNow(), orders.resultNow());

}

1.7 Sequenced Collections (Java 21)

컬렉션에 순서 개념을 추가하는 새 인터페이스입니다.

// SequencedCollection: getFirst(), getLast(), addFirst(), addLast(), reversed()

List<String> list = new ArrayList<>(List.of("a", "b", "c"));

System.out.println(list.getFirst()); // "a"

System.out.println(list.getLast()); // "c"

list.addFirst("z"); // ["z", "a", "b", "c"]

list.addLast("x"); // ["z", "a", "b", "c", "x"]

List<String> reversed = list.reversed(); // ["x", "c", "b", "a", "z"]

// SequencedMap: firstEntry(), lastEntry(), putFirst(), putLast(), reversed()

LinkedHashMap<String, Integer> map = new LinkedHashMap<>();

map.put("one", 1);

map.put("two", 2);

map.put("three", 3);

System.out.println(map.firstEntry()); // one=1

System.out.println(map.lastEntry()); // three=3

2. 람다와 스트림 API 완전 정복

2.1 람다 표현식 문법

// 기본 형태

Runnable r = () -> System.out.println("Hello");

Comparator<String> comp = (a, b) -> a.compareTo(b);

// 블록 람다 (여러 줄)

Function<Integer, Integer> factorial = n -> {

int result = 1;

for (int i = 1; i <= n; i++) result *= i;

return result;

};

// 타입 추론

List<String> names = List.of("Charlie", "Alice", "Bob");

names.sort((a, b) -> a.compareTo(b));

// 또는 메서드 참조

names.sort(String::compareTo);

2.2 함수형 인터페이스

// Function<T, R>: T -> R

Function<String, Integer> strLen = String::length;

Function<Integer, Integer> doubler = x -> x * 2;

Function<String, Integer> lenThenDouble = strLen.andThen(doubler);

System.out.println(lenThenDouble.apply("Hello")); // 10

// BiFunction<T, U, R>: T, U -> R

BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

System.out.println(repeat.apply("Java", 3)); // "JavaJavaJava"

// Predicate<T>: T -> boolean

Predicate<String> isLong = s -> s.length() > 5;

Predicate<String> startsWithA = s -> s.startsWith("A");

Predicate<String> combined = isLong.and(startsWithA);

Predicate<String> either = isLong.or(startsWithA);

Predicate<String> notLong = isLong.negate();

// Supplier<T>: () -> T

Supplier<LocalDate> today = LocalDate::now;

Supplier<List<String>> listFactory = ArrayList::new;

// Consumer<T>: T -> void

Consumer<String> printer = System.out::println;

Consumer<String> logger = s -> log.info("Processing: {}", s);

Consumer<String> both = printer.andThen(logger);

// UnaryOperator<T>: T -> T (Function의 특수화)

UnaryOperator<String> toUpper = String::toUpperCase;

UnaryOperator<Integer> increment = x -> x + 1;

// BinaryOperator<T>: T, T -> T

BinaryOperator<Integer> add = Integer::sum;

BinaryOperator<String> concat = String::concat;

2.3 메서드 참조 (4가지 유형)

// 1. 정적 메서드 참조: ClassName::staticMethod

Function<String, Integer> parseInt = Integer::parseInt;

Comparator<String> comp = String::compareTo; // 아니, 이건 인스턴스 메서드

// 2. 특정 인스턴스의 메서드 참조: instance::instanceMethod

String prefix = "Hello, ";

Function<String, String> greeter = prefix::concat;

System.out.println(greeter.apply("World")); // "Hello, World"

// 3. 임의 인스턴스의 메서드 참조: ClassName::instanceMethod

Function<String, String> upper = String::toUpperCase;

Comparator<String> byLength = Comparator.comparingInt(String::length);

// 4. 생성자 참조: ClassName::new

Supplier<ArrayList<String>> listCreator = ArrayList::new;

Function<String, StringBuilder> sbCreator = StringBuilder::new;

BiFunction<String, Integer, String> repeater = String::new; // 아님, 예시용

2.4 Stream API 핵심 연산

List<Employee> employees = List.of(

new Employee("Alice", "Engineering", 90_000),

new Employee("Bob", "Engineering", 80_000),

new Employee("Charlie", "Marketing", 60_000),

new Employee("Diana", "Marketing", 70_000),

new Employee("Eve", "Engineering", 95_000)

);

// filter + map + collect

List<String> highEarnerNames = employees.stream()

.filter(e -> e.salary() > 75_000)

.map(Employee::name)

.sorted()

.collect(Collectors.toList());

// Java 16+: .toList() (불변 리스트)

// flatMap: Stream<Stream<T>> -> Stream<T>

List<List<Integer>> nested = List.of(List.of(1, 2, 3), List.of(4, 5), List.of(6));

List<Integer> flat = nested.stream()

.flatMap(Collection::stream)

.collect(Collectors.toList());

// [1, 2, 3, 4, 5, 6]

// reduce

int totalSalary = employees.stream()

.mapToInt(Employee::salary)

.sum(); // 또는 reduce(0, Integer::sum)

OptionalDouble avgSalary = employees.stream()

.mapToInt(Employee::salary)

.average();

// groupingBy

Map<String, List<Employee>> byDept = employees.stream()

.collect(Collectors.groupingBy(Employee::department));

Map<String, Double> avgSalaryByDept = employees.stream()

.collect(Collectors.groupingBy(

Employee::department,

Collectors.averagingInt(Employee::salary)

));

Map<String, Long> countByDept = employees.stream()

.collect(Collectors.groupingBy(Employee::department, Collectors.counting()));

// partitioningBy

Map<Boolean, List<Employee>> partition = employees.stream()

.collect(Collectors.partitioningBy(e -> e.salary() > 75_000));

// true -> 고연봉자, false -> 나머지

// joining

String names = employees.stream()

.map(Employee::name)

.collect(Collectors.joining(", ", "[", "]"));

// "[Alice, Bob, Charlie, Diana, Eve]"

// toMap

Map<String, Integer> nameSalaryMap = employees.stream()

.collect(Collectors.toMap(Employee::name, Employee::salary));

// findFirst, findAny, anyMatch, allMatch, noneMatch

Optional<Employee> first = employees.stream()

.filter(e -> e.department().equals("Engineering"))

.findFirst();

boolean anyHigh = employees.stream().anyMatch(e -> e.salary() > 100_000);

boolean allAbove50k = employees.stream().allMatch(e -> e.salary() > 50_000);

2.5 Optional

// Optional 생성

Optional<String> opt1 = Optional.of("value"); // null 불가

Optional<String> opt2 = Optional.ofNullable(null); // null 허용 -> empty

Optional<String> opt3 = Optional.empty();

// 값 추출

String val1 = opt1.get(); // NoSuchElementException 위험

String val2 = opt1.orElse("default"); // 기본값 (항상 평가됨)

String val3 = opt1.orElseGet(() -> computeDefault()); // lazy 평가

String val4 = opt1.orElseThrow(() -> new NotFoundException("not found"));

// 변환

Optional<Integer> length = opt1.map(String::length);

Optional<String> upper = opt1.map(String::toUpperCase);

// flatMap: Optional<Optional<T>> 방지

Optional<Address> address = Optional.of(user)

.flatMap(u -> Optional.ofNullable(u.getAddress()));

// 조건부 처리

opt1.ifPresent(System.out::println);

opt1.ifPresentOrElse(

v -> System.out.println("Found: " + v),

() -> System.out.println("Not found")

);

// filter

Optional<String> longVal = opt1.filter(s -> s.length() > 3);

// 안티패턴: Optional.get() without isPresent() 체크

// 안티패턴: 메서드 파라미터로 Optional 사용 (권장하지 않음)

// 권장: 반환값에만 Optional 사용

2.6 병렬 스트림 주의사항

// 병렬 스트림 - CPU 바운드 작업에 적합

List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)

.boxed().collect(Collectors.toList());

long sum = numbers.parallelStream()

.mapToLong(Integer::longValue)

.sum(); // 안전: 상태 없는 연산

// 주의: 공유 상태 변경 (스레드 불안전)

List<Integer> results = new ArrayList<>();

numbers.parallelStream()

.filter(n -> n % 2 == 0)

.forEach(results::add); // 위험! ArrayList는 thread-safe 하지 않음

// 올바른 방법: collect 사용

List<Integer> safeResults = numbers.parallelStream()

.filter(n -> n % 2 == 0)

.collect(Collectors.toList()); // thread-safe

// I/O 바운드 작업에는 Virtual Threads가 더 적합

// 병렬 스트림은 ForkJoinPool.commonPool() 사용 -> 풀 고갈 주의

3. 제네릭 심화

3.1 와일드카드와 PECS 원칙

PECS: Producer Extends, Consumer Super

// 공변(Covariant): extends - 읽기(Producer)에 사용

public double sumList(List<? extends Number> list) {

double sum = 0;

for (Number n : list) {

sum += n.doubleValue(); // 읽기 OK

}

// list.add(1.0); // 컴파일 에러! 쓰기 불가

return sum;

}

// 호출: sumList(List<Integer>), sumList(List<Double>) 모두 가능

// 반공변(Contravariant): super - 쓰기(Consumer)에 사용

public void addNumbers(List<? super Integer> list) {

list.add(1);

list.add(2); // 쓰기 OK

// Integer n = list.get(0); // 컴파일 에러! 타입 보장 불가 (Object로만 읽기)

Object obj = list.get(0); // OK

}

// 호출: addNumbers(List<Integer>), addNumbers(List<Number>), addNumbers(List<Object>) 가능

// 복사 예시 (PECS 완전 적용)

public static <T> void copy(List<? super T> dest, List<? extends T> src) {

for (T item : src) { // src는 Producer -> extends

dest.add(item); // dest는 Consumer -> super

}

}

// 비한정 와일드카드: ? (모든 타입)

public void printList(List<?> list) {

for (Object o : list) {

System.out.println(o);

}

}

3.2 제네릭 메서드와 타입 소거

// 제네릭 메서드

public static <T extends Comparable<T>> T max(T a, T b) {

return a.compareTo(b) >= 0 ? a : b;

}

// 재귀 타입 바운드

public static <T extends Comparable<T>> T findMax(List<T> list) {

return list.stream().max(Comparator.naturalOrder()).orElseThrow();

}

// 타입 소거 (Type Erasure)

// 컴파일 후 List<String>은 List가 됨 (런타임에 타입 정보 없음)

List<String> strings = new ArrayList<>();

List<Integer> integers = new ArrayList<>();

System.out.println(strings.getClass() == integers.getClass()); // true!

// instanceof에 제네릭 타입 사용 불가

// if (obj instanceof List<String>) {} // 컴파일 에러

// 올바른 방법

if (obj instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof String) {

List<String> stringList = (List<String>) list; // unchecked warning

}

// 제네릭 배열 생성 불가

// T[] arr = new T[10]; // 컴파일 에러

// 해결책:

@SuppressWarnings("unchecked")

T[] arr = (T[]) new Object[10];

// 또는 리스트 사용

List<T> list = new ArrayList<>();

4. 컬렉션 프레임워크

4.1 불변 컬렉션 (Java 9+)

// 불변 List (null 불가, 순서 유지)

List<String> immutableList = List.of("a", "b", "c");

// immutableList.add("d"); // UnsupportedOperationException

// 불변 Set

Set<Integer> immutableSet = Set.of(1, 2, 3, 4, 5);

// Set.of는 순서 미보장, 중복 불가

// 불변 Map

Map<String, Integer> immutableMap = Map.of(

"one", 1,

"two", 2,

"three", 3

);

// 10개 이상: Map.ofEntries

Map<String, Integer> bigMap = Map.ofEntries(

Map.entry("a", 1),

Map.entry("b", 2),

Map.entry("c", 3)

// ...

);

// 복사본 (불변)

List<String> original = new ArrayList<>(List.of("a", "b"));

List<String> copy = List.copyOf(original);

Map<String, Integer> mapCopy = Map.copyOf(immutableMap);

4.2 Map 구현체 선택 기준

// HashMap: 순서 없음, O(1) 평균 (일반적인 경우)

Map<String, Integer> hashMap = new HashMap<>();

// LinkedHashMap: 삽입 순서 유지, O(1) 평균

Map<String, Integer> linkedMap = new LinkedHashMap<>();

// TreeMap: 키 기준 정렬, O(log n)

Map<String, Integer> treeMap = new TreeMap<>();

Map<String, Integer> reverseMap = new TreeMap<>(Comparator.reverseOrder());

// ConcurrentHashMap: 스레드 안전, 세그먼트 잠금

ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();

concurrentMap.computeIfAbsent("key", k -> 0);

concurrentMap.merge("key", 1, Integer::sum); // 원자적 카운터

// EnumMap: Enum 키, 매우 빠름

EnumMap<DayOfWeek, String> schedule = new EnumMap<>(DayOfWeek.class);

4.3 List/Deque 구현체

// ArrayList: 랜덤 접근 O(1), 삽입/삭제 O(n)

List<String> arrayList = new ArrayList<>();

// LinkedList: 삽입/삭제 O(1), 랜덤 접근 O(n) (실제로는 캐시 미스로 느림)

// 권장하지 않음 - ArrayDeque가 대부분의 경우 더 빠름

// ArrayDeque: 양방향 큐, 스택/큐 역할 모두 가능, 빠름

Deque<String> deque = new ArrayDeque<>();

deque.push("first"); // 스택: 앞에 추가

deque.offerLast("last"); // 큐: 뒤에 추가

String top = deque.peek(); // 첫 번째 확인 (제거 없음)

String removed = deque.poll(); // 첫 번째 제거

// CopyOnWriteArrayList: 읽기 많고 쓰기 적은 경우, 스레드 안전

CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

5. 예외 처리 패턴

5.1 Checked vs Unchecked Exception

// Checked Exception: 컴파일러가 처리 강제 (IOException, SQLException 등)

public String readFile(String path) throws IOException {

return Files.readString(Path.of(path));

}

// Unchecked Exception: RuntimeException 하위 (처리 선택)

public int divide(int a, int b) {

if (b == 0) throw new ArithmeticException("Division by zero");

return a / b;

}

// 커스텀 예외 계층 설계

public class AppException extends RuntimeException {

private final String errorCode;

public AppException(String errorCode, String message) {

super(message);

this.errorCode = errorCode;

}

public AppException(String errorCode, String message, Throwable cause) {

super(message, cause);

this.errorCode = errorCode;

}

public String getErrorCode() { return errorCode; }

}

public class ResourceNotFoundException extends AppException {

public ResourceNotFoundException(String resourceType, Object id) {

super("RESOURCE_NOT_FOUND",

resourceType + " not found with id: " + id);

}

}

public class ValidationException extends AppException {

private final Map<String, String> fieldErrors;

public ValidationException(Map<String, String> fieldErrors) {

super("VALIDATION_FAILED", "Validation failed");

this.fieldErrors = Map.copyOf(fieldErrors);

}

public Map<String, String> getFieldErrors() { return fieldErrors; }

}

5.2 try-with-resources & Multi-catch

// try-with-resources (AutoCloseable 구현체)

public String readAndClose(String path) throws IOException {

try (BufferedReader reader = new BufferedReader(new FileReader(path))) {

return reader.lines().collect(Collectors.joining("\n"));

} // reader.close() 자동 호출

}

// 여러 리소스 (역순으로 닫힘)

public void copyFile(String src, String dest) throws IOException {

try (

InputStream in = new FileInputStream(src);

OutputStream out = new FileOutputStream(dest)

) {

in.transferTo(out);

}

}

// Multi-catch

public void process(String input) {

try {

int value = Integer.parseInt(input);

String result = riskyOperation(value);

System.out.println(result);

} catch (NumberFormatException | IllegalArgumentException e) {

System.err.println("Invalid input: " + e.getMessage());

} catch (RuntimeException e) {

log.error("Unexpected error", e);

throw e; // 재발생

}

}

// Effective Java: 예외를 삼키지 말 것

// Bad:

try { riskyOperation(); } catch (Exception e) { /* 무시 */ }

// Good:

try { riskyOperation(); } catch (Exception e) {

log.error("Operation failed", e);

throw new AppException("OPERATION_FAILED", "Failed to complete operation", e);

}

6. 동시성 (Concurrency)

6.1 CompletableFuture

// 비동기 작업 체인

CompletableFuture<String> future = CompletableFuture

.supplyAsync(() -> fetchUserId()) // 비동기 시작

.thenApplyAsync(id -> fetchUser(id)) // 변환 (새 스레드)

.thenApply(user -> user.getName()) // 변환 (같은 스레드)

.exceptionally(ex -> "Unknown User"); // 예외 처리

// thenCompose: flatMap 역할 (비동기 체인)

CompletableFuture<Order> orderFuture = CompletableFuture

.supplyAsync(() -> fetchUser(userId))

.thenCompose(user -> CompletableFuture.supplyAsync(() -> fetchLatestOrder(user)));

// thenCombine: 두 비동기 결과 합치기

CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id));

CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> fetchOrders(id));

CompletableFuture<UserProfile> profileFuture = userFuture.thenCombine(

ordersFuture,

(user, orders) -> new UserProfile(user, orders)

);

// allOf: 모두 완료까지 대기

CompletableFuture<Void> allDone = CompletableFuture.allOf(

task1, task2, task3

);

allDone.thenRun(() -> System.out.println("All done!"));

// anyOf: 하나라도 완료

CompletableFuture<Object> firstDone = CompletableFuture.anyOf(task1, task2, task3);

// 타임아웃

CompletableFuture<String> withTimeout = future

.orTimeout(5, TimeUnit.SECONDS)

.completeOnTimeout("default", 3, TimeUnit.SECONDS);

// 커스텀 Executor 지정

Executor executor = Executors.newFixedThreadPool(10);

CompletableFuture.supplyAsync(() -> heavyTask(), executor);

6.2 Virtual Threads와 구조적 동시성

// Virtual Thread 활용 - I/O 집약적 작업

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {

List<CompletableFuture<String>> tasks = urls.stream()

.map(url -> CompletableFuture.supplyAsync(() -> httpGet(url), executor))

.collect(Collectors.toList());

List<String> results = tasks.stream()

.map(CompletableFuture::join)

.collect(Collectors.toList());

}

// ReentrantLock

ReentrantLock lock = new ReentrantLock(true); // fair lock

lock.lock();

try {

// critical section

} finally {

lock.unlock(); // finally에서 반드시 해제

}

// tryLock으로 타임아웃

if (lock.tryLock(1, TimeUnit.SECONDS)) {

try {

// critical section

} finally {

lock.unlock();

}

} else {

// 잠금 획득 실패 처리

}

// ReadWriteLock: 다수 읽기, 단일 쓰기

ReadWriteLock rwLock = new ReentrantReadWriteLock();

Map<String, String> cache = new HashMap<>();

public String get(String key) {

rwLock.readLock().lock();

try {

return cache.get(key);

} finally {

rwLock.readLock().unlock();

}

}

public void put(String key, String value) {

rwLock.writeLock().lock();

try {

cache.put(key, value);

} finally {

rwLock.writeLock().unlock();

}

}

7. 모던 Java 패턴

7.1 Builder Pattern

// 수동 Builder

public class HttpRequest {

private final String url;

private final String method;

private final Map<String, String> headers;

private final String body;

private final int timeoutMs;

private HttpRequest(Builder builder) {

this.url = builder.url;

this.method = builder.method;

this.headers = Map.copyOf(builder.headers);

this.body = builder.body;

this.timeoutMs = builder.timeoutMs;

}

public static Builder builder(String url) {

return new Builder(url);

}

public static class Builder {

private final String url;

private String method = "GET";

private Map<String, String> headers = new HashMap<>();

private String body;

private int timeoutMs = 5000;

private Builder(String url) { this.url = url; }

public Builder method(String method) { this.method = method; return this; }

public Builder header(String key, String value) { headers.put(key, value); return this; }

public Builder body(String body) { this.body = body; return this; }

public Builder timeout(int ms) { this.timeoutMs = ms; return this; }

public HttpRequest build() { return new HttpRequest(this); }

}

}

// 사용

HttpRequest request = HttpRequest.builder("https://api.example.com/users")

.method("POST")

.header("Content-Type", "application/json")

.header("Authorization", "Bearer token123")

.body("{\"name\": \"Alice\"}")

.timeout(3000)

.build();

7.2 Sealed Class + Pattern Matching으로 ADT 구현

// Result 타입 (Either와 유사)

public sealed interface Result<T> permits Result.Ok, Result.Err {

record Ok<T>(T value) implements Result<T> {}

record Err<T>(String message, Throwable cause) implements Result<T> {

public Err(String message) { this(message, null); }

}

static <T> Result<T> ok(T value) { return new Ok<>(value); }

static <T> Result<T> err(String message) { return new Err<>(message); }

static <T> Result<T> of(Supplier<T> supplier) {

try {

return ok(supplier.get());

} catch (Exception e) {

return err(e.getMessage());

}

}

default <U> Result<U> map(Function<T, U> mapper) {

return switch (this) {

case Ok<T> ok -> Result.ok(mapper.apply(ok.value()));

case Err<T> err -> Result.err(err.message());

};

}

default T getOrElse(T defaultValue) {

return switch (this) {

case Ok<T> ok -> ok.value();

case Err<T> ignored -> defaultValue;

};

}

}

// 사용 예시

Result<Integer> result = Result.of(() -> Integer.parseInt("123"));

Result<String> mapped = result.map(n -> "Number: " + n);

String output = mapped.getOrElse("Parse failed");

System.out.println(output); // "Number: 123"

// Command 패턴 (ADT)

public sealed interface Command permits Command.CreateUser, Command.DeleteUser, Command.UpdateEmail {

record CreateUser(String name, String email) implements Command {}

record DeleteUser(String userId) implements Command {}

record UpdateEmail(String userId, String newEmail) implements Command {}

}

public void handle(Command command) {

switch (command) {

case Command.CreateUser(String name, String email) ->

userService.create(name, email);

case Command.DeleteUser(String userId) ->

userService.delete(userId);

case Command.UpdateEmail(String userId, String newEmail) ->

userService.updateEmail(userId, newEmail);

}

}

7.3 Spring Boot와의 연계

// Record를 DTO로 사용

public record CreateUserRequest(

@NotBlank String name,

@Email String email,

@Min(0) @Max(150) int age

) {}

public record UserResponse(

String id,

String name,

String email,

LocalDateTime createdAt

) {

public static UserResponse from(User user) {

return new UserResponse(

user.getId(),

user.getName(),

user.getEmail(),

user.getCreatedAt()

);

}

}

// Controller

@RestController

@RequestMapping("/api/users")

public class UserController {

@PostMapping

public ResponseEntity<UserResponse> createUser(

@RequestBody @Valid CreateUserRequest request) {

User user = userService.create(request);

return ResponseEntity.status(HttpStatus.CREATED)

.body(UserResponse.from(user));

}

}

// Sealed class로 API 응답 통일

public sealed interface ApiResponse<T> permits ApiResponse.Success, ApiResponse.Error {

record Success<T>(T data, String message) implements ApiResponse<T> {}

record Error<T>(String code, String message, List<String> details) implements ApiResponse<T> {}

}

8. 퀴즈

**보기:**

- A) 컴파일러가 equals, hashCode, toString을 자동 생성한다

- B) 인스턴스 필드를 추가로 선언할 수 있다

- C) 인터페이스를 구현할 수 있다

- D) 다른 클래스를 상속받을 수 없다

**정답**: B

**설명**: Record는 헤더에 정의된 컴포넌트 외에 인스턴스 필드를 추가로 선언할 수 없습니다. static 필드는 가능합니다. Record는 `java.lang.Record`를 암묵적으로 상속하므로 다른 클래스를 상속받을 수 없으며, 인터페이스는 구현 가능합니다.

**보기:**

- A) `extends T`

- B) `super T`

- C) `?`

- D) 와일드카드 불필요

**정답**: B (`super T`)

**설명**: PECS(Producer Extends, Consumer Super)에서 list가 element를 받아들이는 Consumer 역할을 하므로 `? super T`를 사용합니다. `? extends T`는 읽기(Producer)에 사용합니다. `List<? super T>`는 T 타입 또는 T의 상위 타입 리스트를 받을 수 있어 T 요소를 안전하게 추가할 수 있습니다.

**보기:**

- A) Virtual Thread는 OS 스레드와 1:1 매핑된다

- B) CPU 집약적 작업에 특히 효과적이다

- C) 블로킹 I/O 시 캐리어 스레드에서 마운트 해제된다

- D) `synchronized` 블록에서도 항상 최적의 성능을 발휘한다

**정답**: C

**설명**: Virtual Thread는 블로킹 I/O 발생 시 OS 스레드(캐리어 스레드)에서 언마운트되어 다른 Virtual Thread가 해당 OS 스레드를 사용할 수 있습니다. CPU 집약적 작업보다 I/O 집약적 작업에 적합하며, `synchronized` 블록에서는 pinning이 발생할 수 있어 성능 저하가 있을 수 있습니다(ReentrantLock 권장).

**보기:**

- A) 20

- B) 29

- C) 25

- D) 4

**정답**: A (20)

**설명**: filter로 짝수 선별 [2, 4], map으로 제곱 [4, 16], reduce로 합산 4 + 16 = 20입니다.

**보기:**

- A) `final`로 선언되어 더 이상 상속 불가

- B) `sealed`로 선언되어 자신의 하위 타입을 제한

- C) `non-sealed`로 선언되어 자유롭게 상속 허용

- D) `abstract`로 선언되어 직접 인스턴스화 불가

**정답**: D

**설명**: Sealed Class의 직접 하위 클래스는 반드시 `final`, `sealed`, 또는 `non-sealed` 중 하나로 선언되어야 합니다. `abstract`만으로는 선언할 수 없습니다(abstract는 sealed나 final과 함께 사용 가능하지만 단독으로 permits 조건을 충족하지 않습니다). Java 컴파일러는 Sealed 계층의 모든 하위 클래스를 알고 있어야 하기 때문입니다.

마무리

모던 Java는 단순한 문법 개선을 넘어 함수형 프로그래밍, 타입 안전성, 고성능 동시성을 언어 차원에서 지원합니다. Java 21 LTS를 기반으로:

- **Record + Sealed Class + Pattern Matching**: 타입 안전한 도메인 모델

- **Stream API + Optional**: 선언적 데이터 처리

- **Virtual Threads**: 고성능 I/O 동시성

- **CompletableFuture**: 비동기 파이프라인

이 기능들을 적절히 조합하면 간결하고 안전하며 고성능인 Java 코드를 작성할 수 있습니다.

현재 단락 (1/788)

Java는 꾸준히 진화하고 있습니다. Java 21 LTS는 Project Loom(Virtual Threads), Pattern Matching, Record, Sealed Cl...

작성 글자: 0원문 글자: 24,397작성 단락: 0/788