[자바 인 액션] 01 - 자바 8,9,10,11 : 무슨 일이 일어나고 있는가?

8 minute read

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

자바의 병렬 실행 환경 변화

  • 자바 1.0 - 스레드와 락(lock), 메모리 모델 지원

  • 자바 5 - 스레드 풀(thread pool), 병렬 실행 컬렉션(concurrent collection)

  • 자바 7 - 포크/조인 프레임워크 제공

  • 자바 8 - 단순한 방식으로 접근할 수 있는 방법 제공

    • 스트림 API
    • 메서드에 코드를 전달하는 기법(메서드 참조와 람다)
    • 인터페이스의 디폴트 메서드
  • 자바 9 - 리액티브 프로그래밍(병렬 실행 기법) 지원

    • 이 기법을 사용할 수 있는 상황은 한정되어 있지만 요즘 수요가 많은 고성능 병렬 시스템에서 특히 인기를 얻고 있는 RxJava(리액티브 스트림 툴킷)를 표준적인 방식으로 지원한다.

메서드에 코드를 전달하는 기법을 이용하면 새롭고 간결한 방식으로 동작 파라미터화(behavior parameterization)을 구현할 수 있다.

약간만 다른 두 메서드를 인수를 이용해서 다른 동작을 하도록 하나의 메서드를 합칠 때, 자바 8 이전에는 익명 클래스를 이용해서 동작 파라미터화를 구현할 수 있다. 자바 8에서는 간단 명료하게 구현할 수 있다.

메서드에 코드를 전달하는 자바 8 기법은 함수형 프로그래밍에서 위력을 발휘한다.

1.2 왜 아직도 자바는 변화하는가?

우리는 시공을 초월하는 완벽한 언어를 원하짐나 현실적으로 그런 언어는 존재하지 않으며 모든 언어가 장단점을 갖고 있다.

  • C, C++
    • 장점 : 작은 런타임 풋프린트1 → 운영체제와 다양한 임베디드 시스템에서 인기
    • 단점 : 낮은 안전성
  • Java, C#
    • 런타임 풋프린트가 여유가 있는 애플리케이션에서 더 안전한 Java, C#이 인기

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

  • 자바는 처음부터 스레드와 락을 이용한 소소한 동시성을 지원했다.

    (자바의 하드웨어 중립적인 메모리 모델 때문에 멀티코어 프로세서에서 병렬적으로 수행되는 스레드는 싱글코어에서의 동작과 달리 예기치 못한 상황을 일으킬 수 있다.)

  • 코드를 JVM 바이트 코드로 컴파일하는 특징 때문에 자바는 인터넷 애플릿 프로그램의 주요 언어가 되었다.

    (모든 브라우저에서 가상 머신 코드를 지원하기 때문에)

빅테이터(테라바이트 이상의 데이터셋)라는 새로운 도전 직면

⇒ 멀티코어 컴퓨터나 컴퓨터 클러스터를 이용해서 빅데이터를 효과적으로 처리할 필요성이 커졌다.

⇒ 자바 8의 등장

​ “현재 시장에서 요구하는 기능을 효과적으로 제공한다.”

  • 더 다양한 프로그램이 도구 제공

  • 다양한 프로그래밍 문제를 더 빠르고 정확하며 쉽게 유지보수할 수 있다.

자바 8 설계의 밑바탕을 이루는 세가지 프로그래밍 개념

  1. 스트림 처리
  2. 코드 일부를 API로 전달하는 기능

1.2.2 스트림 처리

자바 8에 추가된 첫 번째 프로그래밍 개념은 스트림 처리다.

  • 스트림이란?

    한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임

스트림과 관련된 유닉스 명령

# 파일의 단어를 소문자로 바꾼 다음에 사전순으로 단어를 정렬했을 때 가장 마지막에 위치한 세 단어 출력
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

image

[그림 1 - 2]

[그림 1 - 2]에서 보여주듯이 sort는 여러 행의 스트림을 입력으로 받아 여러 행의 스트림을 출력으로 만들어낸다.

유닉스에서는 여러 명령(cat, tr, sort, tail)을 병렬로 실행한다. 따라서 cat이나 tr이 완료되지 않은 시점에서 sort가 행을 처리하기 시작할 수 있다. (동시 처리)

자바 8에는 java.util.stream 패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream는 T 형식으로 구성된 일련의 항목을 의미한다.

  • 스트림 API 란 ? 어떤 항목을 연속으로 제공하는 어떤 기능

스트림 API의 핵심

  • 작업을 (데이터베이스 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다. (기존에는 한 번에 한 항목을 처리했다.)
  • 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다.
  • 스레드라는 복잡한 작업을 사용하지 않으면서 공짜로 병렬성을 얻을 수 있다.

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

자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.

정렬(sort) 명령에 특정 순서대로 정렬하도록 파라미터를 주고 싶을 수 있다.

  • 자바 8 이전

    메서드를 다른 메서드로 전달할 방법이 없었다. → Compatator 객체를 만들어서 sort에 넘겨주는 방식을 사용 → 복잡하고 기존 코드를 단순하게 재활용한다는 측면과 맞지 않다.

    Collections.sort(inventory, new Compatator<Apple>() {
      public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
      }
    })
    
  • 자바 8

    메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공한다. ⇒ 이러한 기능을 “동작 파리미터화(behavior parameterization)”라고 부른다.

    inventory.sort(comparing(Apple::getWeight));
    

