Skip to content

Latest commit

 

History

History

Chapter_01

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Chapter1 자바 8,9,10,11 : 무슨 일이 일어나고 있는가?

1.1 역사의 흐름은 무엇인가?

자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다.

자바 8 이전의 자바 프로그램은 CPU 코어 중 하나만 이용했고, 나머지 코어를 이용하려면 멀티 스레드를 사용하여야 했다.

자바 8에서는 코어 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공한다.

자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용 이라는 두가지 요구사항을 기반으로 한다.


자바에서 제공하는 새로운 기술

  • 스트림 API
    • 병렬 연산을 지원
    • 멀티스레드를 통한 에러나 syncronized 키워드의 사용을 피할 수 있다.
    • 다른 관점에서 보면 결국 밑에 두 기술도 스트림 API 덕분에 존재함을 알 수 있다.
  • 메서드에 코드를 전달하는 기법
    • 간결한 방식으로 동작 파라미터화를 구현할 수 있다.
    • 약간만 다른 두 메서드를 복사 및 붙여넣기를 통해 프로그램하는 것에 비해 훨씬 간결해진다.
    • 함수형 프로그래밍에서 위력을 발휘한다.
  • 인터페이스의 디폴트 메서드

1.2 왜 자바는 변화하는가?

프로그래밍 언어는 생태계와 닮아서 새로운 언어가 등장하고, 진화하지 않은 기존의 언어는 사장된다.


1.2.1 프로그래밍 언어 생태계에서 자바의 위치

프로그래밍 언어 생태계에 변화의 바람이 불었다.

프로그래머는 빅데이터라는 도전에 직면하면서 멀티코어 컴퓨터나 컴퓨팅 클러스터를 이용해서 빅 데이터를 효과적으로 처리할 필요성이 커졌다.

또한 병렬 프로세싱을 활용해야 하는데 이전의 자바로는 충분히 대응할 수 없었다.

그렇기에 자바는 변화하였다.

자바 8은 더 다양한 프로그래밍 도구 그리고 다양한 프로그래밍 문제를 더 빠르고 정확하며 쉽게 유지 보수할 수 있다는 장점을 제공한다.

자바 8에 추가된 기능은 자바에 없던 완전히 새로운 개념이지만 현 시장에 요구하는 기능을 효과적으로 제공한다.


1.2.2 스트림 처리

💡 스트림이란 한번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다.

Stream<T> 는 T 형식으로 구성된 일련의 항목을 의미한다.

