Java8 - Java8 이란?


이 포스트에서는 Java8 에 추가된 기능인 아래 내용에 대해 간략히 살펴본다.

  • Stream
  • 메서드 레퍼런스
  • 람다 표현식
  • 디폴트 메서드

소스는 github 에 있습니다.


목차


1. Java8 에 추가된 기능

1.1. Stream 처리

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

Java8 에는 Stream API 가 추가되었는데 지금을 일단 단순히 어떤 항목을 연속으로 제공하는 어떠한 기능이라고 생각하면 된다.

Stream API 는 아래와 같은 특징이 있다.

  • 기존에는 한 번에 한 항목을 처리했지만 Stream API 를 이용하여 일련의 스트림으로 만들어 처리 가능
  • 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당 가능
    → 스레드를 사용하지 않으면서도 병렬 처리 가능

Stream API 의 좀 더 자세한 내용은 Java8 - Stream
, Java8 - Stream 활용 (1): 필터링, 슬라이싱, 매핑, 검색, 매칭
, Java8 - Stream 활용 (2): 리듀싱, 숫자형 스트림, 스트림 생성
, Java8 - Stream 으로 데이터 수집 (1): Collectors 클래스, Reducing 과 Summary, Grouping
, Java8 - Stream 으로 데이터 수집 (2): Partitioning, Collector 인터페이스, Custom Collector , Java8 - Stream 으로 병렬 데이터 처리 (1): 병렬 스트림, 포크/조인 프레임워크 , Java8 - Stream 으로 병렬 데이터 처리 (2): Spliterator 인터페이스 를 참고하세요.


1.2. 동작 파라메터화로 메서드에 코드 전달

코드의 일부를 API 로 전달하는 것으로 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공한다.

동작 파라메터화의 좀 더 자세한 내용은 Java8 - 동작 파라메터화 을 참고해주세요.


1.3. 병렬성과 공유 가변 데이터

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

공유된 가변 데이터에 접근하면 어떤 상황이 발생하는지는 Java8 - Stream 으로 병렬 데이터 처리 (1): 병렬 스트림, 포크/조인 프레임워크1.3. 병렬 스트림의 올바른 사용 를 참고하세요.

만일 공유된 변수가 객체가 있으면 병렬성에 문제가 발생한다. (예 - 공유된 변수를 동시에 변경 시도)

기존처럼 synchronized 를 이용하여 공유된 가변 데이터를 보호할 수도 있지만 일반적으로 synchronized 는 시스템 성능에 악영향을 미친다.
Java8 의 Stream 을 이용하면 기존의 Java 스레드 API 보다 쉽게 병렬성을 활용할 수 있다.


2. Java 함수

Java8 에서는 함수를 새로운 값의 형식으로 추가했다. 기존엔 함수가 일급 시민이 아니었지만, Java8 부터는 일급 시민의 조건을 충족한다.

  • 변수나 데이터에 할당 가능해야 함
  • 객체의 인자로 넘길 수 있어야 함
  • 객체의 리턴값으로 리턴할 수 있어야 함

2.1. 메서드 레퍼런스: ::

Java8 에서는 메서드가 일급 시민이므로 기존에 객체 레퍼런스(new 로 객체 레퍼런스를 생성) 를 이용하여 객체를 주고 받는 것이 아닌, :: 문법을 이용하여 메서드 레퍼런스를 만들어 전달 가능하다.

  • ::: 이 메서드를 값으로 사용하라는 의미, 예) File::isHidden

아래는 기존 자바를 메서드 레퍼런스를 이용한 예시이다.

// 기존 자바
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
  public boolean accept(File file) {
    return file.isHidden();
  }
});

// 메서드 레퍼런스 이용
File[] hiddenFiles2 = new File(".").listFiles(File::isHidden);  // File 클래스에 있는 isHidden 함수를 값으로 사용

