나를 기록하다
article thumbnail
반응형

모던 자바 인 액션(출처: 한빛미디어)

자바 8 등장 전과 후

  • 과거의 코드
Collections.sort(inventory, new Compartor<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});
  • 자바 8을 이용한 자연어에 더 가까운 코드
inventory.sort(comparing(Apple::getWeight));

자바 8이 제공하는 새로운 기술들

  1. 스트림 API
  2. 메서드에 코드를 전달하는 기법
  3. 인터페이스의 디폴트 메서드

자바 8은 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공한다.

데이터베이스 질의 언어에서 고수준 언어로 원하는 동작을 표현하면, 구현(자바에서는 스트림 라이브러리가 이 역할을 수행)에서 최적의 저수준 실행 방법을 선택하는 방식으로 동작한다.

즉, 스트림을 이용하면 에러를 자주 일으키며 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다.

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

예를 들어 약간만 다른 두 메서드가 있다고 가정.
이때 두 메서드를 그대로 유지하는 것보다 인수를 이용하여 다른 동작을 하도록 하나의 메서드로 합치는 것이 바람직할 수 있다.
자바 8 이전 상황에선 익명 클래스를 이용해서 동작 파라미터화를 구현할 수 있었다.
메서드에 코드를 전달(뿐만 아니라 결과를 반환하고 다른 자료구조로 전달할 수도 있음)하는 자바 8 기법은 함수형 프로그래밍에서 위력을 발휘한다.

 

 

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

  • 자바는 처음부터 많은 유용한 라이브러리를 포함하는 잘 설계된 객체지향 언어이면서 스레드와 락을 이용한 소소한 동시성 지원하였다.
    (자바의 하드웨어 중립적인 메모리 모델 때문에 멀티코어 프로세서에서 병렬적으로 수행되는 스레드는 싱글코어에서의 동작과 달리 예기치 못한 상황을 일으킬 수 있다.)
  • 코드를 JVM 바이트 코드로 컴파일하는 특징과 모든 브라우저에서 가상 머신 코드를 지원하기 때문에 자바는 인터넷 애플릿 프로그램의 주요 언어가 되었다.
  • 자바는 다양한 임베디드 컴퓨팅 분야(스마트카드, 토스터, 셋톱박스, 자동차 브레이크 시스템 등)를 장악하고 있다.

 

빅데이터(테라바이트 이상의 데이터셋)의 등장으로 멀티코어 컴퓨터나 컴퓨터 클러스터를 이용해서 빅데이터를 효과적으로 처리할 필요성이 커졌다.
→ 병렬 프로세싱을 활용해야 하는데 지금까지의 자바로는 충분히 대응할 수 없었다.

 

  • 자바 8은 더 다양한 프로그래밍 도구, 다양한 프로그래밍 문제를 빠르고 정확하며 쉽게 유지보수할 수 있다는 장점을 제공한다.
  • 자바 8에서는 새로운 멀티코어 병렬성이 강화되었다.

다음으로 자바 8 설계의 밑바탕을 이루는 세 가지 프로그래밍 개념을 소개한다.

스트림 처리

스트림이란?

한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임
이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다.
즉 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있다.

[예시] 유닉스나 리눅스 등 많은 프로그램 데이터 기록 과정

  1. 표준 입력(Unix & C : stdin, Java : System.in)에서 데이터를 읽는다.
  2. 데이터를 처리한다.
  3. 결과를 표준 출력(Unix & C : stdout, Java : System.out)으로 기록한다.
  4. Unix의 cat 명령은 두 파일을 연결해서 스트림 생성, tr은 스트림의 문자를 번역, sort는 스트림의 행을 정렬, tail -3은 스트림의 마지막 3개 행을 제공한다.

[예시] 파일의 단어를 소문자로 바꾼 다음 사전순으로 단어를 정렬했을 때 가장 마지막에 위치한 세 단어를 출력하는 프로그램

  • file1, file2는 한 행에 한 개의 단어를 포함하고 있다고 가정
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

Unix의 스트림

  • 위 그림처럼 sort는 여러 행의 스트림을 입력으로 받아 여러 행의 스트림을 출력으로 만들어낸다.
  • 유닉스는 여러 명령을 병렬로 실행한다.
  • 따라서 cat이나 tr이 완료되지 않은 시점에서 sort가 행을 처리하기 시작할 수 있다.
