모던 자바 인 액션 - CH01. 자바 8, 9, 10,11 : 무슨 일이 일어나고 있는가?
1.1 역사의 흐름은 무엇인가?
가장 큰 변화는 자바 8에서 일어남
멀티코어 CPU 대중화가 영향을 미침 -> 자바8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용을 바탕으로 탄생
- 스트림 API - 병렬 연산을 지원
- 메서드 코드를 전달하는 기법 - 새롭고 간결한 방식으로 동작 파라미터화를 구현 가능
- 인터페이스의 디폴트 메서드
자바 8은 함수형 프로그래밍에서 위력을 발휘한다.
1.2 왜 아직도 자바는 변화하는가?
특정 분야에서 장점은 가진 언어는 다른 경쟁 언어를 도태시킨다.
1.2.1 프로그래밍 언어 생태계에서 자바의 위치
이전에 자바는 객체지향 모델 덕분에 대중적인 프로그래밍 언어로 성장할 수 있었다. 하지만 이후에, 빅데이터를 효과적으로 처리할 필요성이 커졌다. 이전의 자바로는 충분히 대응할 수 없었기에 변화가 필요했다. 따라서 자바 8이 개발되고 멀티코어 병렬성이 강화되었다.
큰 시스템의 설계 방식의 변경도 영향을 끼쳤다. 외부의 큰 하위시스템에서 컴포넌트를 추가하고 다른 벤더가 만든 컴포넌트를 이용해 개발하는 사례가 늘었다. 따라서 자바8, 자바9에서 디폴트 메소드와 모듈을 제공한다.
결론 : 새로운 언어가 등장하고 새로운 언어는 변화하는 환경에 빠르게 적응하면서 점점 대중화 된다.
1.2.2 스트림 처리
자바 8에는 java.utils.stream 패키지에 스트림 API가 추가되었다.
스트림 패키지에 정의된 Stream<T>는 T 형식으로 구성된 일련의 항목을 의미한다.
스트림이란?
한 번에 한 개씩 만들어지는 역속적인 데이터 항목들의 모임
스트림 API의 핵심?
- 기존에는 한 번에 한 항목을 처리했지만 자바 8에서는 우리가 하려는 작업을 (데이터베이스 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것
- 스트림파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 것
- 즉, 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.
1.2.3 동작 파라미터화로 메서드에 코드 전달하기
자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.
-> 동작 파라미터화 : 메서드를 다른 메서드의 인수로 넘겨주는 기능
이후에 2장, 3장에서 자세하게 배우게 된다고 한다..
1.2.4 병렬성과 공유 가변 데이터
- 병렬성을 위해서는 스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다.
- 스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다. -> 공유된 가변 데이터에 접근하지 않아야 한다. -> 이것을 순수 함수, 부작용 없는 함수, 상태 없는 함수라고 부른다.
- 공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드로 전달하는 두가지 기능은 함수형 프로그래밍의 핵심적인 사항
1.2.5 자바가 진화해야 하는 이유
- 제네릭이 나타남 -> 컴파일을 할 때 더 많은 에러를 검출할 수 있으며, 리스트의 유형을 알 수 있어 가독성이 좋아짐
- Iterator 대신 for-each 루프 -> 고전적 객체지향x, 함수형 프로그래밍으로 다가섬
자바 8 함수형 프로그래밍을 도입함으로써 전통적 객체지향 프로그래밍, 함수형 프로그래밍 두 가지 패러다임의 장점을 모두 활용할 수 있게 됨
=> 요약 : 언어는 하드웨어나 프로그래머의 기대의 변화에 부응하는 방향으로 변화해야 한다.
1.3 자바 함수
메서드를 일급 시민으로 만들면 프로그래밍에 유용하게 활용할 수 있다. 자바 8 설계자들은 이급 시민을 일급 시민을 바꿀 수 있는 기능을 추가했다.
- 일급 시민 : 조작할 수 있는 값 (int형식, double형식), 객체(객체의 참조, 인스턴스, 배열)
- 이급 시민 : 메서드, 클래스
1.3.1 메서드와 람다를 일급 시민으로
메서드를 일급값으로 사용하면 프로그래머가 활용할 수 있는 도구가 다양해지면서 프로그래밍이 수월해진다. 그래서 자바 8 설계자들은 메서드를 값으로 취급할 수 있게 자바 8을 설계하였다.
메서드 참조
File[] hiddenFiles = new File(".").listFiles(new FileFilter(){
public boolean accept(File file) {
return file.isHidden();
}
});
자바 8에서는 위 코드를 다음 처럼 구현할 수 있다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
이미 isHidden이라는 함수는 준비되어 있으므로 자바 8의 메서드 참조 ::('이 메서드를 값으로 사용하라'는 의미)를 이용해서 listFiles에 직접 전달할 수 있다.
람다 : 익명 함수
'x라는 인수로 호출하면 x+1을 반환'하는 동작을 (int x) -> x+1 로 코드를 구현할 수 있다.
직접 메서드를 정의할 수도 있지만, 이용할 수 있는 편리한 클래스나 메서드가 없을 때 새로운 람다 문법을 이용하면 더 간결하게 코드를 구현할 수 있다.
1.3.2 코드 넘겨주기 : 예제
Apple 클래스와 getColor 메서드가 있고, Apples 리스트를 포함하는 변수 inventory가 있다.
모든 녹색 사과를 선택해서 반환하는 프로그램을 구현하려고 한다.
자바 8 이전에는 다음처럼 filterGreenApples라는 메서드를 구현했을 것이다.
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;
}
사과를 무게로 필터링하고 싶을 때는 다음처럼 코드를 복붙해서 사용할 수 있다.
public static List<Apple> filterHeavyApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (apple.getWeight() > 150) {
result.add(apple);
}
} return result;
}
자바 8에서는 코드를 인수로 넘겨줄 수 있으므로 filter 메서드를 중복으로 구현할 필요가 없다. 위의 코드를 자바 8에 맞게 구현할 수 있다.
public static boolean isGreenApple(Apple apple) {
return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
메서드가 p라는 이름의 프레디케이트 파라미터로 전달된다. 메서드를 다음처럼 호출할 수 있다.
filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);
1.3.3 메서드 전달에서 람다로
isHeavyApple, isGreenApple 처럼 한두 번만 사용할 메서드를 매번 정의하는 것은 귀찮다. 자바 8에서는 람다로 이 문제도 해결할 수 있다.
filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight > 150 );
라이브러리 메서드 filter를 이용하면 filterApples 메서드를 구현할 필요도 없다.
filter(inventory, (Apple a) -> a.getWeight() > 150);
1.4 스트림
리스트에서 고가의 트랜잭션(거래)만 필터링한 다음에 통화로 결과를 그룹화하려면 다음과 같이 많은 기본 코드를 구현해야한다.
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for(Transaction transaction : transactions) {
if(transaction.getPrice() > 1000) {
Currency currrency = transaction.getCurrency();
List<Transacion> transactionsForCurrency = transactionsByCurrencies.get(currency);
if(transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
}
중첩된 제어 흐름 문장이 많아서 코드를 한 번에 이해하기 어렵다. 스트림 API를 이용하면 다음처럼 문제를 해결할 수 있다.
import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream()
.filter((Transaction t) -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
컬렉션에서는 반복 과정을 직접 처리해야 했다. 즉, for-each 루프를 이용해서 각 요소를 반복하면서 작업을 수행한다.(외부반복) 반면 스트림 API를 이용하면 루프를 신경 쓸 필요가 없다. 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다.(내부 반복)
1.4.1 멀티스레딩은 어렵다
자바 8은 스트림 API로 '컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제' 그리고 '멀티코어 활용 어려움'이라는 두 가지 문제를 모두 해결했다.
컬렉션을 필터링할 수 있는 가장 빠른 방법은 컬렉션을 스트림으로 바꾸고, 병렬로 처리한 다음에, 리시트로 다시 복원하는 것이다.
스트림과 람다 표현식을 이용하면 '병렬성을 공짜로'얻을 수 있고 리스트에서 순차적으로 또는 병렬로 필터링할 수 있다.
순차 처리방식 코드
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
병렬 처리 방식 코드
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
1.5 디폴트 메서드와 자바 모듈
자바 8은 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다. 메서드 본문은 클래스 구현이 아니라 엔터페이스의 일부로 포함된다. 그래서 이를 디폴트 메서드라고 부른다. 디폴트 메서드를 이용하면 기존의 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있다.
자바 8에서는 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문에 List에 직접 sort 메서드를 호출할 수 있다.
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
자바 8 이전에는 List를 구현하는 모든 클래스가 sort를 구현해야 했지만 자바 8부터는 디폴트 sort를 구현하지 않아도 된다.
1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어
자바 8에서는
- Optional<T> 클래스를 제공해서 NullPointer 예외를 피할 수 있도록 도와준다.
- 구조적 패턴 매칭 기법을 사용해서 if-then-else나 switch문 보다 더 정확한 비교를할 수 있다.