동작 파라미터화가 왜 중요할까?

sort의 기준이 되는 로직을 구현 메서드를 파라미터화하는 것 처럼, 스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초하기 때문이다.

image

1.2.4 병렬성과 공유 가변 데이터

스트림 API를 통해 “병렬성을 공짜로 얻을 수 있다.” 대신, 포기해야하는 것이 있다.

스트림 메서드로 전달하는 코드의 동작방식을 변경해야 한다.

스트림 메서드로 전달하는 코드는

  • 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다.

  • 안전하게 실행할 수 있는 코드를 만들려면?

    공유된 가변 데이터에 접근하지 않아야 한다.

    why?

    ​ 공유된 변수나 객체가 있으면 병렬성에 문제가 발생한다.

    ​ ex) 두 프로세스가 공유된 변수를 동시에 바꾸는 경우

  • “순수(pure) 함수”, “부작용 없는(side-effect-free) 함수”, “상태없는(stateless) 함수”

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

다중 프로세싱 코어에서 synchronized를 사용하면 생각보다 훨씬 더 비싼 대가를 치러야 할 수 있다. (다중 처리 코어에서는 코드가 순차적으로 실행되어야 하므로 병렬이라는 목적을 무력화시킨다.)

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

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

1.3 자바 함수

자바의 함수 - “수학적인 함수”, 부작용을 일으키지 않는 함수를 의미

자바 8에서는 함수를 새로운 값의 형식으로 추가했다.

why? 멀티코어에서 병렬 프로그래밍을 활용할 수 있는 스트림과 연계될 수 있도록 함수를 만들었기 때문

프로그래밍 언어에의 핵심은 “값을 바꾸는 것”이다. 이 값은 일급(first-class) 값(또는 시민) 이라고 한다.

자바에서 메서드, 클래스는 이급 시민(값으로 전달할 수 없는 것)으로 자바 8에서는 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.

런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들면 프로그래밍에 유용하게 활용할 수 있다.

함수를 값처럼 취급할 수 있을 때의 장점을 살펴보자

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

메서드 참조

예제를 통해서 기존 코드와 자바 8에 새롭게 추가된 메서드 참조 코드를 비교해보자

예제 ) 디렉터리에서 모든 숨겨진 파일을 필터링한다.

  • File 클래스의 isHidden 메서드를 사용

  • 기존 코드

// File 클래스에는 이미 isHidden이라는 메서드가 있는데 불필요하게 FileFilter를 인스턴스화 한다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
  public boolean accept(File file) {
    return file.isHidden();
  }
});
  • 메서드 참조 코드
File[] hiddenFiles = new File(".").listFiles(File::isHidden);

⇒ isHidden이라는 함수가 이미 있으니 메서드 참조 :: (‘이 메서드를 값으로 사용하라’는 의미)를 이용해서 listFiles에 직접 메서드를 전달할 수 있다.

기존에 객체 참조(new로 객체 참조를 생성)를 이용해서 객체를 주고받았던 것처럼 자바 8에서는 메서드 참조를 만들어 메서드를 전달할 수 있게된 것이다.

람다 : 익명함수

자바 8에서는 (기명 named) 메서드를 일급값으로 취급할 뿐 아니라 람다 (익명 함수 anonymous functions)를 포함하여 함수도 값으로 취급할 수 있다.

// x라는 인수를 호출하면 x + 1을 반환
(int x) -> x + 1

이용할 수 있는 편리한 클래스나 메서드가 없을 때 새로운 람다 문법을 이용하면 더 간결하게 코드를 구현할 수 있다.

  • 함수형 프로그래밍 : 람다 문법 형식으로 구현된 프로그램. 즉, “함수를 일급값으로 넘겨주는 프로그램”

예제 ) Apples 리스트에서 특정 조건에 해당하는 사과를 선택해서 리스트를 반환

  • 필터(filter) : 특정 항목을 선택해서 반환하는 동작
public static boolean isGreenApple(Apple apple) {
  return "green".equals(apple.getColor());
}

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

public interface Predicate<T> {
  boolean test(T t);
}

// 메서드가 p라는 이름의 프레디케이트 파라미터로 전달됨
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;
}

// 다음과 같이 호출할 수 있다.
List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);

※ 프레디케이트(predicate)

​ 인수로 값을 받아 true, false를 반환하는 함수

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

하지만, 람다가 몇 줄 이상으로 길어진다면 익명 람다 보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직하다. “코드의 명확성이 우선시 되어야 한다.”

1.4 스트림