스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만(for문) 자바 8에서는 우리가 하려는 작업을 (데이터베이스 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다.

또한 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다.


1.2.3 동작 파라미터화로 메서드에 코드 전달하기

자바 8에서는 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공한다.

이를 이론적으로 동작 파라미터화라고 부른다.


1.2.4 병렬성과 가변 데이터

스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다.

다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터에 접근하지 않아야 한다.

syncronized를 이용해서 공유된 가변 데이터를 보호할 수 있으나 이는 비싼 대가를 치러야할 수 있다.

하지만 자바 8 스트림을 이용하면 기존의 자바 스레드 API 보다 쉽게 병렬성을 활용할 수 있다.


공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드로 전달하는 기능은 함수형 프로그래밍 패러다임의 핵심적인 사항이다. (18,19 장에서 자세히 설명한다.)

반면 명령형 프로그래밍(객체지향, 절차지향) 패러다임에서는 일련의 가변 상태로 프로그램을 정의한다.


1.2.5 자바가 진화해야하는 이유

언어는 하드웨어나 프로그래머 기대의 변화에 부응하는 방향으로 변화해야 한다.

자바 8에서는 함수형 프로그래밍을 도입함으로써 객체지향과 함수형 프로그래밍의 두 가지 패러다임의 장점을 모두 활용할 수 있게 되었다.


1.3 자바 함수

자바 8에서 함수 사용법은 일반적인 프로그래밍 언어의 함수 사용법과 아주 비슷하다. 프로그래밍 언어의 핵심은 값을 바꾸는 것이다.

전통적으로 프로그래밍 언어에서는 이 값을 일급(first-class) 값(또는 시민)이라고 부른다.


이전까지 자바 프로그래밍 언어에서는 기본값, 인스턴스만이 일급 시민이었다.

메서드와 클래스는 이 당시 일급 시민이 아니었는데, 런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들면 프로그래밍에 유용하게 활용될 수 있다.

자바 8 설계자들은 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.


1.3.1 메서드와 람다를 일급 시민으로

동작의 전달을 위해 익명 클래스를 만들고 메서드를 구현해서 넘길 필요 없이, 준비된 함수를 메서드 참조 ::(’이 메서드를 값으로 사용하라’는 의미)를 이용해서 전달할 수 있다.

아래 예제를 통해 자바 8에서는 더 이상 메서드가 이급값이 아닌 일급값인 것을 확인할 수 있다.


  • 익명 클래스를 통한 파일 리스팅
// FileFilter 객체로 isHidden 메서드를 감싼 다음에 File.listFiles 메서드로 전달해야한다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});
  • 메서드 참조를 이용한 파일 리스팅
// File 내부에 메서드를 참조하여 전달한다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);

자바 8에서는 위와 같이 기명의 메서드를 일급값으로 취급할 뿐 아니라 람다(또는 익명 함수)를 포함하여 함수도 값으로 취급할 수 있다.

예를 들어, (int x) → x + 1, 즉 x라는 인수로 호출하면 x+1을 반환하는 동작을 수행하도록 코드를 구현할 수 있다.

람다 문법 형식으로 구현된 프로그램을 함수형 프로그래밍, 즉 ‘함수를 일급 값으로 넘겨주는 프로그램을 구현한다’라고 한다.


1.3.2 코드 넘겨주기 : 예제

Apple 클래스와 getColor 메서드가 있고, Apples 리스트를 포함하는 변수 inventory가 있다고 하자.

모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현한다고 해보자.

또 비슷하게 150 그램 이상의 사과를 구한다고 해보자.

이처럼 특정 항목을 선택해서 반환하는 동작을 필터라고 한다.


자바 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;
}

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 이후의 코드

public static boolean isGreenApple(Apple apple) {
    return "green".equals(apple.getColor());
}

public static boolean isHeavyApple(Apple apple) {
    return apple.getWeight() > 150;
}

// 메서드가 predicate 파라미터로 전달됨
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
    List<Apple> result = new ArrayList<>();
    for (Apple apple : inventory) {
				// 사과는 p가 제시하는 조건에 맞는가?
        if (p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);

💡 프레디케이트(predicate)란?

예제에서 메서드를 넘겨받는 인자로 사용되었다.

수학에서는 인수로 값을 받아 true나 false를 반환하는 함수를 프레디케이트라고 한다. 자바 8에서는 Function<T, Boolean>으로 구현할 수 있지만, Predicate를 사용하는 것이 더 일반적이다.(또한, boolean을 Boolean으로 변환하지 않아도 되어서 더 효율적이다.

1.3.3 메서드 전달에서 람다로

위에서 처럼 한두 번만 사용할 메서드를 매번 클래스에 정의하는 것은 귀찮은 일이다.

이를 람다 함수를 사용하여 해결할 수 있다.

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()));

즉, 한번만 사용할 메서드는 따로 정의를 구현할 필요가 없다.

그러나 람다가 몇 줄 이상으로 길어진다면 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직하다.


아마도 자바는 filter 그리고 다음과 같은 몇몇 일반적인 라이브러리 메서드를 추가하는 방향으로 발전했을 수도 있었다.

static <T> Collection<T> filter(Collection<T> c, Predicate<p> p);

filter(inventory, (Apple a) -> a.getWeight() > 150);

그러나 병렬성이라는 중요성 때문에 설계자들은 이와 같은 설계를 포기했다.

대신 자바 8에서 filter와 비슷한 동작을 수행하는 연산집합을 포함하는 새로운 스트림 API를 제공한다.


1.4 스트림

스트림 API 이전 컬렉션 API를 이용하여 다양한 로직을 처리하였을 것이다.

