- CHAPTER 1 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?
1.1 역사의 흐름은 무엇인가?
1.2 왜 아직도 자바는 변화하는가?
1.3 자바 함수
1.4 스트림
1.5 디폴트 메서드와 자바 모듈
1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어
1.7 마치며
2021/6/26 토요일 오전10시에 총원 5명이서 스터디를 시작했다
1.1 역사의 흐름은 무엇인가?
자바 역사를 통틀어 가장 큰 변화가 자바8에서 일어났다. 자바9, 10, 11 등에서도 크고 작은 변화가 일어났다. 이런 변화들 덕분에 프로그램을 더 쉽게 구현할 수 있게 되었다.
멀티코어 CPU 대중화와 같은 하드웨어적인 변화도 자바 8에 영향을 미쳤다.
지금까지의 대부분의 자바 프로그램은 코어 중 하나만을 사용했다(즉, 나머지 코어는 유휴 idle 상태로 두거나 운영체제나 바이러스 검사 프로그램과 프로세스 파워를 나눠서 사용했다)
자바8이 등장하기 이전에는 나머지 코어를 활용하려면 스레드를 사용하는 것이 좋다고 누군가 조언했을 것이다.
하지만 스레드를 사용하면 관리하기 어렵고 많은 문제가 발생할 수 있다는 단점이 있다.
자바는 이러한 병렬 실행 환경을 쉽게 관리하고 에러가 덜 발생하는 방향으로 진화하려 노력했다.
- 자바1.0: 스레드(Thread)와 락(lock), 심지어 메모리 모델까지 지원했다. 하지만 특별 전문가로 구성된 프로젝트팀이 아닌 한 이와 같은 저수준 기능을 온전히 활용하기 어려웠다.
- 자바 5: 스레드 풀(Thread Pool), 병렬 실행 컬렉션(Concurrent collection) 등 아주 강력한 도구를 도입했다.
- 자바 7: 병렬 실행에 도움을 줄 수 있는 포크/조인 프레임워크를 제공했지만 여전히 개발자가 활용하기는 쉽지 않았다.
자바 8에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공한다. 하지만 이 기법을 이용하려면 몇 가지 규칙을 지켜야 한다.
- 자바 9: 리액티브 프로그래밍이라는 병렬 실행 기법을 지원한다. 요즘 수요가 많은 고성능 병렬 시스템에서 특히 인기를 얻고 있는 RxJava(리액티브 스트림 툴킷)을 표준 방식으로 지원한다.
자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 한다.
스트림 API, 메서드에 코드를 전달하는 기법, 인터페이스의 디폴트 메서드.
스트림을 이용하면 에러를 자주 일으키며 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다
메서드에 코드를 전달(뿐만 아니라 결과를 반환하고 다른 자료구조로 전달할 수도 있음)하는 자바 8기법은 함수형 프로그래밍(Functional Programmin)에서 위력을 발휘한다.
1.2 왜 아직도 자바는 변화하는가?
1960년대에 사람들은 완벽한 프로그래밍 언어를 찾고자 노력했다. 우리는 시공을 초월하는 완벽한 언어를 원하지만 현실적으로 그런 언어는 존재하지 않으며 모든 언어가 장단점을 갖고 있다. 예를 들어 C,C++는 프로그래밍 안정성은 부족하지만 작은 런타임 풋프린트 덕분에 운영체제와 다양한 임베디드 시스템에서 여전히 인기를 끌고 있다. 하지만 C,C++의 낮은 안정성 때문에 프로그램이 예기치 않게 종료되거나 바이러스 등이 침투할 수 있는 보안 구멍이 있을 수 있다. 실제로 런타임 풋프린트에 여유가 있는 애플리케이션에서는 자바, C# 같이 안전한 형식의 언어가 C, C++를 압도한다.
특정 분야에서 장점을 가진 언어는 다른 경쟁 언어를 도태시킨다. 자바는 지난 1995년 첫 베타버전이 공개된 이후로 경쟁 언어를 대신하며 커다란 생태계를 성공적으로 구축했다.
자바8 설계의 밑바탕을 이루는 세 가지 프로그래밍 개념을 소개한다.
1. 스트림 처리(stream processing)
스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있다.
다음 예제처럼 유닉스 명령행에서는 파이프(|)를 이용해서 명령을 연결할 수 있다.
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
이 예제는 파일의 단어를 소문자로 바꾼 다음에 사전순으로 단어를 정렬했을 때 가장 마지막에 위치한 세 단어를 출력하는 프로그램이다.
유닉스에서는 여러 명령(cat, tr, sort, tail)을 병렬로 실행한다. 따라서 cat이나 tr이 완료되지 않은 시점에서 sort가 행을 처리하기 시작할 수 있다. [ 순서와 관계 없음 ]
이를 기계적인 예로 자동차 생산 공장 라인에 비유할 수 있다. 자동차 생산 공장은 여러 자동차로 구성된 스트림을 처리하는데, 각각의 작업장에서는 자동차를 받아서 수리한 다음에, 다음 작업장에서 다른 작업을 처리할 수 있도록 넘겨준다.
조립 라인은 자동차를 물리적인 순서로 한 개 씩 운반하지만 각각의 작업장에서는 동시에 작업을 처리한다.
스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만 이제 자바8에서는 우리가 하려는 작업을(DB 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다.
또한 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다. 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다는 것이다.
2. 동작 파라미터화로 메서드에 코드 전달하기(Behavior Parameterization)
자바8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다. 이게 무슨 말인지 전혀 감이 잡히지 않아 어리둥절할 것이다. 예를 들어 유닉스 예제에서 sort 명령에 파라미터를 추가하고 싶은 사용자도 있을 것이다. sort에 파라미터를 제공해서 역순 정렬 등 다양한 정렬을 수행할 수는 있지만 어쨌든 sort로 수행할 수 있는 동작은 미리 정해져 있다.
요약하면 하나의 stream내부에서 끝까지 데이터가 처리되기 전에 중간 공장에서 다른 파라미터를 가진 메서드로 데이터를 처리한 후(예시로 sort) 다음의 공장으로 보낼 수 있다는 말이다.
자바8 이전에서는 메서드를 다른 메서드로 전달할 방법이 없었다. 1장을 시작하면서 보여준 에제처럼 Compartor 객체를 만들어서 sort에 넘겨주는 방법도 있지만 이는 너무 복잡하며 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않다.
자바8에서는 메서드(우리 코드)를 다른 메서드의 인수로 넘겨주는 기능을 제공한다.
이러한 기능을 동작 파라미터화(behavior parameterization)라고 부른다.
동작 파라미터화가 왜 중요할까? compareUsingCustomerId를 이용해 sort의 동작을 파라미터화했던 것처럼 스트림API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초하기 때문이다.
3. 병렬성과 공유 가변 데이터
세 번째 프로그래밍의 개념은 "병렬성을 공짜로 얻을 수 있다"라는 말에서 시작된다. 세상에 공짜는 없다고 했는데 그럼 병렬성을 얻는 대신 무엇을 포기해야 할까? 스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다.
스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다.
보통 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만드려면 공유된 가변 데이터(shared mutable data)에 접근하지 않아야 한다.
이러한 함수를 순수(pure) 함수, 부작용 없는(side-effect-free) 함수, 상태 없는(stateless)함수라고 부른다.
지금까지는 독립적으로 실행될 수 있는 다중 코드 사본과 관련된 병렬성을 고려했다. 하지만 공유된 변수나 객체가 있으면 병렬성에 문제가 발생한다.
예를 들어 두 프로세스가 공유된 변수를 동시에 바꾸려하면 어떻게될까? 이 책 전체에서 이와 같은 유형의 문제를 어떻게 해결하는지 확인할 수 있다.
물론 기존처럼 synchronized를 이용해서 공유된 가변 데이터를 보호하는 규칙을 만들 수 있을 것이다(일반적으로 synchronized는 시스템 성능에 악영향을 미친다). 하지만 자바8 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 잇다. 다중 프로세싱 코어에서 synchronized를 사용하면(다중 처리 코어에서는 코드가 순차적으로 실행되어야 하므로 병렬이라는 목적을 무력화시키면서) 생각보다 훨씬 더 비싼 대가를 치러야 할 수 있다.
공유되지 않은 가변 데이터(no shared mutable data), 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 프로그래밍 패러다임의 핵심적인 사항이다.
반면 명령령 프로그래밍(imperative programming) 패러다임에서는 일련의 가변 상태로 프로그램을 정의한다. 공유되지 않은 가변 데이터 요구사항이란 인수를 결과로 반환하는 기능과 관련된다.
즉, 이 요구사항은 수학적인 함수처럼 함수가 정해진 기능만 수행하며(겉으로 보이는) 다른 부작용은 일으키지 않음을 의미한다.
1.3 자바 함수
프로그래밍 언어에서 함수(function)라는 용어는 메서드(method), 특히 정적 메서드(static method)와 같은 의미로 사용된다. 자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.
사실 자바는 완벽한 객체지향이라고 말할 수 없었다. 자바에서는 Primitive, Wrapper 형이 있었기 때문이다.
하지만 자바8에서는 함수를 새로운 값의 형식으로 추가했다.
자바 프로그램에서 조작할 수 있는 값을 생각해보자. 첫번째로 42(int), 3.14(double) 등의 기본값(Primitive)이 있다.
두번째로 객체(엄밀히 따지면 객체의 참조)도 값이다. new or 팩토리 메서드 or 라이브러리 함수를 이용해서 객체의 값을 얻을 수 있다.
객체 참조는 클래스의 인스턴스(Instance)를 가리킨다.
예를 들어 "abc"(String), new Integer(1111)(Integer), new HashMap<Integer, String>(100)(HashMap 생성자 호출) 등으로 객체 참조를 얻을 수 있다. 심지어 배열도 객체이다. 그런데 왜 함수가 필요할까?
프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 역사적으로 그리고 전통적으로 프로그래밍에서는 이 값(바꿀 수 있는 값)을 일급(first-class) 또는 시민(citizens)라고 부른다. 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값의 구조를 표현하는 데 도움이 될 수 있다.
하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다.
이렇게 전달할 수 없는 구조체는 이급 시민이다.
위에서 언급한 값은 모두 일급 자바 시민이지만 메서드, 클래스 등은 이급 자바 시민에 해당한다.
메서드와 클래스는 그 자체로 값이 될 수 없다는게 중요할까? 그렇다. 예를 들어 런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들면 프로그래밍에 유용하게 활용할 수 있다.
1. 메서드와 람다를 일급 시민으로
스칼라와 그루비 같은 언어에서 메서드를 일급값으로 사용하면 프로그래머가 활용할 수 있는 도구가 다양해지면서 프로그래밍이 수월해진다는 사실을 이미 실험을 통해 확인했다.
더불어 자바 8에서 메서드를 값으로 취급할 수 있는 기능은 스트림 같은 다른 자바 8기능의 토대를 제공했다.
첫 번째로 메서드 참조(method reference)라는 새로운 자바 8의 기능을 소개한다.
디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정하자. 우선 주어진 파일이 숨겨져 있는지 여부를 알려주는 메서드를 현해야 한다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
});
그런데 완성한 코드가 마음에 들지 않는다. 단 세 행의 코드지만 각 행이 무슨 작업을 하는지 투명하지 않다.
File 클래스에는 이미 isHidden이라는 메서드가 있는데 왜 굳이 FileFilter로 isHidden을 복잡하게 감싼 다음에 FileFilter를 인스턴스화해야 할까? 자바 8이 나타나기 전까지는 달리 방법이 없었기 때문이었다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
이제 자바8에서는 다음처럼 코드를 구현할 수 있다.
이미 isHidden이라는 함수는 준비되어 있으므로 자바8의 메서드 참조-method reference :: ('이 메서드를 값으로 사용하라'는 의미)
를 이용해서 listFiles에 직접 전달할 수 있다. 여기서 메서드가 아닌 함수라는 용어를 사용했다는 사실도 주목하자.
람다 : 익명 함수
자바 8에서는(기명 named) 메서드를 일급값으로 취급할 뿐 아니라 람다(또는 익명 함수 anonymous functions)를 포함하여 함수도 값으로 취급할 수 있다. 예를 들어 (Int x) -> x + 1, 즉 'x라는 인수로 호출하면 x+1을 반환'하는 동작을 수행하도록 코드를 구현할 수 있다.
람다 문법 형식으로 구현된 프로그램을 함수형 프로그래밍, 즉 '함수를 일급값으로 넘겨주는 프로그램을 구현한다'라고 한다.
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);
}
}
라는 Apple 클래스가 있다고 하자.
이때 모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현하려 한다.
이처럼 특정 항목을 선택해서 반환하는 동작을 필터(filter)라고 한다. 자바 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;
}
소프트웨어공학적인 면에서 복사&붙여넣기의 단점은 무엇인지 이미 알고있을 것이다. (예: 어떤 코드에 버그가 있다면 복사&붙여넣기 한 모든 코드를 고쳐야 한다)
이 예제에서는 if 구문만 다르다.
이전에도 언급했지만 다행히 자바8에서는 코드를 인수로 넘겨줄 수 있으므로 filter 메서드를 중복으로 구현할 필요가 없다.
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
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;
}
그리고 다음처럼 메서드를 호출할 수 있다.
filterApples(inventory, FilteringApples::isGreenApple);
filterApples(inventory, FilteringApples::isHeavyApple);
- What is Predicate? 프레디케이트란 무엇인가?
수학에서는 인수로 값을 받아 true나 false를 반환하는 함수를 프레디케이트라고 한다. 나중에 설명하겠지만 자바8에서도 Function<Apple, Boolean> 같이 코드를 구현할 수 있지만 Predicate<Apple>을 사용하는 것이 더 표준적인 방식이다(또한 boolean을 Boolean으로 변환하는 과정이 없으므로 더 효율적이기도 하다)
3. 메서드 전달에서 람다로
메서드를 값으로 전달하는 것은 분명 유용한 기능이다. 하지만 isHeavyApple, isGreenApple처럼 한두 번만 사용할 메서드를 매번 정의하는 것은 귀찮은 일이다. 자바 8에서는 이 문제도 간단히 해결할 수 있다.
자바 8에서는 다음처럼(익명함수 또는 람다라는) 새로운 개념을 이용해서 코드를 구현할 수 있다.
filterApples(inventory, (Apple a) -> "green".equals(a.getColor()));
filterApples(inventory, (Apple a) -> a.getWeight() > 150);
// 심지어 다음과 같이 구현도 가능
filterApples(inventory,
(Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()));
즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요가 없다. 위 코드는 우리가 넘겨주려는 코드를 애써 찾을 필요가 없을 정도로 더 짧고 간결하다.
1.4 스트림
거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다.
예를 들어 고가의 트랜잭션(거래)만 필터링한 다음에 통화로 결과를 그룹화해야 한다고 가정하자.
다음처럼 많은 기본 코드를 구현해야 한다.
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for (Transaction transaction : transactions) {
if (transaction.getPrice() > 1000) {
Currency currency = transaction.getCurrency(); // 트랜잭션의 통화 추출
List<Transaction> transactionsForCurrency() =
transactionsByCurrencies.get(currency);
if (transactionsForCurrency == null) { // 현재 통화의 그룹화된 맵에 항목이 없으면 새로 만듦
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currentcy, 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()); // 통화로 그룹화함
스트림API를 이용하면 컬렉션 API는 상당히 다른 방식으로 데이터를 처리할 수 있다는 사실만 기억하자.
컬렉션에서는 반복 과정을 직접 처리해야 했다. 즉 for-each 루프를 이용해서 각 요소를 반복하면서 작업을 수행했다.
이러한 방식의 반복을 외부 반복(external iteration)이라고 한다.
반면 스트림API를 이용하면 루프를 신경 쓸 필요가 없다. 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다.
이와 같은 반복을 내부 반복(internal iteration)이라고 한다.
1.멀티스레딩은 어렵다
이전 자바 버전에서 제공하는 스레드 API로 멀티스레딩 코드를 구현해서 병렬성을 이용하는 것은 쉽지 않다.
멀티스레딩 환경에서 각각의 스레드는 동시에 공유된 데이터에 접근하고, 데이터를 갱신할 수 있다. 결과적으로 스레드를 잘 제어하지 못하면 원치 않는 방식으로 데이터가 바뀔 수 있다.
멀티스레딩 모듈은 순차적인 모델보다 다루기가 어렵다.(공유된 자원 문제, synchronized의 버그, 활용성의 어려움 등등)
자바8은 스트림API(java.util.stream)로 '컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제' 그리고 '멀티코어 활용 어려움' 이라는 두 가지 문제를 모두 해결했다.
자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링(filtering)하거나, 데이터를 추출(extracting)하거나 데이터를 그룹화(grouping)하는 등의 기능이 있다.
또한 이러한 동작들을 쉽게 병렬화할 수 있다는 점도 변화의 동기가 되었다.
예를 들어 두 CPU를 가진 환경에서 리스트를 필터링 할때 한 CPU는 리스트의 앞부분을 처리하고, 다른 CPU는 리스트의 뒷부분을 처리하도록 요청할 수 있다. 이 과정을 포킹 단계(forking step)이라고 한다. 그리고 각각의 CPU는 자신이 맡은 절반의 리스트를 처리한다. 마지막으로 하나의 CPU가 두 결과를 정리한다.(구글 검색도 이와 같은 방식으로 작동하면서 빠르게 검색 결과를 제공한다.-물론 두 개 이상의 프로세서를 사용해서)
- 순차 처리 방식의 코드
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 디폴트 메서드와 자바 모듈
이전에 설명한 것처럼 요즘은 외부에서 만들어진 컴포넌트를 이용해 시스템을 구축하는 경향이 있다. 이와 관련해 지금까지 자바에서는 특별한 구조가 아닌 평범한 자바 패키지 집합을 포함하는 JAR 파일을 제공하는 것이 전부였다. 게다가 이러한 패키지의 인터페이스를 바꿔야 하는 상황에서는 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야 했으므로 여간 고통스러운 작업이 아니었다. 자바 8, 자바9는 이 문제를 다른 방법으로 해결한다.
우선 자바9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다.
모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인 작업이 용이해졌다.
또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.
자바 8은 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다.
메서드 본문(bodies)은 클래스 구현이 아니라 인터페이스의 일부로 포함되기 때문에 이를 디폴트 메서드(default method)라고 부른다.
디폴트 메서드를 이용하면 기존의 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있다.
자바 8에서는 인터페이스는 규격명세에 default라는 새로운 키워드를 지원한다.
예를 들어 자바8에서는 List에 직접 sort메서드를 호출할 수 있다. 이는 자바8의 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문이다(이 디폴트 메서드는 정적 메서드인 Collections.sort를 호출한다).
default void sort(Comparator<? super E> c) {
Collections.sort(this, c);
}
따라서 자바 8 이전에는 List를 구현하는 모든 클래스가 sort를 구현해야했지만 자바8부터는 디폴트sort를 구현하지 않아도 된다.
그런데 하나의 클래스에서 여러 인터페이스를 구현할 수 있지 않은가? 그러므로 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있다는 것은 결국 다중 상속이 허용된다는 의미일까?
엄밀히 말하면 다중상속은 아니지만 어느 정도는 '그렇다'고 말할 수 있다.
9장에서는 특히 C++에서 악명 높은 다이아몬드 상속 문제(diamond inheritance problems)를 피할 수 있는 방법을 설명한다.
1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어
지금까지 자바에 포함된 함수형 프로그래밍의 핵심적인 두 아이디어를 살펴봤다.
하나는 메서드와 람다를 일급값으로 사용하는 것이고, 다른 하나는 가변 공유 상태가 없는 병렬 실행을 이용해서 효율적이고 안전하게 함수나 메서드를 호출할 수 있다는 것이다.
일반적인 함수형 언어(SML, OCaml, Haskell)도 프로그램을 돕는 여러 가지 장치를 제공한다. 일례로 명시적으로 서술형의 데이터 형식을 이용해 null을 회피하는 기법이 있다.
컴퓨터 거장인 토니 호아레(Tony Hoare)는 2009년 QCon London의 프레젠테이션에서 다음과 같은 말을 했다.
1965년에 널 참조를 발명했던 일을 회상하며 '그 결정은 정말 뼈아픈 실수였다'고 반성하고 있다... 단지 구현이 편리하단 이유로 널 참조를 만들어야겠다는 유혹을 뿌리치지 못했다.
자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다.
Optional<T>는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체다.
또한 구조적(structural) 패턴 매칭 기법도 있다.(자바14가 되어서 preview가 발표되었다 - 자바8 시점에선 제안만 됐던 상태)
스칼라 프로그래밍 언어에는 패턴 매칭 기법이 있다.
스칼라의 expr match는 자바의 switch(expr)과 같은 기능을 수행한다.
패턴 매칭이 switch를 확장한 것으로 데이터 형식 분류와 분석을 한 번에 수행할 수 있다는 정도로만 생각하자.
왜 자바의 switch문에는 문자열과 기본값만 이용할 수 있는 걸까?
함수형 언어는 보통 패턴 매칭을 포함한 다양한 데이터 형식을 switch에 사용할 수 있다(스칼라에서는 match를 활용).
일반적으로 객체지향 설계에서 클래스 패밀리를 방문할 때 방문자 패턴(visitor pattern)을 이용해서 각 객체를 방문한 다음에 원하는 작업을 수행한다. 패턴 매칭을 이용하면 "Brakes 클래스는 Car클래스를 구성하는 클래스 중 하나입니다. Brakes를 어떻게 처리해야 할지 설정하지 않았습니다"와 같은 에러를 검출할 수 있다.
1.7 마치며
- 언어 생태계의 모든 언어는 변화해서 살아남거나 그대로 머물면서 사라지게 된다. 지금은 자바의 위치가 견고하지만 코볼과 같은 언어의 선례를 떠올리면 자바가 영원히 지배적인 위치를 유지할 수 있는 것은 아닐 수 있다.
- 자바 8은 프로그램을 더 효과적이고 간결하게 구현할 수 있는 새로운 개념과 기능을 제공한다.
- 기존의 자바 프로그래밍 기법으로는 멀티코어 프로세서를 온전히 활용하지 어렵다.
- 함수는 일급값이다. 메서드를 어떻게 함수형 값으로 넘겨주는지, 익명 함수(람다)를 어떻게 구현하는지 기억하자.
- 자바 8의 스트림 개념 중 일부는 컬렉션에서 가져온 것이다. 스트림과 컬렉션을 적절하게 활용하면 스트림의 인수를 병렬로 처리할 수 있으며 더 가독성이 좋은 코드를 구현할 수 있다.
- 기존 자바 기능으로는 대규모 컴포넌트 기반 프로그래밍, 그리고 진화하는 시스템의 인터페이스를 적절하게 대응하기 어려웠다. 자바 9에서는 모듈을 이용해 시스템의 구조를 만들 수 있고 디폴트 메서드를 이용해 기존 인터페이스를 구현하는 클래스를 바꾸지 않고도 인터페이스를 변경할 수 있다.
- 함수형 프로그래밍에서 Null 처리 방법과 패턴 매칭 활용 등 흥미로운 기법을 발견했다.
'Study > 모던 자바 인 액션' 카테고리의 다른 글
모던 자바 인 액션스터디 (1) - 자바의 역사 및 특징 (0) | 2021.06.27 |
---|---|
모던 자바 인 액션스터디 (0) - 시작 & Why? & 목차 (1) | 2021.06.27 |
댓글