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에서 두개의 결과를 정리하는 식으로 병렬적으로 처리할 수 있습니다.

CollectionStream이 비슷해 보일 수 있습니다. 하지만 중점을 어디에 두느냐에 따른 차이가 있습니다.

  • Collection은 어떻게 데이터를 저장하고 접근할지에 중점을 둡니다.
  • Stream은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둡니다.

StreamStream내의 요소를 쉽게 병렬처리 할 수 있는 환경을 제공한다는 것이 핵심입니다.

Collection을 가장 빠르게 필터링 할 수 있는 방법은 CollectionStream으로 바꾸고 병렬로 처리 후, 다시 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-elseswitch문을 이용하여 비교를 했습니다.
다른 언어에서 패턴 매칭if-then-else보다 더 정확한 비교를 할 수 있다는 사실을 증명했습니다. 하지만, Java 8에서는 패턴 매칭이 완벽하게 지원되지는 않습니다.

💡 패턴 매칭은 현재 자바 개선안으로 제안된 상태입니다.

우선은 패턴 매칭switch를 확장한 것으로 데이터형식 분류와 분석을 한 번에 수행할 수 있다는 정도만 기억하고 넘어갑시다.

1.7 마치며

  • 언어의 생태계의 모든 언어는 변화해서 살아남거나 그대로 머물면서 사라집니다. 코볼의 사례를 보면 Java가 영원히 지배적인 위치를 유지할 수 있는 것은 아닐 수 있습니다.
  • Java 8은 프로그램을 더 효과적이고 간결하게 구현할 수 있는 새로운 개념과 기능을 제공합니다.
  • 기존의 Java 프로그래밍 기법으로는 멀티코어 프로세서를 온전히 활용하기 어렵습니다.
  • 함수일급값입니다.
  • StreamCollection을 적절하게 활용하면 Stream의 인수를 병렬로 처리할 수 있으며 더 가독성 좋은 코드를 구현할 수 있습니다.
  • 기존 Java 기능으로는 대규모 컴포넌트 기반 프로그래밍 및 진화하는 Interface를 대응하기 어려웠습니다.
    • Java 8에서는 default method를 통해 Interface를 구현하는 클래스를 바꾸지 않고 기능을 확장할 수 있습니다.
    • Java 9에서는 모듈을 이용해 시스템의 구조를 만들 수 있게 되었습니다.
  • Java 8에 추가된 Optional<T>으로 null을 처리하여 NullPointerException을 피할 수 있습니다. (+ 패턴매칭)