출시·고도화 중
Java 안내서 · 6/6
현대 Java 코드를 읽고 쓰려면 람다와 함수형 인터페이스, 스트림, Optional, 그리고 동시성의 기본기를 알아야 합니다. 이 장에서는 컬렉션을 선언적으로 처리하는 방법과 여러 작업을 동시에 실행하는 방법을 예제로 살펴봅니다.
추상 메서드가 하나뿐인 인터페이스를 함수형 인터페이스라고 합니다. 람다 식은 이런 인터페이스의 구현을 짧게 적는 문법입니다. 이미 있는 메서드를 그대로 넘길 때는 Integer::parseInt 같은 메서드 참조를 씁니다.
| 인터페이스 | 메서드 | 쓰임 |
|---|---|---|
Function<T, R> | R apply(T t) | 값을 다른 값으로 변환 |
Predicate<T> | boolean test(T t) | 조건 검사 |
Supplier<T> | T get() | 값 생성 |
Consumer<T> | void accept(T t) | 값을 받아 처리 |
import java.util.function.*;
Function<String, Integer> length = s -> s.length();
Predicate<String> isEmpty = String::isEmpty;
Supplier<java.util.List<String>> newList = java.util.ArrayList::new;
Consumer<String> printer = System.out::println;
Predicate<String> notEmpty = isEmpty.negate();
Function<String, Integer> lengthPlusOne = length.andThen(n -> n + 1);
printer.accept("len = " + lengthPlusOne.apply("java")); // len = 5람다 안에서는 바깥의 지역 변수를 읽을 수 있지만, 그 변수는 다시 대입되지 않는(사실상 final) 변수여야 합니다. 직접 함수형 인터페이스를 만들 때는 @FunctionalInterface 를 붙이면 컴파일러가 조건을 검사해 줍니다.
스트림은 컬렉션 같은 데이터 원본을 filter, map, sorted 같은 중간 연산으로 가공하고, toList, count, collect 같은 최종 연산으로 결과를 얻는 파이프라인입니다. 최종 연산을 호출하기 전까지는 실제 계산이 일어나지 않으며, 원본 컬렉션은 바뀌지 않습니다.
import java.util.*;
import java.util.stream.*;
record Employee(String name, String team, int salary) {}
List<Employee> staff = List.of(
new Employee("Kim", "dev", 5200),
new Employee("Lee", "dev", 6100),
new Employee("Park", "ops", 4800));
List<String> highPaid = staff.stream()
.filter(e -> e.salary() >= 5000)
.map(Employee::name)
.sorted()
.toList(); // [Kim, Lee]
Map<String, Double> avgByTeam = staff.stream()
.collect(Collectors.groupingBy(Employee::team,
Collectors.averagingInt(Employee::salary)));
int total = staff.stream().mapToInt(Employee::salary).sum();IntStream.range(0, 10) 처럼 기본 타입 전용 스트림을 쓰면 박싱 비용 없이 합계나 평균을 구할 수 있습니다. 스트림은 한 번만 소비할 수 있으므로 같은 데이터를 다시 처리하려면 새 스트림을 만듭니다. 단순한 반복이나 중간에 빠져나와야 하는 로직은 일반 for 문이 더 읽기 쉬울 때도 있습니다.
Optional<T> 은 값이 있을 수도 없을 수도 있다는 것을 타입으로 드러냅니다. 주로 메서드의 반환 타입에 써서 호출자가 "없음"을 처리하도록 유도합니다. get() 을 바로 호출하기보다 map, orElse, orElseThrow, ifPresent 로 다루는 것이 안전합니다.
import java.util.Optional;
static Optional<Employee> findByName(List<Employee> list, String name) {
return list.stream()
.filter(e -> e.name().equals(name))
.findFirst();
}
String team = findByName(staff, "Lee")
.map(Employee::team)
.orElse("unknown"); // dev
findByName(staff, "Choi").ifPresentOrElse(
e -> System.out.println(e.team()),
() -> System.out.println("없음"));
Employee lee = findByName(staff, "Lee")
.orElseThrow(() -> new IllegalStateException("Lee 없음"));필드나 메서드 매개변수, 컬렉션의 원소에는 Optional 을 쓰지 않는 것이 일반적인 권장 사항입니다. 빈 컬렉션은 그 자체로 "없음"을 표현할 수 있습니다.
Java는 처음부터 스레드를 지원했지만, 스레드를 직접 만들고 관리하기보다 ExecutorService 에 작업을 맡기는 것이 기본입니다. submit 은 결과를 나중에 받을 수 있는 Future 를 돌려주며, get() 은 결과가 준비될 때까지 기다립니다.
import java.util.*;
import java.util.concurrent.*;
static int slowSquare(int n) throws InterruptedException {
Thread.sleep(100); // 오래 걸리는 작업을 흉내
return n * n;
}
public static void main(String[] args) throws Exception {
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int n = i;
futures.add(pool.submit(() -> slowSquare(n)));
}
int sum = 0;
for (Future<Integer> f : futures) sum += f.get();
System.out.println("sum = " + sum); // 55
} // close()가 남은 작업을 기다린 뒤 종료
}최근의 JDK는 가상 스레드(virtual thread)를 제공합니다. 가상 스레드는 JVM이 관리하는 가벼운 스레드라서 요청마다 하나씩 만들어도 부담이 적고, 네트워크나 파일 I/O를 기다리는 작업이 많은 서버에 잘 맞습니다. 코드 작성 방식은 일반 스레드와 같고, 실행기만 바꾸면 됩니다.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
executor.submit(() -> {
Thread.sleep(50); // 블로킹해도 부담이 적다
return id;
});
}
}여러 스레드가 같은 데이터를 바꿀 때는 ConcurrentHashMap, AtomicInteger 같은 스레드 안전한 클래스나 synchronized, ReentrantLock 으로 접근을 조율해야 합니다. 가능하면 불변 객체(레코드 등)를 공유하고, 변경 가능한 상태는 한곳에 모으는 설계가 버그를 줄입니다.
filter, map, collect 로 데이터를 선언적으로 가공합니다.Optional 은 반환값의 "없음"을 드러내며 map, orElse, orElseThrow 로 다룹니다.ExecutorService 에 맡기고, I/O가 많은 작업에는 가상 스레드를 고려합니다.더 깊이 공부하려면 다음 공식 자료를 참고합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.