1장 - 자바 8, 9, 10, 11: 무슨 일이 일어나고 있는가.

Posted by yunki kim on February 12, 2022

스트림 처리

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

  자바 8에는 java.util.stream 패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream<T>는 T 형식으로 구성된 일련의 항목을 의미한다. 스트림 API는 파이프라인을 만드는 데 필요한 메서드들을 제공한다.

  스트림 API는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다. 또 한 스트림 파이프라인을 이용해 입력 부분을 여려 CPU 코어에 쉽게 할당할 수 있다. 스레드라는 복잡한 작업을 사용하지 않고도 병렬성을 얻을 수 있다.

  스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초한다.

동작 파라미터화(behavior parameterization)로 메서드에 코드 전달하기

  자바 8에서는 메서드를 다른 메서드의 인수로 넘기는 기능을 제공한다. 이런 기능을 동작 파라미터화라고 한다.

병렬성과 공유 가변 데이터(shared mutable data)

  스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행해도 안전하게 실행되야 한다. 안전하다는 의미는 공유된 가변 데이터(shared mutable data)에 접근하지 않는다는 의미고 이런 함수를 순수 함수(pure function)라 한다.

  공유되지 않은 가변 데이터(no shared mutable data), 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 프로그래밍 패러다임의 핵심 사항이다. 여기서 공유되지 않은 가변 데이터 요구사항은 인수를 결과로 변환하는 기능과 관련된다. 즉, 요구사항은 수학적인 함수처럼 함수가 정해진 기능만 수행하며 다른 부작용이 없음을 의미한다.

자바가 진화해야 하는 이유

  자바 8에서의 가장 큰 변화는 함수형 프로그래밍으로 다가섰다는 것이다. 함수형 프로그래밍에서는 하려는 작업이 최우선시 되며 그 작업 어떻게 수행하는지는 별개의 문제로 취급한다.

  언어는 하드웨어나 프로그래머 기대에 부흥하는 방식으로 발전한다. 자바가 인기있는 이유는 새로운 기능을 추가하며 진화하기 때문이다.

자바 함수

  프로그래밍 언어에서 함수라는 용어는 메서드 특히 정적 메서드와 같은 의미로 사용된다. 자바의 함수는 여기서 더 나아가 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.

  기존 자바에서는 primitive type과 객체의 참조(ex. new HashMap<Interget, String>(100))를 조작할 수 있는 값으로 두었다.

  프로그래밍 언어의 핵심을 값을 바꾸는 것이다. 이런 값을 일급시민(first-class citizens)라 한다. 실행하는 동안 전달할 수 없는 구조체(메서드, 클래스 등)는 이급 시민이다. 만약 런타임에서 메서들르 전달할 수 있다면 프로그래밍에 유용하게 활용할 수 있고 자바 8에서 이 기능이 추가되었다.

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

  자바 8은 메서드를 값으로 취급할 수 있게 해서 프로그래머들이 더 쉽게 프로그램을 구현할 수 있는 환경을 제공한다. 더불어 자바 8에서 메서드를 값으로 취급할 수 있는 기능은 스트림 같은 다른 자바 8기능을 토대로 제공한다.

메서드 참조(mehotd reference)

  디렉토리에 있는 모든 숨김 파일을 골라내는 코드를 작성한다 해보자. File 클래스는 isHidden 메서드를 제공한다.

1
2
3
4
5
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden(); // 숨겨진 파일 필터링
    }
}
cs

  위 코드는 File 클래스에 이미 isHidden 메서드가 있음에도 굳이 FileFilter로 isHidden을 복잡하게 감싸고 FileFilter를 인스턴스화 한다. 만약 이미 isHidden이라는 함수가 있으므로 메서드를 참조해 전달하면 코드가 아주 깔끔해 질것이다. 자바 8에서는 이런 메서드 참조 기능을 제공한다.

1
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
cs

  이를 통해 자바 8은 기존에 비해 문제 자체를 더 직접적으로 설명한다는 것을 알 수 있다. 또 한 메서드를 더 이상 이급 시민이 아닌 일급 시민으로 사용하고 있다.

기존 방식 VS 메서드 참조

람다

  자바 8에서는 메서드를 일급 시민으로 취급할 뿐 아니라 람다를 포함해 함수도 값으로 취급할 수 있다. 람다 문법 현식으로 구현된 프로그램을 함수형 프로그래밍, 즉 "함수를 일급 값으로 넘겨주는 프로그램을 구현한다"라고 한다.