또 다른 예로 Apple 이라는 클래스가 있고, Apple 리스트를 담고 있는 inventory 리스트가 있을 때 여기서 빨간 사과와 100g 이 넘는 사과를 필터링하려면 기존 자바로는 아래와 같이 구현 가능하다.

List<Apple> inventory = Arrays.asList(new Apple(10, "red"),
        new Apple(100, "green"),
        new Apple(150, "red"));

// Apple 이라는 클래스
  public static class Apple {
    private int weight;
    private String color;

    public Apple(int weight, String color) {
      this.weight = weight;
      this.color = color;
    }

    public int getWeight() {
      return weight;
    }

    public String getColor() {
      return color;
    }
  ...
  }

기존 자바

// 기존 자바에서 red 사과 필터링 시
public static List<Apple> filterRedApples(List<Apple> inventory) {
  List<Apple> result = new ArrayList<>();
  for (Apple apple: inventory) {
    if ("red".equals(apple.getColor())) {
      result.add(apple);
    }
  }
  return result;
}

// 기존 자바에서 100 이상 필터링 시
public static List<Apple> filterWeightApples(List<Apple> inventory) {
  List<Apple> result = new ArrayList<>();
  for (Apple apple: inventory) {
    if (apple.getWeight() >= 100) {
      result.add(apple);
    }
  }
  return result;
}

// 호출 시

// 기존 자바에서 red 사과 필터링 시
List<Apple> redApples = filterRedApples(inventory);
// [Apple{weight=10, color='red'}, Apple{weight=150, color='red'}]
System.out.println(redApples);

// 기존 자바에서 100 이상 필터링 시 (filterRedApples() 와 중복)
List<Apple> weightApples = filterWeightApples(inventory);
// [Apple{weight=100, color='green'}, Apple{weight=150, color='red'}]
System.out.println(weightApples);

메서드 레퍼런스로 변경

// 메서드 레퍼런스로 필터링 시
public static boolean isRedApple(Apple apple) {
  return "red".equals(apple.getColor());
}
public static boolean isWeightApple(Apple apple) {
  return apple.getWeight() >= 100;
}
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;
}

// 호출 시

// 메서드 레퍼런스로 red 사과 필터링 시
List<Apple> redApples2 = filterApples(inventory, Chap01Application::isRedApple);  // Chap01Application 은 클래스명
// [Apple{weight=10, color='red'}, Apple{weight=150, color='red'}]
System.out.println(redApples2);

// 메서드 레퍼런스로 100 이상 필터링 시
List<Apple> weightApples2 = filterApples(inventory, Chap01Application::isWeightApple);
// [Apple{weight=100, color='green'}, Apple{weight=150, color='red'}]
System.out.println(weightApples2);

Predicate
위에서 Apple::isRedApple 은 filterApples 의 두 번째 인자인 Predicate<Apple> p 로 넘겨주었다.

인수를 값으로 받아서 true/false 로 반환하는 함수를 Predicate 라고 한다.

메서드 레퍼런스의 좀 더 상세한 내용은 Java8 - 람다 표현식 (2): 메서드 레퍼런스, 람다 표현식과 메서드의 조합 을 참고하세요.


2.2. 람다 (익명 함수)

Java8 은 메서드를 일급값으로 취급할 뿐 아니라 람다(=익명 함수)를 포함하여 함수도 값으로 취급할 수 있다.
예) (int x) -> x + 1; // x 을 인수로 호출하면 x+1 리턴

람다 문법으로 구현된 프로그램을 함수형 프로그래밍(=함수를 일급값으로 넘겨줌) 을 구현한다라고 한다.

바로 위의 메서드 레퍼런스로 사과를 필터링하는 부분을 다시 보자.
메서드를 값으로 전달하는 것도 좋지만 만일 한 번만 사용할 기능이라면 조건이 늘어날때마다 isRedApple(), isWeightApple() 를 추가해주는게 아니라 람다로 개발하는 것이 편하다.