※ 컬렉션을 만들어서 데이터를 처리하는 방식과 스트림 API를 사용했을 때의 차이

  • 외부 반복 : 컬렉션에서 반복 과정을 for-each 루프를 이용해서 각 요소를 반복하면서 작업을 수행
  • 내부 반복 : 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다.

스트림 API를 이용하면 내부 반복 처리로 루프를 신경 쓸 필요가 없다.

※ 컬렉션을 이용했을 때 문제

거대한 리스트는 어떻게 처리? 단일 CPU로는 거대한 데이터를 처리하기 힘들다.

⇒ 멀티코어 컴퓨터이기 때문에 서로 다른 CPU 코어에 작업을 각각 할당해서 처리 시간을 줄인다.

​ ex) 8개 코어를 가진 컴퓨터라면, 8개의 코어를 활용해서 병렬로 작업을 수행하여 단일 CPU 컴퓨터에 비해 8배 빨리 작업을 처리할 수 있다.

1.4.1 멀티스레딩은 어렵다.

스트림 API(java.util.stream)

  • 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 해결
  • 멀티코어 활용 어려움 해결

기존의 컬렉션에서 데이터를 처리할 때 반복되는 패턴을 라이브러리에서 제공한다.

  1. 필터링(filtering) - 자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링
  2. 추출(extracting) - 데이터를 추출
  3. 그룹화(grouping) - 데이터를 그룹화

또한, 위 동작들을 쉽게 병렬화할 수 있도록 한다.

  1. 포킹 단계(forking step) - 두 CPU를 가진 환경에서 리스트를 필터링할 때 한 CPU는 리스트의 앞부분을 처리하고, 다른 CPU는 리스트의 뒷부분을 처리하도록 한다.
  2. 필터 - 각각의 CPU는 자신이 맡은 절반의 리스트를 처리한다.
  3. 결과 합침 - 하나의 CPU가 두 결과를 정리한다.

image

컬렉션 vs 스트림

컬렉션 - 어떻게 데이터를 저장하고 접근할지에 중점

스트림 - 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점

스트림은 스트림 내의 요소를 쉽게 병렬로처리할 수있는 환경을 제공한다는 것이 핵심이다.

※ 컬렉션을 필터링할 수 있는 가장 빠른 방법

1. 컬렉션을 스트림으로 바꾼다. 2. 병렬로 처리한다. 3. 리스트로 다시 복원한다.

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

자바 8 이전의 프로그래밍의 어려운 점 - “기존 인터페이스의 변경”

인터페이스를 업데이트하려면 해당 인터페이스를 구현하는 모든 클래스도 업데이트해야 하므로 매우 어려운 작업이다.

⇒ 자바 8의 디폴트 메서드 등장

디폴트 메서드

미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능이다. (특정 프로그램을 구현하는데 도움을 주는 기능이 아님)

(등장 배경) 어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까?

⇒ 자바 8 : 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다.

메서드 본문(bodies)은 클래스 구현이 아니라 인터페이스의 일부로 포함된다. (이를 디폴트 메서드라고 부른다.)

인터페이스 규격명세에 default라는 새로운 키워드를 지원

예제 ) Collections.sort는 사실 List 인터페이스에 포함되지만 실제로 List에 포함된 적은 없다. 이론적으로는 Collections.list(list, comparator)가 아니라 list.sort(comparator)를 수행하는 것이 자연스럽다.

자바 8에서는 List에 직접 메서드를 호출할 수 있다. 이는 자바 8의 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문이다.

default void sort(Comparator<? super E> c) {
  Collections.sort(this, c); //내부적으로 정적 메서드인 Collections.sort를 호출한다.
}

하나의 클래스에서 여러 인터페이스를 구현할 수 있지 않나?

⇒ “여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있다.”는 것은 결국 “다중 상속이 허용된다.”는 의미일까?

⇒ 어느정도 ‘그렇다.’

⇒ 다이아몬드 상속 문제를 피할 수 있는 방법이 있다.

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

Optional

명시적으로 서술형의 데이터 형식을 이용해 null을 회피하는 기법

NullPointer 에외를 피할 수 있도록 도와주는 Optional 클래스를 제공한다.

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

패턴 매칭 기법

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

왜 자바의 switch문에는 문자열과 기본값만 이용할 수 있는 걸까?

일반적으로 객체지향 설계에서 클래스 패밀리를 방문할 때 방문자 패턴(visitor pattern)2을 이용해서 각 객체를 방문한 다음에 원하는 작업을 수행한다. 패턴 매칭을 이용하면 “Brakes 클래스는 Car 클래스를 구성하는 클래스 중 하나입니다. Brakes를 어떻게 처리해야 할지 설정하지 않았습니다.”와 같은 에러를 검출할 수 있다.

  1. 풋프린트 - 특정 하드웨어나 소프트웨어 단위가 차지하고 있는 공간의 크기 

  2. 방문자 패턴 - 방문자와 방문 공간을 분리하여, 방문 공간이 방문자를 맞이할 때, 이후에 대한 행동을 방문자에게 위임하는 패턴