예시 - 자동차 생산 공장 라인
자동차 생산 공장은 여러 자동차로 구성된 스트림을 처리
각각의 작업장에서는 자동차를 받아서 수리한 다음 다음 작업장에서 다른 작업을 처리할 수 있도록 넘겨준다.
조립 라인은 자동차를 물리적인 순서로 한 개씩 운반하지만 각각의 작업장에서는 동시에 작업을 처리한다.
스트림 API의 핵심
기존에는 한 번에 한 항목을 처리했었다.
자바 8에서는 고수준(DB 질의처럼)으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다.
또한 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있다.
스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.

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

[예시] 송장 ID 정렬

  1. 예를 들어 2013UK0001, 2014US0002, ... 등의 형식을 갖는 송장 ID가 존재
  2. 처음 네 개의 숫자는 연도, 다음 두 글자는 국가 코드, 마지막 네 개의 숫자는 고객 ID를 의미
  3. 송장 ID를 고객 ID 또는 국가 코드순으로 정렬해야 한다.
    • 자바 8 이전 - 메서드를 다른 메서드로 전달할 방법이 없었다.(Comparator 객체를 만드는 것은 복잡하다.)
    • 자바 8 이후 - 메서드를 다른 메서드의 인수로 넘겨주는 기능 제공
      → 이런 기능을 동적 파라미터화라고 부른다.
public int compareUsingCustomerId(String inv1, String inv2) {
...
}

동적 파라미터화

병렬성과 공유 가변 데이터

병렬성을 얻을 수 있는 대신 스트림 메서드로 전달하는 코드의 동작 방식을 조금 변경해야 한다.
스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다.

  • 보통 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터에 접근하지 않아야 한다.
    → 이런 함수를 순수(pure) 함수, 부작용 없는(side-effect free) 함수, 상태 없는(stateless) 함수라 부른다.
    하지만 공유된 변수나 객체가 있으면 병렬성에 문제가 발생한다.

[예시] 두 프로세스가 공유된 변수를 동시에 바꾸려 하면 어떻게 될까?

  1. synchronized를 이용해서 공유된 가변 데이터를 보호하는 규칙을 만들 수 있다.
    • 일반적으로 synchronized는 시스템에 악영향을 미친다.
    • 다중 프로세싱 코어에서 synchronized를 사용하면 (다중 처리 코어에서는 코드가 순차적으로 실행되어야 하므로 병렬이라는 목적을 무력화시키면서) 생각보다 훨씬 더 비싼 대가를 치러야 할 수 있다.
  2. 자바 8 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용 가능하다.

공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 프로그래밍 패러다임의 핵심적인 사항이다.

반면, 명령형 프로그래밍 패러다임에서는 일련의 가변 상태로 프로그램을 정의한다.
공유되지 않은 가변 데이터 요구사항이란 인수를 결과로 변환하는 기능과 관련된다.
이 요구사항은 수학적인 함수처럼 함수가 정해진 기능만 수행하며 다른 부작용은 일으키지 않음을 의미한다.

자바가 진화해야 하는 이유

  • 제네릭의 등장, List → List 등의 변화 등 컴파일 시 에러 검출과 가독성 향상을 가져왔다.
  • Iterator → for-each 루프를 사용할 수 있게 되었다.
  • 기존의 값을 변화시키는 데 집중했던 고전적 객체지향을 벗어나 함수형 프로그래밍으로 다가섰다는 것이 자바 8의 가장 큰 변화
  • 함수형 프로그래밍에서는 우리가 하려는 작업이 최우선시되며 그 작업을 어떻게 수행하는지는 별개의 문제로 취급한다.
  • 자바 8에서 함수형 프로그래밍을 도입함으로써 두 가지 프로그래밍 패러다임의 장점을 모두 활용할 수 있게 되었다.
    → 문제를 효율적으로 해결할 수 있는 다양한 도구를 얻게 된 것
언어는 하드웨어나 프로그래머의 기대의 변화에 부응하는 방향으로 변화해야 한다.

 

자바 함수

자바의 함수란?

  • 함수(function): 메서드 특히 정적 메서드(static method)와 같은 의미로 사용된다.
  • 자바의 함수 : 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.

자바 프로그램에서 조작할 수 있는 값

  • int, double 등의 기본값
  • 객체(객체의 참조). new 또는 팩토리 메서드 또는 라이브러리 함수를 이용해서 객체의 값을 얻을 수 있다.
    객체 참조는 클래스의 인스턴스를 가리킨다.
    • 예시) "abc"(String 형식), new Integer(1111)(Integer 형식), new HashMap<Integer, String>(100) (HashMap의 생성자 호출) 등으로 객체 참조를 얻을 수 있다.