스트림 API를 이용하면 컬렉션 API와는 상당히 다른 방식으로 데이터를 처리할 수 있다.

컬렉션 API를 사용한 for-each 루프를 이용하여 각 요소를 반복하면서 작업을 수행했다.

이러한 방식의 반복을 외부 반복(external iteration)이라 한다.

반면 스트림 API를 이용하면 루프를 신경 쓸 필요가 없다. 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다.

이와 같은 반복을 내부 반복(internal iteration)이라고 한다.


스트림의 기능과 장점

스트림은 자주 반복되는 패턴을 제공하는데, 주어진 조건에 따라 데이터를 필터링(filtering), 추출(extracting), 그룹화(grouping) 등의 기능이 있다.

또, 컬렉션을 이용했을 때 많은 요소를 가진 목록을 반복한다면 오랜시간이 걸리지만, 스트림은 멀티코어를 활용하여 더 높은 효율을 낼 수 있다.

기존에는 멀티 스레드를 사용하여야 병렬성을 얻을 수 있었지만, 스트림은 내부적으로 리스트를 나눠 n개의 CPU로 fork한 다음 처리 후 결과를 join하는 방식을 통해 병렬성을 제공한다.

//순차 처리 방식의 stream
inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(Collectors.toList());
//병렬 처리 방식의 stream
inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(Collectors.toList(

자바의 병렬성과 공유되지 않은 가변 상태

자바의 병렬성은 흔히 말하듯 어렵고 쉽게 에러를 일으킨다.

자바 8에서는 큰 스트림을 병렬로 처리할 수 있도록 라이브러리에서 작은 스트림으로 분할한다.

또한, 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있다.

함수형 프로그래밍에서 함수형이란 ‘함수를 일급값으로 사용한다’라는 의미도 있지만 부가적으로 ‘프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다’라는 의미도 포함한다.


1.5 디폴트 메서드와 자바 모듈

기존 자바 기능으로는 대규모 컴포넌트 기반 프로그래밍 그리고 진화하는 시스템의 인터페이스를 적절히 대응하기 어려웠다.

자바 8에서 지원하는 디폴트 메서드를 이용해 기존 인터페이스를 구현하는 클래스를 바꾸지 않고도 인터페이스를 변경할 수 있다.


예를 들어 List/나 Collection/ 인터페이스는 이전에 stream이나 parallelStream 메서드를 지원하지 않았다.

이를 지원하도록 바꾸려면 인터페이스에 stream 메서드를 정의하고, 모든 구현 클래스에 추가로 메서드를 구현하여야했다.

하지만 자바 8에서 Collection 인터페이스에 stream메서드를 추가하고 이를 디폴트 메서드로 제공하여 기존 인터페이스를 쉽게 변경할 수 있었다.


다중 상속?

하나의 클래스에서 여러 인터페이스를 구현할 수 있으므로, 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있다는 것은 다중 상속이 허용된다는 것일까?

엄밀히 다중 상속은 아니지만 어느 정도는 그렇다. 이에 관해 C++에서 악명 높은 다이아몬드 상속 문제를 피할 수 있는 방법을 설명한다.


1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어

자바에 포함된 함수형 프로그래밍의 핵심적인 두 아이디어

  • 메서드와 람다를 일급값으로 사용
  • 가변 공유 상태가 없는 병렬 실행을 이용해서 효율적이고 안전하게 함수나 메서드를 호출

일반적인 함수형 언어에서 프로그램을 돕는 여러 장치

  1. NPE 회피 기법을 제공한다.

자바 8에서는 NPE(NullPointerException)을 피할 수 있도록 도와주는 Optional 클래스를 제공한다.

Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체로, 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.

즉, NPE를 피할 수 있다.


  1. 패턴 매칭 기법을 제공한다.

패턴 매칭은 switch를 확장한 것으로 데이터 형식 분류와 분석을 한번에 수행할 수 있다.

자바 8은 패턴 매칭을 완벽히 제공하지 않는다. (자바 개선안으로 제안된 상태다.)