람다로 필터링 시

// 람다로 red 사과 필터링 시
List<Apple> redApples3 = filterApples(inventory, (Apple a) -> "red".equals(a.getColor()));
// [Apple{weight=10, color='red'}, Apple{weight=150, color='red'}]
System.out.println(redApples3);

// 람다로 100 이상 필터링 시
List<Apple> weightApples3 = filterApples(inventory, (Apple a) -> a.getWeight() >= 100);
// [Apple{weight=100, color='green'}, Apple{weight=150, color='red'}]
System.out.println(weightApples3);

람다의 좀 더 상세한 내용은 Java8 - 람다 표현식 (1): 함수형 인터페이스, 형식 검사1. 람다 표현식 을 참고하세요.


3. Stream

Java 애플리케이션은 Collection 을 많이 활용한다.

Stream API 를 이용하면 Collection API 와는 상당히 다른 방식으로 데이터를 처리할 수 있다.

Collection 에서는 반복 작업 시 for-each 를 통해 각 요소를 반복하면서 직접 반복 작업을 진행(=외부 반복)하는 반면
Stream API 에서는 루프를 신경쓸 필요없이 라이브러리 내부에서 모든 데이터가 처리된다. (=내부 반복)

또한 Collection 을 이용할 때 많은 요소를 가진 목록을 반복한다면 단일 CPU 로는 힘들수도 있기 때문에 서로 다은 CPU 코어에 작업을 각각 할당해서 처리 시간을 줄이는 것이 좋다.

이전의 자바에서 제공하는 스레드 API 로 멀티스레딩을 구현해서 병렬성을 이용하는 것은 쉽지 않다.(각 스레드가 동시에 공유된 데이터에 접근하고 데이터를 갱신할 수 있음)
Java8 은 Stream API 로 Collection 을 처리하면서 발생하는 반복적인 코드 문제멀티코어 활용의 어려움을 해결한다.

Stream API 는 요소를 병렬로 쉽게 처리할 수 있는 환경을 제공한다.

아래는 각각 순차/병렬 처리 방식의 코드이다.

순차 처리 방식

List<Apple> weightApples = inventory.stream().filter((Apple a) -> a.getWeight() >= 100)
                                             .collect(toList());

병렬 처리 방식

List<Apple> weightApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() >= 100)
                                             .collect(toList());

Collection 을 필터링할 수 있는 가장 빠른 방법은 Collection 을 Stream 으로 변경하여 병렬로 처리한 다음 List 로 다시 복원하는 것임.

병렬 처리와 성능에 대해서는 Java8 - Stream 으로 병렬 데이터 처리 (1): 병렬 스트림, 포크/조인 프레임워크Java8 - Stream 으로 병렬 데이터 처리 (2): Spliterator 인터페이스 를 참고하세요.


4. 디폴트 메서드: default

디폴트 메서드는 메서드 바디를 제공하여 인터페이스를 구현하는 다른 클래스에서 해당 메서드를 구현하지 않아도 되는 기능이다. 즉, 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스가 포함할 수 있도록 한다.

Java8 에서 List 의 sort 를 직접 호출할 수 있는 이유도 Java8 의 List 인터페이스에 아래와 같은 디폴트 메서드 정의가 추가되었기 때문이다.

default void sort(Comparator<? super E> c) {
  Collections.sort(this, c);
}

따라서 Java8 이전에는 List 를 구현하는 모든 클래스가 sort 를 구현해야 했지만 Java8 부터는 디폴트 sort 를 구현하지 않아도 된다.

이 외에도 NullPointer 예외를 피할 수 있도록 해주는 Optional<T> 클래스도 제공한다.
Optional<T> 는 값을 갖거나 갖지 않을 수 있는 컨테이너이다.

Optional<T> 에 대한 좀 더 상세한 내용은 Java8 - Optional 클래스 를 참고하세요.


참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트 저자의 Java 8 in Action을 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10