메서드 전달에서 람다로

  메서드를 값으로 전달하는 것은 유용하다. 하지만 한 번만 사용하는 메서드를 정의하기는 귀찮다. 이때 사용할 수 있는 것이 람다이다.

1
filterApples(inventory, (Apple a) -> Green.equals(a.getColor());
cs

  하지만 람다가 몇 줄이 넘어간다면 코드가 수행하는 일을 더 잘 설명하기 위해 이름을 가진 메서드를 정의하고 메서드 참조를 활용해야 한다.

스트림

  기존 자바 컬랙션 만을 활용한 코드는 길고 이해하기 어려웠다. 이 문제는 스트림을 활용해 해결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for (Transaction transaction : tranractions) {
    if (transaction.getPrice() > 100) {
        Currency currency = transaction.getCurrency();
        List<Transaction> transactionsForCurrency = 
            transactionsByCurrencies.get(currency);
        if (transactionsForCurrency == null) {
            transactionsForCurrency = new ArrayList<>();
            transactionsByCurrencies.put(currency, transactionsForCurrency);
        }
        transactionsForCurrency.add(transaction);
    }
}
 
// 위 코드와 같은 기능을 한다
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
    .filter((Transaction t) -> t.getPrice() > 100)
    .collect(groupingBy(Transaction::getCurrency));
 
cs

  컬랙션에서는 반복과정을 집접 처리해야 했다. for-each 루프를 활용해 각 요소를 반복해 작업을 수행하는 방식을 외부 반복(external iteration)이라 한다. 스트림 API는 라이브러리 내부에서 모든 데이터가 처리된다. 이런 반복을 내부 반복(internal iteration)이라 한다.

멀티스레딩은 어렵다

  멀티스레딩 코드를 구현해 병렬성을 이용하는 것은 racing condition의 위험 때문에 까다롭다. 자바 8은 스트림 API로 컬랙션을 처리하면서 발생하는 모호함과 반복적인 코드 문제, 그리고 멀티코어 활용 어려움을 해결했다.

  스트림의 핵심은 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공하는 것이다. 다음은 스트림을 활용한 순차 처리 방식과 병렬 처리 방식의 예시다.

1
2
3
4
5
6
7
8
9
// 순차 처리
List<Apple> heavyApples = inventory.stream()
    .filter((Apple a) -> a.getWeight() > 150)
    .collect(toList());
 
// 병렬 처리
List<Apple> heavyApples = inventory.parallelStream()
    .filter((Apple a) -> a.getWeight() > 150)
    .collect(toList());
cs

  위 코드에서 병렬 처리 부분을 두 개의 CPU가 처리하는 과정을 그림으로 표현하면 다음과 같은 과정을 거친다. 여기서 포킹 단계(forking step)란 CPU들이 리스트를 여러 부분으로 나누어 각자의 부분만을 처리하는 것을 의미한다.

디폴트 메서드와 자바 모듈

  이전까지는 외부에서 만들어진 컴퓨넌트를 이용해 시스템을 구축할 경우 특별한 구조가 아닌 평범한 자바 패키지 집합을 포함하는 JAR파일을 제공하는 것이 최선이였다. 또 한 인터페이스를 바꿔야 하는 상황에서는 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야 했다.

  자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 따라서 JAR 같은 컴포넌트에 구조를 적용할 수 있어 문서화와 모듈 확인 작업이 용이해진다.

  자바 8에서는 인터페이스를 쉽게 바꿀 수 있는 디폴트 메서드를 지원한다. 따라서 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있고 기존 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있다.

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

  자바에 포함된 함수형 프로그래밍의 핵심적인 아이디어는 다음과 같다.

    1. 메서드와 람다를 일급 시민으로 사용한다.

    2. 가변 공유 상태가 없는 병렬 실행을 이용해 효율적이고 안전하게 함수나 메서드를 호출할 수 있다.

  일반적인 함수형 언어(하스켈 등)는 null을 회피하는 기법이 있다. 자바 8은 NullPointer 예외를 피하기 위해 Optional<T> 클래스를 제공한다. 이는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.

  패턴 매칭 기법 역시 유용하다. 패턴 매칭은 if-then-else가 아닌 케이스로 정의하는 수학과 함수형 프로그래밍의 기능이다. 정규 표현식이 그 예시다. 

 

출처 - 모던 자바 인 액션