이 글은 모던 자바 인 액션 책을 읽고 정리한 내용입니다.
http://www.yes24.com/Product/Goods/77125987
1.1 역사의 흐름은 무엇인가?
자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다. 자바 9에서도 중요한 변화가 있었지만 곧 책에서 살펴볼 수 있듯이 자바 8만큼 획기적이거나 생산성이 바뀌는 것은 아니다. 자바 10에서는 형 추론과 관련해 약간의 변화만 일어났다. 이런 크고 작은 변화 덕분에 프로그램을 더 쉽게 구현할 수 있게 되었다. 예를 들어 다음은 사과 목록을 무게순으로 정렬하는 고전적 코드다.
Coolecrions.sort(inventory, new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().comepareTo(a2.getWeight());
}
});
자바 8을 이용하면 자연어에 더 가깝게 간단한 방식으로 코드를 구현할 수 있다.
inventory.sort(comparing(Apple::getWeight));
위 코드는 사과의 무게를 비교해서 목록에서 정렬한다. 코드가 어떻게 작동하는지는 나중에 설명할 것이므로 지금은 신경 쓰지 말자.
자바 8은 간결한 코드, 멀티 코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 한다.
자바 8 에서 제공하는 새로운 기술
- 스트림 API
- 메서드에 코드를 전달하는 기법
- 인터페이스의 디폴트 메서드
1.2.2 스트림 처리
스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있다.
자바 8에는 java.util.stream
패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream<T>
는 T 형식으로 구성된 일련의 항목을 의미한다. 우선은 스트림 API가 조립 라인처럼 어떤 항목을 연속으로 제공하는 어떤 기능이라고 단순하게 생각하자.
스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만, 이제 자바 8에서는 우리가 하려는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다.
장점
- 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다.
- 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.
1.2.3 동작 파라미터화로 메서드에 코드 전달하기
자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.
예를 들어 2013UK0001, 2014050002. ... 등의 형식을 갖는 송장 ID가 있다고 가정하자. 처 음 네 개의 숫자는 연도를, 다음 두 글자는 국가 코드를, 마지막 네 개의 숫자는 고객 ID를 의 미한다. 이제 우리는 이 송장 ID를 고객 ID 또는 국가 코드순으로 정렬해야 한다. sort 명령을 이용하려면 sort가 고객 ID나 국가 코드로 송장 ID를 정렬하도록 sort에 따로 코드를 제공해야 한다.
그런데 자바 8 이전의 자바에서는 메서드를 다른 메서드로 전달할 방법이 없었다. Comparator
객체를 만들어서 sort에 넘겨주는 방법도 있지만 이는 너무 복잡하며 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않다. 자바 8에서는 메서드 (우리 코드)를 다른 메서드의 인수로 넘겨주는 기능을 제공한다. 이것을 동작 파라미터화 라고 한다.
1.2.4 병렬성과 공유 가변 데이터
- '병렬성을 공짜로 얻을 수 있다'는 말에서 시작
- 대신 스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 함함수형 패러다임의 핵심적인 사항
- 공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은
명령형 프로그래밍 패러다임
- 일련의 가변 상태로 프로그램을 정의
1.3 자바 함수
프로그래밍 언어에서 함수 (function)라는 용어는 메서드 (method) 특히 정적 메서드 (static method)와 같은 의미로 사용된다.
프로그래밍 언어의 핵심은 값을 바꾸는 것이고 이 값을 일급 값(또는 시민) 이라고 부른다. 하지만, 프로그램을 실행하는 동안 모든 구조체를 자유롭게 전달할 수 없다. 이러한 구조체는 이급 시민이다.
1.3.1 메서드와 람다를 일급 시민으로
자바 8 이전
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden(); // 숨겨진 파일 필터링
}
})
- 각 행이 무슨 작업을 하는지 투명하지 X
- File 클래스에는 이미 isHidden이 있는데 굳이 FileFilter로 isHidden을 복잡하게 감싼 다음에 FileFilter을 인스턴스화 해야 할까?
자바 8
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
- 메서드 참조 (method reference) :: ('이 메서드를 값으로 사용하라'의 의미)를 이용해서 listFiles 에 직접 전달 가능
- 람다 : 익명함수
- 예)
(int x) -> x + 1
, 즉'x
라는 인수로 호출하면x + 1
을 반환' - 이용할 수 있는 편리한 클래스나 메서드가 없을 때 람다 문법을 이용해 더 간결하게 코드를 구현 가능
- 예)
1.3.2 코드 넘겨주기 : 예제
자바 8 이전
- 녹색 사과만 선택 (특정 항목을 선택해서 반환)
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (GREEN.equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
- 사과를 무게로 필터링 (150그램 이상)
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
- 이 예제에서 두 메서드는 한 줄의 코드만 다르다. 만약, 두 메서드가 단순히 크기를 기준으로 사과를 필터링하는 상황이었다면 인수로 (150, 1000)을 넘겨주어 150그램 이상의 사과를 선택하거나, (0, 80)을 넘겨주어 80그램 이하의 사과를 선택할 수 있을 것이다.
자바 8 이후
class a {
public static boolean isGreenApple(Apple apple) {
return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public interface Predicate<T> {
boolean test(T t);
}
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
//메서드가 p라는 이름의 프레디케이트 파라미터로 전달됨.
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
}
- 다음처럼 메서드 호출 가능
filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);
📌 프레디케이트(predicate)란?
수학에서는 인수로 값을 받아 true나 false를 반환하는 함수를 프레디케이트라고 한다.
1.3.3 메서드 전달에서 람다로
람다 도입
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()));
filterApples(inventory, (Apple a) -> a.getWeight() > 150);
- 병렬성이라는 중요성 때문에 설계자들은 이와 같은 설계를 포기하고 새로운 스트림 API를 제공한다.
1.4 스트림
스트림 X
Map<Currency, List<Transaction>> transactionByCurrencies = new HashMap<>();
for (Transaction transaction : transactions) {
if (transaction.getPrice() > 1000) { //고가의 트랜잭션 필터링
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionByCurrencies.get(currency);
if (transactionsForCurrency == null) { //그룹화된 맵에 항목이 없으면? 새로 생성
transactionsForCurrency = new ArrayList<>();
transactionByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction); //같은 통화의 트랜잭션 리스트에 추가
}
}
스트림 API 이용
import java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies
= transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000) //필터링
.collect(groupingBy(Transaction::getCurrency)); //그룹화
1.4.1 멀티스레딩은 어렵다
자바 8은 스트림 API 로 '컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제' 그리고 '멀티코어 활용 어려움'이라는 두 가지 문제를 모두 해결했다.
기존의 컬렉션에서 데이터를 처리할 때, 반복되는 패턴이 많았다. -> 자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링하거나, 데이터를 추출하거나, 데이터를 그룹화 하는 등의 기능이 있다.
1.5 디폴트 메서드와 자바 모듈
- 자바 9 의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다.
- 또한 자바 8 에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.
- 디폴트 메서드를 이용하면 기존의 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있다.
- 예를 들어 자바 8에서는 List 에 직접 sort 메서드를 호출할 수 있다.
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어
- 자바 8 에서는 NullPointer 예외를 피할 수 있도록 Optional 클래스 제공한다.
- Optional은 값이 없는 상황을 어떻게 처리할 지 명시적으로 구현하는 메서드를 포함한다.
- 패턴 매칭
1.7 마치며
- 언어 생태계의 모든 언어는 변화 후 살아 남거나 사라진다.
- 자바 8은 프로그램을 더 효과적이고 간결하게 구현할 수 있는 새로운 개념과 기능을 제공한다.
- 함수는 일급 값이다.
- 자바 8 스트림 일부는 컬렉션에서 가져온 것이다.
- 자바 9 부터 모듈을 이용해 시스템의 구조를 만들 수 있고 디폴트 메서드를 이용해 기존 인터페이스를 구현하는 클래스를 바꾸지 않고도 인터페이스를 변경할 수 있다.
- 함수형 프로그래밍에서의 null 처리 방법 등 기법 발견했다.