Today I Learned

[모던 자바 인 액션] 1장. 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가? 본문

JAVA & Spring/모던 자바 인 액션

[모던 자바 인 액션] 1장. 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?

하이라이터 2021. 5. 31. 03:02
728x90

이 장의 내용

  • 자바가 거듭 변화하는 이유
  • 컴퓨팅 환경의 변화
  • 자바에 부여되는 시대적 변화 요구
  • 자바 8과 자바 9의  새로운 핵심 기능 소개

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

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

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

 

자바 8 이전에는 병렬 실행 환경을 관리하기 위해 자바1.0의 스레드와 락, 메모리 모델부터 자바 5의 스레드 풀, 병렬 실행 컬렉션, 자바 7의 포크/조인 프레임워크 등의 도구를 지원해왔다.

하지만 여전히 개발자가 활용하기는 쉽지 않았고, 자바 8에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공한다.


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

프로그래밍 언어는 마치 생태계를 닮았다. 새로운 언어가 등장하면서 진화하지 않은 기존 언어는 사장되었으며, 특정 분야에서 장점을 가진 언어는 다른 경쟁언어를 도태시킨다.

완벽한 언어는 존재하지 않으며 모든 언어가 장단점을 갖고있다.

C, C++은 임베디드 시스템에서 여전히 인기를 끌고 있으며, 런타임 풋프린트에 여유가 있는 어플리케이션에서는 자바, C#같이 안전한 형식의 언어가 더 선호된다.

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

자바는 처음부터 많은 유용한 라이브러리를 포함하는 잘 설계된 객체지향 언어로 시작됐다.

 

캡슐화 덕분에 C에 비해 소프트웨어 엔지니어링적인 문제가 훨씬 적다는 점과 객체지향의 정신적인 모델 덕분에 윈도우 95 및 그 이후의 WIMP 프로그래밍 모델에 쉽게 대응할수 있다는 이유로 각광받기 시작했다.

 

스레드와 락을 이용한 동시성을 지원했으며, 코드를 JVM 바이트 코드로 컴파일하는 특징 때문에 인터넷 애플릿 프로그램의 주요 언어가 되었다. 또한 자바는 다양한 임베디드 컴퓨팅 분야를 성공적으로 장악하고 있다.

 

하지만 빅데이터라는 도전에 직면하면서 멀티코어 컴퓨터나 컴퓨팅 클러스터를 이용해서 빅데이터를 효과적으로 처리할 필요성이 커졌다. 즉, 병렬 프로세싱을 활용해야 하는데 지금까지의 자바로는 충분히 대응할 수 없었다.

 

이러한 문제들을 해결하기 위해 자바 8에서는 자바에 없던 새로운 개념들을 도입해서 현재 시장에서 요구하는 기능을 효과적으로 제공한다.

1.2.2 스트림 처리

자바 8에 추가된 첫 번째 프로그램 개념은 스트림 처리(stream processing)다.

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

프로그램은 입력 스트림에서 데이터를 한 개씩 읽어들이며, 출력 스트림으로 데이터를 한 개씩 기록한다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있다.

 

유닉스의 명령어로 예를 들어보자.

cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

cat 명령은 두 파일을 연결해서 스트림을 생성하며, tr은 스트림의 문자를 번역하고, sort는 스트림의 행을 정렬하며, tail -3은 스트림의 마지막 3개 행을 제공한다. 이러한 명령을 파이프(|)를 이용해서 연결할 수 있다.

 

자바 8에서는 java.util.stream 패키지에 스트림 API가 추가되었으며, 스트림 패키지에 정의된 Stream<T>는 T 형식으로 구성된 일련의 항목을 의미한다. 유닉스 명령어로 복잡한 파이프라인을 구성했던 것 처럼 스트림 API는 파이프라인을 만드는데 필요한 많은 메서드를 제공한다.

 