함수가 필요한 이유

  • 프로그래밍 언어의 핵심은 값을 바꾸는 것.
  • 이 값을 일급 값(first-class)(또는 시민(citizens))이라 부른다.
  • 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스, ...)가 값의 구조를 표현하는데 도움이 될 수 있다.
  • 프로그램을 실행하는 동안 모든 구조체를 자유롭게 전달할 수는 없는데 전달할 수 없는 구조체를 이급 시민이라 한다.
  • 위 예시로 든 값들은 모두 일급 자바 시민 / 메서드, 클래스 등은 이급 자바 시민에 해당
  • 인스턴스화한 결과가 값으로 귀결되는 클래스를 정의할 때 메서드를 아주 유용하게 활용할 수 있지만 여전히 메서드와 클래스는 그 자체로 값이 될 수 없다.
    • 예시) 런타임에 메서드를 전달할 수 있다면, 즉 메서드를 일급 시민으로 만들면 프로그랠밍에 유용하게 활용할 수 있다.
    • 자바 8에서 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.

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

메서드 참조(method reference)

디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정

  • 우선 주어진 파일이 숨겨져 있는지 여부를 알려주는 메서드를 구현해야 한다. File 클래스의 isHidden 메서드
    • isHidden은 File 클래스를 인수로 받아 boolean을 반환하는 함수

[예시] FileFilter 객체 내부에 위치한 isHidden의 결과를 File.listFiles 메서드로 전달하는 방법으로 숨겨진 파일 필터링

자바 8 등장 전의 코드

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

자바 8 등장 이후의 코드

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

이미 isHidden이라는 함수는 준비되어 있으므로 자바 8의 메서드 참조 ::('이 메서드를 값으로 사용하라'는 의미)를 이용해서 listFiles에 직접 전달할 수 있다. 여기서 메서드가 아닌 함수라는 용어를 사용했다.
자바 8에서는 메서드가 일급값이다. 기존에 객체 참조(new로 객체 참조를 생성함)를 이용해서 객체를 주고받았던 것처럼 자바 8에서는 File::isHidden을 이용해서 메서드 참조를 만들어 전달할 수 있게 되었다.

람다: 익명함수

람다를 포함하여 함수도 값으로 취급 가능하다.

[예시] x라는 인수로 호출하면 x+1을 반환하는 동작을 수행하는 코드(MyMathUtils)

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

기존의 숨겨진 파일 필터링 방식

  • FileFilter 객체로 isHidden 메서드를 감싼 다음에 File.listFiles 메서드로 전달해야 했다.

기존의 숨겨진 파일 필터링 방식


자바 8 스타일

  • 자바 8에서는 메서드 참조 :: 문법을 이용해서 직접 isHidden 함수를 listFiles 메서드로 전달할 수 있다.

자바 8 스타일

코드 넘겨주기

Apple 클래스와 getColor 메서드가 있고, Apples 리스트를 포함하는 변수 inventory가 있다고 가정.
이때 모든 녹색 사과를 선택해서 리스트를 반환하는 프로그램을 구현하려 한다. 이처럼 특정 항목을 선택해서 반환하는 동작을 필터(filter)라고 한다.

자바 8 등장 전 filterGreenApples 메서드 구현

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>();
    // 반환되는 result는 List로, 처음에는 비어 있지만 점점 녹색 사과로 채워진다.

    for (Apple apple : inventory) {
        if(GREEN.equals(apple.getColor())) { // 녹새 사과만 선택한다.
            result.add(apple);
        }
    } return result;
}

누군가 사과를 무게로 필터링하고 싶으면 복사&붙여넣기를 하고 if문을 수정해야 한다.


자바 8 등장 후 구현

  • 자바 8에서는 코드를 인수로 넘겨줄 수 있으므로 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> {
// 명확히 하기 위해 추가(보통 java.util.function에서 임포트함)
    boolean test(T t);
}

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

다음처럼 메서드를 호출할 수 있다.

filterApples(inventory, Apple::isGreenApple);

또는

filterApples(inventory, Apple::isHeavyApple);
프레디케이트(predicate)란 무엇인가?
예제에서 Apple::isGreenApple 메서드를 filterApples로 넘겨주었다.
filterApples는 Predicate을 파라미터로 받는다.
수학에서는 인수로 값을 받아 true나 false로 반환하는 함수를 predicatefk gksek.
Function<Apple, Boolean> 같이 코드를 구현할 수 있지만 Predicate을 사용하는 것이 더 표준적인 방식이며 boolean을 Boolean으로 변환하는 과정이 없으므로 더 효율적이다.

 

메서드 전달에서 람다로

메서드를 값으로 전달하는 것은 유용한 기능이지만 한두 번만 사용할 메서드를 매번 정의하는 것의 귀찮음을 해결할 방법으로 익명 함수 또는 람다라는 개념을 사용할 수 있다.

filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()) );

위와 같은 방법으로 즉, 한 번만 사용할 메서드는 따로 정의를 구현할 필요 없이 간결하게 표현할 수 있다.
하지만 람다가 몇 줄 이상으로 길어진다면(조금 복잡한 동작을 수행하는 상황) 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직하다. 코드의 명확성이 우선시되어야 한다.

다음처럼 라이브러리 메서드 filter을 이용하면 filterApples 메서드를 구현할 필요가 없다.

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

하지만 병렬성이라는 중요성 때문에 설계자들은 이와 같은 설계를 포기했다.
대신 자바 8에서는 filter와 비슷한 동작을 수행하는 연산집합을 포함하는 새로운 스트림 API를 제공한다.

 

스트림(Stream)

거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용한다.

[예시] 리스트에서 고가의 거래(트랜잭션)만 필터링한 다음에 통화로 결과를 그룹화하는 상황

map<Currency, List<Transaction>> transactionByCurrencies = new HashMap<>();
// 그룹화된 트랜잭션을 더할 Map 생성
for (Transaction transaction : transactions) { // 트랜잭션의 리스트를 반복
    if (transaction.getPrice() > 1000) { // 고가의 트랜잭션을 필터링
        Currency currency = transaction.getCurrency(); // 트랜잭션의 통화 추출
        List<Transaction> transactionForCurrency =
            transactionsByCurrencies.get(currency);
        if (transactionForCurrency == null) {
        // 현재 통화의 그룹화된 맵에 항목이 없으면 새로 만든다.
            transactionsForCurrency = new ArrayList<>();
            transactionsByCurrencies.put(currency, transactionsForCurrency);
        }
        transactionsForCurrency.add(transaction);
        // 현재 탐색된 트랜잭션을 같은 통화의 트랜잭션 리스트에 추가한다.
    }
}

위의 코드를 스트림 API를 이용하면 다음처럼 문제를 해결할 수 있다.

import static java.util.stream.Collectors.groupingBy;

Map<Currency, List<Transaction>> transactionsByCurrencies =
    transactions.stream()
        .filter((Transaction t) -> t.getPrice() > 1000) // 고가의 트랜잭션 필터링
        .collect(groupingBy(Transaction::getCurrency)); // 통화로 그룹화

컬렉션에서는 반복 과정을 직접 처리해야 했다. ex) for-each 루프를 돌면서 작업 수행
→ 이런 방식의 반복을 외부 반복(external iteration)이라 한다.

스트림 API를 이용하면 라이브러리 내부에서 모든 데이터가 처리된다.
→ 이런 방식의 반복을 내부 반복(internal iteration)이라 한다.

컬렉션을 이용했을 때 많은 요소를 가진 목록을 반복한다면 오랜 시간이 걸린다. 단일 CPU로는 거대한 데이터를 처리하기 힘들 것이다. 하지만 멀티코어 컴퓨터가 있기에 서로 다른 CPU 코어에 작업을 각각 할당해서 처리 시간을 줄일 수 있다면 병렬로 작업을 수행하여 단일 CPU에 비해 8배 빨리 작업을 처리할 수 있다.

멀티코어 컴퓨터
예시로 구글의 검색 엔진은 하나의 컴퓨터로는 수행할 수 없는 종류의 코드를 실행하는 좋은 예제다.
구글 검색 엔진은 인터넷의 모든 페이지를 읽어 인터넷 페이지에 등장한 모든 단어를 관련 URL과 매핑하는 인덱스를 생성한다.
우리가 구글 검색에 몇 가지 단어를 입력하면 소프트웨어는 만들어진 인덱스를 이용해서 입력한 단어를 포함한 몇 가지 웹페이지를 보여준다.

 

멀티스레딩은 어렵다

멀티스레딩 환경에서 각각의 스레드는 동시에 공유된 데이터에 접근하고, 데이터를 갱신할 수 있다. 결과적으로 스레드를 잘 제어하지 못하면 원치 않는 방식으로 데이터가 바뀔 수 있다. 멀티스레딩 모델은 순차적인 모델보다 다루기 어렵다.

자바 8은 스트림 API(java.util.stream)로 '컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제' 그리고 '멀티코어 활용 어려움'이라는 두 가지 문제를 모두 해결했다.

기존의 컬렉션에서 데이터를 처리할 때 반복되는 패턴(이전 예시의 filterApples나 SQL과 같은 DB 질의 언어 동작과 비슷한)이 많았다. 따라서 라이브러리에서 이러한 반복되는 패턴을 제공한다면 좋을 것이라는 아이디어가 변화의 동기가 되었다.

