- Published on
Chapter 1. 자바 8, 9, 10, 11: 무슨 일이 일어나고 있는가?
Chapter 1. 자바 8, 9, 10, 11: 무슨 일이 일어나고 있는가?
1.1 역사의 흐름은 무엇인가?
01 자바 역사를 통틀어 가장 큰 변화는 Java 8
에서 일어났습니다.
예를 들어, 사과 목록을 무게순으로 정렬하는 고전적인 방법은 아래와 같습니다.
class Foo {
public static void main(String[] args) {
Collections.sort(inventory, new Comparator<Apply>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
}
}
Java 8
에서는 아래와 같이 간단하게 구현 할 수 있습니다.
class Foo {
public static void main(String[] args) {
inventory.sort(comparing(Apply::getWeight));
}
}
또한, Java 8
등장 이전까지 대부분의 자바 프로그램은 코어 중 하나만을 사용했습니다. 나머지 코어를 활용하기 위해서는 스레드
를 사용하는 방법이 있지만, 관리하기가 어렵고 많은 문제가 발생할 수 있다는 단점이 있습니다.
자바는 이러한 병렬 실행 환경을 쉽게 관리하고 에러가 덜 방생하는 방향으로 진화하려고 노력했습니다.
Java 1.0
에서는 스레드와 락, 심지어 메모리 모델까지 지원했지만 특별전문가로 구성된 팀이 아닌 이상 사용하기 어려웠습니다.Java 5
에서는 스레드 풀, 병렬 실행 컬렉션 등을 강력한 도구를 도입하였고Java 7
에서는 병렬 실행에 도움을 줄 수 있는 포크/조인 프레임워크를 제공했지만, 개발자가 활용하기에는 쉽지 않았습니다.Java 8
에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공합니다. 단, 몇 가지 규칙을 지켜야 합니다.Java 9
에서는 리액티브 프로그래밍이라는 병렬 실행 기법을 지원합니다. 이 기법을 사용할 수 있는 상황은 한정되어 있지만, 고성능 병렬 시스템에서 인기를 얻고 있는RxJava
를 표준 방식으로 지원합니다.
Java 8
은 간결한 코드와 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 합니다. 새로운 기술은 다음과 같습니다.
- 스트림 API
- 메서드에 코드를 전달하는 기법
- 인터페이스의 디폴트 메서드
데이터베이스 질의 언어에서 고수준 언어로 원하는 동작을 표현하면, 구현에서 최적의 저수준 실행 방법을 선택하는 방식으로 동작합니다. (자바에서는 스트림 라이브러리가 이 역할을 수행)
즉, 스트림을 이용하면 에러를 자주 일으키며 멀티코어 CPU
를 이용하는 것 보다 비용이 훨씬 비싼 synchronized
를 사용하지 않아도 됩니다.
1.2 왜 아직도 자바는 변화하는가?
1960년대 사람들은 완벽한 프로그래밍 언어를 찾고자 노력했습니다.
1966년에 이미 프로그래밍 언어가 700개에 이르렀으며, 이후로 수천개의 언어가 나왔고, 학계에서는 프로그래밍 언어가 마치 생태계와 닮았다는 결론을 내렸습니다.
즉, 새로운 언어가 등장하면서 진화하지 않은 기존 언어는 사장되었습니다.
우리는 완벽한 언어를 원했지만 현실적으로 그런 언어는 존재하지 않으며, 모든 언어에는 장단점이 존재합니다.
예를 들어, C
, C++
의 경우 안정성은 부족하지만 작은 런타임 풋프린트
덕분에 운영체제
와 임베디드 시스템
에서 여전히 인기를 끌고 있습니다.
런타임 풋프린트
에 여유가 있는 경우 Java
, C#
같이 안전한 형식의 언어가 압도적입니다.
💡 런타임 풋프린트 란 특정 하드웨어나 소프트웨어 단위가 차지하고 있는 공간의 크기를 말합니다.
단지 새로운 하나의 기능 때문에 기존 언어를 버리고 새로운 언어로 바꾼다는 것은 쉽지 않은 일입니다.
하지만 프로그래밍을 새롭게 배우는 사람이라면 당연히 새로운 언어를 배울 것이며, 기존 언어는 도태될 수 밖에 없습니다.
Java
는 1995년에 첫 베타 버전을 출시했지만, 경쟁 언어를 대신하며 커다란 생태계를 성공적으로 구축했습니다.
1.2.1 프로그래밍 언어 생태계에서 자바의 위치
Java
는 처음부터 많은 유용한 라이브러리를 포함하는 잘 설계된 객체지향 언어로 시작했습니다.
스레드
와락
을 이용한 소소한 동시성 지원 (하드웨어 중립적인 메모리 모델 때문에 멀티코어 프로세서에서 병렬적으로 수행되는 스레드는 싱글코어에서의 동작과 달리 예기치 못한 상황을 일으킬 수 있음)JVM
바이트 코드로 컴파일하는 특징- 모든 브라우저가 가상 머신 코드를 지원하기 때문에 인터넷 애플릿 프로그램의 주요 언어가 됨
Java
보다JVM
을 중요시 하는 애플리케이션에서는JVM
에서 실행되는스칼라
그루비
등이 사용 됨JDK7
에서invokedynamic
바이트 코드가 추가 됨으로써, 경쟁 언어는JVM
에서 더 부드럽게 실행되며,Java
와의 상호동작도 할 수 있게 됨
- 다양한 임베디드 컴퓨팅 분야를 성공적으로 장악 중 (스마트카드, 토스터, 셋톱박스, 자동차 브레이크 시스템 등)
하지만, 프로그래밍 언어 생태계에 변화가 생겼습니다. 빅데이터가 등장하면서 병렬 프로세싱을 활용해야 하는데, 지금까지의 Java
로는 충분히 대응할 수 없었습니다.
Java 8
에 추가된 기능은 Java
에 없던 완전히 새로운 개념이지만, 현재 시장에서 요구하는 기능을 효과적으로 제공합니다.
💡 자바는 어떻게 대중적인 프로그래밍 언어로 성장했는가?
객체지향은 1990년대에 두가지 이유로 각광받았습니다.
- 캡슐화 덕분에
C
에 비해 소프트웨어 엔지니어링적인 문제가 적음- 객체지향의 정신적인 모델 덕분에
윈도우 95
이후의 WIMP 프로그래밍 모델에 쉽게 대응초기 브라우저가 자바 모델과 자바 코드 애플릿을 안전하게 실행할 수 있었기 때문에 대학으로 깊숙이 자리 잡을 수 있었고, 졸업생들이 자바를 업계에서 사용하기 시작했습니다.
1.2.2 스트림 처리
스트림
은 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임입니다.
이론적으로 프로그램은 입력 스트림
에서 데이터를 한 개씩 읽어 들이며, 출력 스트림
으로 데이터를 한 개씩 기록합니다.
즉, 어떤 프로그램의 출력 스트림
은 다른 프로그램의 입력 스트림
이 될 수 있습니다.
예를 들어, Java
의 경우 System.in
으로 데이터를 읽어 System.out
으로 기록합니다. (유닉스
와 C
의 경우 stdin
, stdout
)
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
위 예제는 파일의 단어를 소문자로 바꾼 다음에 사전순으로 단어를 정렬 후, 가장 마지막에 위치한 세 단어를 출력하는 명령어입니다.
유닉스에서는 여러 명령 (cat
, tr
, sort
, tail
)을 병렬로 실행합니다.
그렇기 때문에 cat
이나 tr
이 오나료되지 않은 시점에 sort
가 행을 처리하기 시작할 수 있습니다.
그러면 순서가 보장되지 않으니까 문제 아닌가? 라고 생각할 수 있지만, 책에서는 자동차 생산라인을 예시로 들고있습니다.
자동차 생산 라인은 여러 자동차로 구성된 스트림을 처리하는데, 각각의 작업장에서는 자동차를 받아서 수리한 후 다음 작업장으로 넘겨줍니다.
조립 라인은 물리적인 순서로 한 개씩 운반하지만, 각각의 작업장은 동시에 작업을 처리하는 것과 같습니다.
기존의 Java
는 한 번에 한 항목을 처리했지만, 스트림 API
를 이용하면 데이터베이스 질의 처럼 고수준으로 추상화해서 일련의 스트림
으로 만들어 처리 할 수 있습니다.
또한, 스레드
라는 복잡한 작업을 하지 않아도 스트림 파이프라인
을 이용해서 입력 부분을 여러 CPU
코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있습니다.
1.2.3 동작 파라미터화로 메서드에 코드 전달하기
Java 8
에 추가된 두 번째 프로그램 개념은 코드 일부를 API
로 전달하는 기능입니다.
예를 들어, 특정 조건으로 정렬 방식을 지정하고 싶다면 기존에는 Comparator
객체를 만들어서 sort
에 넘겨줘야 했습니다.
이 방식은 너무 복잡하며 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않습니다.
Java 8
에서는 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공합니다.
💡 이러한 기능을
동작 파라미터화 (behavior parameterization)
라고 부릅니다.
1.2.4 병렬성과 공유 가변 데이터
세상에 공짜는 없다. 즉, 병렬성을 얻는 대신 무언가는 포기해야 합니다.
여기서는 병렬성을 얻는 대신 스트림 메서드
로 전달하는 코드의 동작 방식을 조금 바꿔야 합니다.
스트림 메서드
로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행 될 수 있어야 합니다.
그렇게 만들기 위해선, 공유된 가변 데이터
에 접근하지 않아야 합니다.
💡 이런 함수를
순수 함수 (pure)
,부작용 없는 함수 (side-effect-free)
,상태 없는 함수 (stateless)
라고 부릅니다.
기존처럼 synchronized
를 이용해서 공유된 가변 데이터를 보호하도록 만들 수 있지만, 다중 프로세싱 코어 에서 사용하면 병렬이라는 목적을 무력화 시키면서 생각보다 훨씬 더 비싼 대가를 치뤄야 할 수 있습니다. (다중 처리 코어에서는 코드가 순차적으로 실행되어야 함)
하지만, Java 8
의 스트림
을 이용하면 스레드 API
보다 쉽게 병렬성을 활용할 수 있습니다.
공유 되지 않은 가변 데이터 요구사항
은 인수를 결과로 반환하는 기능과 관련됩니다.
즉, 수학적인 함수처럼 함수가 정해진 기능만 수행하며 다른 부작용은 일으키지 않음을 의미합니다.
1.2.5 자바가 진화해야 하는 이유
지금까지 Java
는 진화해 왔습니다.
예를 들어, 제네릭 (Generic)
이 등장했고, Iterator
대신 for-each
루프를 사용할 수 있게 되었습니다.
처음에는 변한 문법에 당황스러웠을수 있지만, 이내 익숙해졌고 컴파일 단계에서 에러를 잡으며 많은 편리함과 이점을 얻을 수 있게 되었습니다.
또한, 객체지향
에서 벗어나 함수형 프로그래밍
으로 다가섰다는 것이 Java 8
의 가장 큰 변화입니다.
객체지향
과 함수형 프로그래밍
은 상극이지만, Java 8
에서 함수형 프로그래밍을 도입함으로써, 두 가지 프로그래밍 패러다임의 장점을 모두 활용할 수 있게 되었습니다.
즉, 어떤 문제를 더 효율적으로 해결할 다양한 도구를 얻게 된 셈입니다.
언어는 하드웨어나 프로그래머 기대의 변화에 부응하는 방향으로 변화해야 합니다.
일례로 지금은 비인기 언어인
코볼
은 상업적으로 엄청 중요한 언어였습니다.
Java
는 새로운 기능을 추가하면서 인기 언어의 자리를 유지하고 있습니다. 하지만, 새로운 기능이 추가되어도 사용하지 않으면 아무런 의미가 없습니다.
계속해서 새로운 기능에 관심을 가지고 사용을 함으로써, 우리 스스로도 자바 프로그래머로서의 삶을 유지할 수 있습니다.
1.3 자바 함수
Java 8
에서는 멀티코어에서 병렬 프로그래밍을 활용할 수 있는 스트림
과 연계될수 있도록 하기 위해서 함수를 새로운 값
의 형식으로 추가 했습니다.
기존 Java
에서 값
이라고 하면, int
, double
등과 같은 기본 타입이거나 참조 타입인 객체
가 있습니다.
이렇게 값
으로 사용될 수 있는 것들을 일급 시민
이라고 합니다.
그 외에 다양한 구조체 (메서드, 클래스 등)
은 값의 구조를 표현하는데 도움이 될 수는 있지만, 값으로 전달할 수는 없습니다.
이렇게 전달할 수 없는 구조체는 이급 시민
이라고 합니다.
💡
시민
이라는 용어는 1960년대 미국 시민 권리에서 유래되었습니다.
Java 8
설계자들은 이급 시민
을 일급 시민
으로 바꿀 수 있는 기능을 추가했으며, 이미 Javascript
같은 다양한 언어에서는 일급 시민
으로 가득찬 세계를 성공적으로 만들어 가고 있습니다.
1.3.1 메서드와 람다를 일급 시민으로
스칼라
와 그루비
같은 언어에서 메서드를 일급값으로 사용하면 프로그래밍이 수월해진다는 사실을 이미 실험을 통해 확인했습니다.
그렇기에 Java 8
설계자들은 프로그래머들이 더 쉽게 프로그램을 구현할 수 있는 환경이 제공되도록 설계하기로 결정 했고, Java 8
에서 메서드를 값으로 취급할 수 있는 기능은 스트림
같은 다른 Java 8
기능의 토대를 제공했습니다.
- 첫 번째로
메소드 참조
라는 기능이 있습니다.
예를 들어 디렉토리에서 모든 숨겨진 파일을 필터링 하는 로직을 작성한다고 하면 기존에는 아래와 같이 작성했습니다.
class Foo {
public static void main(String[] args) {
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
}
}
Java 8
에서 제공하는 메소드 참조
를 사용하면 아래와 같이 한 줄로 표현이 가능합니다.
class Foo {
public static void main(String[] args) {
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
}
}
기존 코드보다 훨씬 간결하며 코드를 해석하기도 쉽습니다.
💡
메소드 참조
에 사용되는::
는 '해당 메소드를 사용하라'라는 의미입니다.
💡 람다 : 익명 함수
Java 8
에서는 위 처럼 기명 메소드
도 값
으로 취급하지만, 람다 (익명 함수)
도 값으로 취급합니다.
예를 들어 값에 +1을 하는 함수를 기명 메소드
로 사용할 시, 아래와 같이 Utils::add
를 사용할 수 있습니다.
class Utils {
public int add(int num) {
return num + 1;
}
}
class Foo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream().mapToInt(Utils::add).toArray();
}
}
하지만, 해당 기능이 자주 쓰이는 게 아니라면, 매번 특정 클래스에 메소드
를 만드는 것이 번거로울 수 있습니다. 이때, 람다
를 사용하면 메소드
를 만들지 않고도 간단하게 사용할 수 있습니다.
class Foo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream().mapToInt(num -> num + 1).toArray();
}
}
1.3.2 코드 넘겨주기 : 예제
public static class Apple {
private int weight = 0;
private String color = "";
public Apple(int weight, String color) {
this.weight = weight;
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@SuppressWarnings("boxing")
@Override
public String toString() {
return String.format("Apple{color='%s', weight=%d}", color, weight);
}
}
위와 같은 Apply
클래스를 통해 생성 된 Applys
라는 리스트가 존재 할 때, Green
색깔만 필터
하는 기능을 만든다고 했을 때, 기존에는 아래와 같이 작성했습니다.
public class FilteringApples {
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 class FilteringApples {
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;
}
}
필터 조건
빼고 모든 코드가 동일 하며, 복사
, 붙여넣기
가 좋지 않다는건 이미 모두가 알고 있을 것입니다.
Java 8
에서는 Predicate
를 사용하면 필터 조건
만 함수로 받아 처리 할 수 있습니다.
public class FilteringApples {
public 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;
}
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
}
public class FilteringApples {
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(80, "green"),
new Apple(155, "green"),
new Apple(120, "red")
);
filterApples(inventory, FilteringApples::isGreenApple);
filterApples(inventory, FilteringApples::isHeavyApple);
}
}
Predicate
는 수학에서 인수로 값을 받아boolean
을 반환하는 함수를 지칭 합니다.
1.3.3 메서드 전달에서 람다로
위 처럼 기명 메소드
를 사용할 수도 있지만, 간단하게 람다
로 구현할 수도 있습니다.
public class FilteringApples {
public static void main(String[] args) {
filterApples(inventory, (apple) -> "green".equals(apple.getColor()));
filterApples(inventory, (apple) -> apple.getWeight() > 150);
}
}
여기서 스트림
의 filter
을 사용하면 filterApples
메소드 또한 구현할 필요가 없습니다.
public class FilteringApples {
public static void main(String[] args) {
inventory.stream().filter(apple -> "green".equals(apple.getColor())).collect(Collectors.toList())
}
}
이 처럼 Java 8
에 추가된 기능을 사용하면 이전보다 훨씬 간단하게 기능을 만들 수 있습니다.
1.4 스트림
Java
의 모든 애플리케이션은 Collcation
을 만들고 활용합니다. 하지만 Collcation
만으로 모든 문제가 해결되지는 않습니다.
예를 들어 고가의 거래만 필터링 한 다음 통화로 그룹화를 해야한다고 했을 떄, Collection
만을 이용하면 아래와 같이 구현할 수 있습니다.
class Foo {
public static void main(String[] args) {
Map<Currency, Transaction> transactionByCurrencies =
new HashMap<>(); // 그룹화된 트랜잭션을 더할 Map
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); // 현재 통화에 트랜잭션 추가
}
}
}
}
중첩된 제어문
이 많아서 위의 코드를 한 번에 이해하기는 어렵습니다. 반면, 스트림
을 사용하면 아래처럼 간단하게 사용할 수 있습니다.
class Foo {
public static void main(String[] args) {
transactions.stream()
.filter(t -> t.getPrice() > 1000)
.collect(groupingBy(Transaction::getCurrency));
}
}
기존 코드 처럼 for-each
를 이용해서 각 요소를 반복하면서 작업하는 것을 외부 반복
이라고 합니다.
반면, 스트림
은 라이브러리 내부에서 모든 데이터가 처리되기 때문에 루프를 신경 쓸 필요가 없습니다. 이와 같은 반복을 내부 반복
이라고 합니다.
데이터가 많으면 훨씬 더 오래걸리는 것은 당연한 이야기지만, 스트림
을 사용하면 이론적으로 모든 코어를 활용하여 병렬로 작업을 하기 때문에, 단일 모듈에 비해 코어 개수만큼 배로 빠릅니다.
1.4.1 멀티스레딩은 어렵다
이전 자바에서 제공하는 스레드
를 이용하여 멀티스레딩 코드를 구현해서 병렬성을 이용하는 것은 쉽지 않았습니다.스레드
를 잘 제어하지 못한 채 공유된 데이터에 접근하게 되면 예기치 못한 상황이 벌어질수 있습니다.
가령, A 스레드
에서 10을 더하고, B 스레드
에서 5를 더했을 때, 15가 더해진게 아니라 값이 덮어써져 5만 더해질수도 있다는 이야기 입니다.
Stream
은 2가지 문제를 모두 해결합니다.
Collection
을 처리하면서 발생하는 모호함과 반복적인 코드- 멀티코어 활용 어려움
Collection
에서는 데이터를 처리할 때 반복되는 패턴이 너무 많았습니다. 이러한 패턴을 '라이브러리에서 제공한다면 좋겠다' 라는 아이디어가 변화의 동기가 되었습니다.
주로 반복되는 패턴은 아래와 같습니다.
- 조건에 따라 데이터를
필터링
- 데이터를
추출
- 데이터를
그룹화
또한, 이러한 동작을 쉽게 병렬화할 수 있다는 점도 변화의 동기가 되었습니다.
예를들어 리스트를 필터링
한다고 했을 때, 알부분과 뒷부분은 각각 다른 CPU
에서 처리하도록 요청할 수 있습니다. (이 과정을 포킹 단계
라고 합니다.)
이후, 또 다른 CPU
에서 두개의 결과를 정리하는 식으로 병렬적으로 처리할 수 있습니다.
Collection
과Stream
이 비슷해 보일 수 있습니다. 하지만 중점을 어디에 두느냐에 따른 차이가 있습니다.
Collection
은 어떻게 데이터를 저장하고 접근할지에 중점을 둡니다.Stream
은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둡니다.
Stream
은Stream
내의 요소를 쉽게 병렬처리 할 수 있는 환경을 제공한다는 것이 핵심입니다.
Collection
을 가장 빠르게 필터링
할 수 있는 방법은 Collection
을 Stream
으로 바꾸고 병렬로 처리 후, 다시 Collection
으로 복원하는 것 입니다.
- 순차적으로 처리하는 방법
class Foo {
public static void main(String[] args) {
List<Apple> heavyApples = inventory.stream()
.filter(apple -> apple.getWeight() > 150)
.collect(toList());
}
}
- 병렬로 처리하는 방법
class Foo {
public static void main(String[] args) {
List<Apple> heavyApples = inventory.parallelStream()
.filter(a -> a.getWeight() > 150)
.collect(toList());
}
}
stream()
을 parallelStream()
로만 바꿔주면 병렬처리를 할 수 있습니다.
💡 자바의 병렬성과 공유되지 않은 가변 상태
흔히
Java
의 병렬성은 어렵고,synchronized
은 쉽게 에러를 일으킨다고 생각합니다.
Java 8
에서는 병렬 처리를 위해큰 스트림
을작은 스트림
으로 분할 합니다.
또한,filter
같은 라이브러리 메소드로 전달된 메소드가 상호작용을 하지 않는다면, 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있습니다.
함수형 프로그래밍
에서함수형
이란 함수를 일급값으로 사용한다는 의미도 있지만, 프로그램이 실행되는 동안 컴포넌트 간에 상호 작용이 일어나지 않는다라는 의미도 포함합니다.
1.5 디폴트 메서드와 자바 모듈
보통은 외부에서 만들어진 컴포넌트를 이용해 시스템을 개발하는데, 지금까지 Java
에서는 특별한 구조가 아닌 평범한 패키지 집합을 포함하는 Jar
파일을 제공하는게 전부였습니다.
이러한 패키지의 Interface
를 바꿔야하는 경우에는 해당 Interface
를 구현하는 모든 Class
의 구현을 바꿔야하므로 굉장히 고통스러웠습니다.
Java 9
에서는 모듈을 정의하는 문법을 제공하여Jar
같은 컴포넌트에 구조를 적용할 수 있게 되었습니다.Java 8
에서는Interface
를 쉽게 바꿀수 있도록default method
를 제공합니다.
class Foo {
public static void main(String[] args) {
inventory.stream().filter(a -> a.getWeight() > 150)
.collect(toList());
inventory.parallelStream().filter(a -> a.getWeight() > 150)
.collect(toList());
}
}
Java 8
이전에는 Collection<T>
가 stream
이나 parallelStream
메소드를 지원하지 않으므로 컴파일 에러가 발생합니다.
해당 기능을 추가하기 위해선 Collection Interface
에 해당 메소드를 추가하면 되는데, 그렇게 되면 Collection
을 구현하고 있는 모든 클래스가 해당 메소드를 구현해야 하므로 큰 고통이 따릅니다.
Java 8
에서는 구현 클래스에서 구현하지 않아도 되는 메소드를 Interface
에 추가할 수 있습니다. (그래서 default method
라고 부릅니다.)
예를 들어 Collections.sort()
기능을 Java 8
에서는 list.sort()
처럼 사용할 수 있습니다.
public interface List<E> extends Collection<E> {
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
}
이제 List<T>
를 구현하는 클래스들은 따로 구현하지 않아도 sort
메소드를 사용할 수 있습니다.
하나의 클래스는 여러개의
Interface
를 구현할 수 있으므로, 다중default method
가 존재한다는 것은 결국 다중 상속이 허용된다는 의미로 받아들일수도 있습니다.엄밀히 말해서 다중상속은 아니지만 어느정도는
그렇다
라고 할 수 있습니다. 그렇기 때문에다이아몬드 상속 문제
가 발생할 수 있습니다.
1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어
일반적인 함수형 언어 (SML, 오캐멀, 하스켈)
도 프로그램을 돕는 여러 장치를 제공합니다. 일례로 명시적으로 서술형의 데이터 형식을 이용해 null
을 회피하는 기법이 있습니다.
- 컴퓨터의 거장
토니 호아레
는 2009년 QCon London의 프레젠테이션에서 다음과 같은 말을 했습니다.
💡 1965년에 널 참조를 발명했던 일을 회상하며 '그 결정은 정말 뼈아픈 실수였다'고 반성하고 있다... 단지 구현이 편리하단 이유로 널 참조를 만들어야겠다는 유혹을 뿌리치지 못했다.
Java 8
에서는 NullPointerException
을 피할 수 있도록 Optional<T>
클래스를 제공합니다.
Optional<T>
는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체입니다. 값이 존재하지 않는 경우 어떻게 처리할지 명시적으로 구현하는 메소드를 포함하고 있기 때문에 어떤 변수에 값이 없을 때 어떻게 처리할지를 명시함으로써, NullPointerException
을 피할 수 있습니다.
또한, 패턴 매칭
기법도 있습니다. 패턴 매칭
은 수학에서 아래처럼 사용됩니다.
f(0) = 1
f(n) = n*f(n-1)
Java
에서는 if-then-else
나 switch
문을 이용하여 비교를 했습니다.
다른 언어에서 패턴 매칭
이 if-then-else
보다 더 정확한 비교를 할 수 있다는 사실을 증명했습니다. 하지만, Java 8
에서는 패턴 매칭
이 완벽하게 지원되지는 않습니다.
💡
패턴 매칭
은 현재 자바 개선안으로 제안된 상태입니다.
우선은 패턴 매칭
이 switch
를 확장한 것으로 데이터형식 분류와 분석을 한 번에 수행할 수 있다는 정도만 기억하고 넘어갑시다.
1.7 마치며
- 언어의 생태계의 모든 언어는 변화해서 살아남거나 그대로 머물면서 사라집니다.
코볼
의 사례를 보면Java
가 영원히 지배적인 위치를 유지할 수 있는 것은 아닐 수 있습니다. Java 8
은 프로그램을 더 효과적이고 간결하게 구현할 수 있는 새로운 개념과 기능을 제공합니다.- 기존의
Java 프로그래밍 기법
으로는 멀티코어 프로세서를 온전히 활용하기 어렵습니다. 함수
는일급값
입니다.Stream
과Collection
을 적절하게 활용하면Stream
의 인수를 병렬로 처리할 수 있으며 더 가독성 좋은 코드를 구현할 수 있습니다.- 기존
Java
기능으로는 대규모 컴포넌트 기반 프로그래밍 및 진화하는Interface
를 대응하기 어려웠습니다.Java 8
에서는default method
를 통해Interface
를 구현하는 클래스를 바꾸지 않고 기능을 확장할 수 있습니다.Java 9
에서는 모듈을 이용해 시스템의 구조를 만들 수 있게 되었습니다.
Java 8
에 추가된Optional<T>
으로null
을 처리하여NullPointerException
을 피할 수 있습니다. (+ 패턴매칭
)