스트림 API의 핵심은 기존에는 한번에 한 항목씩 처리했던 작업을 (데이터베이스 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다. 또한 스트림 파이프라인을 이요하면 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 장점도 있다.

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

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

기존의 자바에서 sort에 특정한 정렬 순서를 지정하기 위해서는 Comparator 객체를 만들어서 sort 메서드에 전달해주어야 했다. 하지만 이는 너무 복잡하며 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않다.

 

Comparator을 사용한 사용자지정 정렬

Collections.sort(inventory, new Comparator<Apple>() {
  public int compare(Apple a1, Apple a2) {
    return a1.getWeight().compareTo(a2.getWeigth());
  }
});

 

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

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

세 번째 프로그래밍 개념은 "병렬성을 공짜로 얻을 수 있다"라는 말에서 시작된다.

 

이러한 병렬성을 얻기 위해서는 스트림 메서드로 전달하는 코드가 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있도록 코드의 동작 방식을 조금 바꿔야 한다.

 

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

기존에는 synchronized를 이용해서 공유된 가변 데이터를 보호하는 규칙을 만들었으나, 자바 8의 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있다.

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

지금까지 자바는 진화해왔다. 제네릭(generic)이 나타나고, List가 List<String> 등으로 바뀌었으며, for-each 루프가 추가되는 등의 변화를 계속해왔다.

자바 8에서는 기존 값을 변화시키는 데 집중했던 고전적인 객체지향에서 벗어나 함수형 프로그래밍으로 다가섰다. 함수형 프로그래밍에서는 우리가 하려는 작업이 최우선시 되며 그 작업을 어떻게 수행하는지는 별개의 문제로 취급한다.

언어는 하드웨어나 프로그래머의 기대의 변화에 부응하는 방향으로 변화해야하며, 자바는 계속 새로운 기능을 추가하며 인기 언어의 자리를 유지하고 있다.


1.3 자바 함수

프로그래밍 언어에서 함수(function)라는 용어는 메서드(method) 특히 정적 메서드와 같은 의미로 사용된다.

자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.

 

자바 프로그램에서 조작할 수 있는 값을 생각해보자.

  • int 형, double 형 등의 기본값
  • new 또는 팩토리 메서드, 라이브러리 함수를 이용해 얻을 수 있는 객체(의 참조) 값

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

 

자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값의 구조를 표현하는데 도움이 될 수 있다.

하지만 프로그래밍 실행하는 동안 이러한 구조체를 자유롭게 전달할 수는 없으며, 이렇게 전달할 수 없는 구조체는 이급 시민이다.

인스턴스화한 결과가 값으로 귀결되는 클래스를 정의할 때 메서드를 유용하게 활용할 수 있지만 여전히 메서드와 클래스는 그 자체로 값이 될 수 없다. 

 

자바 8에서는 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.

이를 통해 런타임 단계에서 메서드를 전달할 수 있으며, 프로그래밍에 유용하게 활용할 수 있다.

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

자바 8에서는 메서드를 값으로 취급할 수 있도록 설계되었으며, 이러한 기능은 스트림 같은 다른 자바 8 기능의 토대를 제공했다.

 

메서드 참조(method reference)

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

기존의 코드는 다음과 같다.

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

File 클래스에 이미 isHidden이라는 메서드가 있지만 굳이 FileFilter로 isHidden을 복잡하게 감싼 다음에 FileFilter를 인스턴스화해야 했다.

 

자바 8에서는 다음처럼 코드를 구현할 수 있다.

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

isHidden이라는 함수는 준비되어 있으므로 자바 8의 메서드참조 ::('이 메서드를 값으로 사용하라'는 의미)를 이용해서 listFiles에 직접 전달할 수 있다.

메서드가 아닌 함수라는 용어를 사용했다는 사실도 주목하자.

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

 

람다 : 익명 함수

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

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

1.3.2 코드 넘겨주기 : 예제

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

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

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

하지만 누군가 사과의 무게(예를 들면 150그램 이상)으로 필터링 하고 싶을 수 있다.

그러면 다음과 같이 코드를 구현할 것이다. (아마 전체 코드를 복사&붙여넣기해서)

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

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

그리고 다음과 같이 메서드를 호출할 수 있다.

fileterApples(inventory, Apple::isGreenApple);

fileterApples(inventory, Apple::isHeavyApple);

 

프레디케이트(predicate)란 무엇인가?
수학에서는 인수로 값을 받아 true나 false로 반환하는 함수를 프레디케이트라고 한다.
자바 8에서도 Function<Apple, Boolean> 같이 코드를 구현할 수 있지만 Predicate<Apple>을 사용하는 것이 더 표준적인 방식이다.

1.3.3 메서드 전달에서 람다로

위의 메서드들이 한두번만 사용할 기능이라면 익명함수를 통해 코드를 구현할 수 있다.

fileterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );

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

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

멀티코어 CPU가 아니었다면 자바 8은 함수형 프로그래밍을 활용한 filter 등의 몇몇 일반적인 라이브러리 메서드를 추가하는 방향으로 발전했을 수도 있었다.

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

하지만 병렬성이라는 중요성 때문에 자바 8의 설계자들은 이와같은 설계를 포기하였다.

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


1.4 스트림

기존의 컬렉션 API에서는 for-each 루프를 통한 반복 과정을 직접 처리했다.

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

 

반면 스트림 API를 이용하면 루프를 신경 쓸 필요가 없다.

내부 반복(internal iteration) 방식을 사용해 API 내부에서 모든 데이터가 처리되기 때문이다.

1.4.1 멀티스레딩은 어렵다.

스트림 API는 기존 자바에서 발생하던 두가지 문제를 모두 해결했다.

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

자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 '필터링'하거나, 데이터를 '추출'하거나, 데이터를 '그룹화'하는 기능들 등의 반복되는 패턴을 제공한다. 또한 이러한 동작들을 쉽게 병렬화할 수 있다.

 

컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할것인지 묘사하는 것에 중점을 둔다.


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

자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공한다. 이를 통해 패키지 모음을 포함하는 모듈을 정의할 수 있다.

모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인 작업이 용이해졌다.

 

또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.

구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공하며, 메서드 본문은 클래스 구현이 아니라 인터페이스 일부로 포함된다.

 

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

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

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

함수형 언어에는 명시적으로 서술형의 데이터를 이용해 null을 회피하는 기법이 있다.

자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다.

Optional<T>는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.

 

 

 

728x90
Comments