즉, 자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링(filtering)하거나 추출(extracting), 그룹화(grouping)하는 등의 기능이 있다. 또한 이러한 동작들을 쉽게 병렬화할 수 있다는 점도 변화의 동기가 되었다.

[예시] 두 CPU를 가진 환경에서 리스트를 필터링 하는 경우

  1. 한 CPU는 리스트의 앞부분을 처리하고 다른 CPU는 리스트의 뒷부분을 처리하도록 요청 가능(포킹 단계(forking step))
  2. 각각의 CPU는 자신이 맡은 절반의 리스트를 처리
  3. 하나의 CPU가 두 결과를 정리

포크, 필터, 결과합침

 

스트림 API도 기존의 컬렉션 API와 아주 비슷한 방식으로 동작한다고 간주한다.
다만 컬렉션은 어떻게(How) 데이터를 저장하고 접근할지에 중점을 두는 반면,
스트림은 데이터에 어떤(What) 계산을 할 것인지 묘사하는 것에 중점을 둔다.

순차 처리 방식의 코드

import static java.util.stream.Collectors.toList;

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

 

병렬 처리 방식의 코드

import static java.util.stream.Collectors.toList;

List<Apple> heavyApples =
    inventory.parallelStream().fileter((Apple a) -> a.getWeight() > 150)
                              .collect(toList());
자바의 병렬성과 공유되지 않은 가변 상태자바 8이 제공하는 두 가지 요소
라이브러리에서 분할을 처리한다.즉, 큰 스트림을 병렬로 처리할 수 있도록 작은 스트림으로 분할한다.filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있다.상호작용을 하지 않는다는 제약의 예시로 Apple::isGreenApple을 생각해보면, 함수형 프로그래밍에서 함수형이란 '함수를 일급값으로 사용한다'는 의미도 있지만 부가적으로 '프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다'라는 의미도 포함된다.

자바의 어려움 중 하나는 기존 인터페이스의 변경이다. 예시로 Collections.sort는 사실 List 인터페이스에 포함되지만 실제로 List로 포함된 적은 없다. 이론적으로는 Collection.list(list, comparator)가 아니라 list.sort(comparator)를 수행하는 것이 적절하다. 자바 8 이전에는 인터페이스를 업데이트하려면 해당 인터페이스를 구현하는 모든 클래스도 업데이트해야 하므로 인터페이스 변경이 불가능에 가까웠지만 자바 8에서는 디폴트 메서드(default method)로 이 문제를 해결할 수 있다.

 

 

디폴트 메서드와 자바 모듈

자바 9의 모듈 시스템은 모듈의 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인 작업이 용이해졌다. 또한 자바 8에서 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.

[예시] 이전의 예제 코드 - 무거운 사과 리스트

List<Apple> heavyApples1 = 
inventory.stream().filter((Apple a) -> a.getWeight() > 150)
                  .collect(toList));
List<Apple> heavyApples2 =
inventory.parralelStream().fileter((Apple a) -> a.getWeight() > 150)
                          .collect(toList());

자바 8은 기존의 구현을 고치지 않고 이미 공개된 인터페이스를 변경할 수 있는, 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다. 메서드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함된다(이를 디폴트 메서드라 부른다).

예를 들어 List에 직접 sort 메서드를 호출할 수 있다.
이는 List 인터페이스에 다음과 같은 디폴트 메서드 정의가 추가되었기 때문이다(이 디폴트 메서드는 정적 메서드인 Collections.sort를 호출한다).

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

그런데 하나의 클래스에서 여러 인터페이스를 구현할 수 있지 않나? 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있다는 것은 다중 상속이 허용된다는 의미일까? → 어느 정도는 '그렇다'고 말할 수 있다.

 

 

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

자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다. Optional<T>는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체다. Optional<T>는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다. 따라서 Optional<T>를 사용하면 NullPointer 예외를 피할 수 있다. 즉, 형식 시스템을 이용해서 어떤 변수에 값이 없을 때 어떻게 처리할지 명시할 수 있다.

(구조적) 패턴 매칭 기법도 존재한다.

[예시] 수학에서의 패턴 매칭

f(0) = 1
f(n) = n * f(n-1) 그렇지 않으면

자바에서는 if-then-elsesk switch문을 이용했을 것이다. 다른 언어에서는 if-then-else보다 패턴 매칭으로 더 정확한 비교를 구현할 수 있다.
자바 8은 패턴 매칭을 완벽하게 지원하지 않지만 사용은 가능하다.

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...