[Modern-Java-in-Action] 1장 : 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?

이동엽·2022년 12월 10일
3

java

목록 보기
11/18

이 장의 주요 내용

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

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

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

  • 사과 목록을 무게순으로 정렬하는 고전적 코드
Collections.sort(inventory, new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
});
  • 자바 8을 이용하여 자연어에 더 가깝게 간단한 방식으로 구현
inventory.sort(comparing(Apple::getWeight));

멀티코어 CPU 대중화와 같은 하드웨어적인 변화도 자바 8에 영향을 미쳤다.


  • 자바 8이 등장하기 이전에는 자바 프로그램이 코어 중 하나만을 사용했다.
    • 이때, 나머지 코어를 활용하려면? → 스레드를 사용하는 것이 좋다.
    • 단, 스레드를 사용하면 관리하기 어렵고 많은 문제가 발생할 수 있다.

  • 자바는 이러한 병렬 실행 환경을 쉽게 관리하고 에러가 덜 발생하는 방향으로 진화하려 노력했다.
    • 자바 1 → 스레드와 락, 심지어 메모리 모델까지 지원.
    • 자바 5 → 스레드 풀, 병렬 실행 컬렉션 등 강력한 도구를 도입.
    • 자바 7 → 포크/조인 프레임워크 제공.
    • 자바 8 → 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공.
      • 스트림 API
      • 메서드에 코드를 전달하는 기법 (메소드 참조와 람다)
      • 인터페이스의 디폴트 메소드

  • 자바 8은 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공한다.
    • 마치 데이터베이스의 질의 언어에서 표현식을 처리하는 것처럼.
    • 데이버테이스 질의 언어에서 고수준 언어로 원하는 동작을 표현 → 구현에서 최적의 저수준 실행 방법을 선택하는 방식으로 동작
    • 즉, 스트림을 이용하면 에러를 자주 일으키지도 않고, synchronized 키워드를 쓰지 않아도 된다.

→ 조금 다른 관점에서 보면, 결국 자바 8의 스트림 API 덕에 다른 두 가지 기능도 존재할 수 있음을 알 수 있다.


  • 자바 8 기법은 함수형 프로그래밍에서 위력을 발휘한다.
  • 본론에서는 먼저 왜 언어가 진화하는가와 관련한 내용을 다루고, 자바 8의 핵심 기능을 소개한다.
    • 1.1절 : 자바의 멀티코어 병렬성 쉽게 이용하기
    • 1.2절 : 자바 8에서 제공하는 코드를 메소드로 전달하는 기법
    • 1.3절 : 스트림 API가 어째서 강력한 도구인가
    • 1.4절 : 디폴트 메소드라는 새로운 자바 8의 기능을 어떻게 활용할 지
    • 1.5절 : JVM을 구성하는 자바 및 기타 언어에서 함수형 프로그래밍의 영향


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

  • 1960년대에 수천 개의 언어가 쏟아져 나오면서, 더 이상 진화하지 않은 기존 언어는 사장되었다.
  • 특정 분야에서 장점을 가진 언어는 다른 경쟁 언어를 도태시킨다.
    • 새로운 하나의 기능 때문에 기존 기능을 버리고 새로운 언어와 툴 체인으로 바꾼다는 것은 쉽지 않다.

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

  • 자바는 처음부터 많은 유용한 라이브러리를 포함하는 잘 설계된 객체지향 언어로 출발이 좋았다.
  • 처음부터 스레드와 락을 이용한 소소한 동시성도 지원했다.
  • 코드를 JVM 바이트 코드로 컴파일하는 특징 덕에 → 인터넷 애플릿 프로그램의 주요 언어가 되었다.

스트림 처리

  • 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다.
  • 스트림 API의 핵심은 기존에 한 번에 한 항목을 처리했지만, 이제는 아래와 같이 변화할 수 있게 한다.
    • 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다.
    • 또한 스트림 파이프라인을 이용해 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다.
    • 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.

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

  • 이는 코드 일부를 API로 전달하는 기능이다.
  • ex) 송장 ID를 정렬하기
    • 2013UK0002, 2014US0003, …
      • 앞에 4자리는 연도를, 2자리는 국가 코드를, 다음 4자리는 고객 ID라고 하자.

  • 우리가 지정하는 순서대로 자료를 정리하도록 sort 메소드에 명령을 할 수 있을까?
    • 자바 8 이전에는 메소드를 다른 메소드로 전달할 방법이 없었다.
    • 1장을 시작하면서 보여준 예제처럼 Comparator 객체를 sort에 넘겨주는 방법은 어떨까?
      • 코드가 복잡하고, 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않다.
    • 자바 8에서는 메소드를 다른 메소드의 인수로 넘겨주는 기능을 제공한다. → 동작 파라미터화

병렬성과 공유 가변 데이터

  • 이 개념은 ‘병렬성을 공짜로 얻을 수 있다’라는 말에서 시작된다.

    • 세상에 공짜는 없다는데, 병렬성을 얻는 대신 무엇을 포기해야 할까?
    • 스트림 메소드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다.
  • 스트림 메소드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전해야 실행되어야 한다.

    • 안전하게 실행할 수 있는 코드? → 공유된 가변 데이터에 접근하지 않아야 한다.
    • 이러한 함수를 순수 함수, 부작용 없는 함수, 상태 없는 함수라 부른다.
  • 자바 8의 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있다.

    • 다중 프로세싱 코어에서 synchronized 를 사용하면 생각보다 훨씬 큰 대가를 치러야 할 수 있다.

함수형 프로그래밍의 패러다임

  • 공유되지 않은 가변 데이터, 메소드
  • 함수 코드를 다른 메소드로 전달하는 기능

명령형 프로그래밍의 패러다임

  • 일련의 가변 상태로 프로그램을 정의

자바가 진화해야 하는 이유

  • 지금까지 자바는 진화해왔다.
    • 갑작스런 제네릭의 등장
      • List → List
    • 틀에 박힌 Iterator
      • for-each 루프의 등장

  • 하지만, 많은 이들은 자바의 변화에 이미 익숙해져 있고, 그것이 가져다주는 편리함을 누린다.

기존 값을 변화시키는 데 집중했던 고전적인 객체지형에서 벗어나, 함수형 프로그래밍으로 다가섰다는 것이 자바 8의 가장 큰 변화이다.


  • 함수형 프로그래밍에서는 우리가 하려는 작업이 최우선시되며, 그 작업을 어떻게 수행하는 지는 별개의 문제로 취급된다.
    • 극단적으로 생각하면 전통적인 객체지향 프로그래밍과는 완전 상극이다.
    • 자바 8에서 함수형 프로그래밍을 도입함으로써 두 가지 프로그래밍 패러다임의 장점을 모두 활용한다.


🔥 자바 함수

  • 프로그래밍 언어에서 함수라는 용어는 정적 메소드와 같은 의미로 사용된다.
  • 자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.
  • 자바 8에서는 함수를 새로운 값의 형식으로 추가했다.
    • 뒤에서 설명할 멀티코어에서 병렬 프로그래밍을 활용할 수 있는 스트림과 연계될 수 있도록 함수를 만들었다.
  • 자바 프로그램에서 조작할 수 있는 값을 생각해보자.
    • 기본값 (int, double …)
    • 객체 → new 또는 팩토리 메소드 또는 라이브러리 함수를 이용해 얻을 수 있다.

→ 왜 함수가 필요할까? 를 생각해보자.


  • 프로그래밍 언어의 핵심은 값을 바꾸는 것이다.
    • 전통적으로 이 값을 일급 값 또는 일급 시민으로 부른다.
  • 하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다.
    • 이런 전달할 수 없는 구조체(메소드, 클래스 같은)를 이급 시민이라고 한다.

하지만 이게 중요할까? (값 : 일급 시민 , 구조체 : 이급 시민)

중요하다!

런타임에 메소드를 전달할 수 있다면? = 메소드를 일급 시민으로 만든다면?
→ 프로그래밍에 굉장히 유용하게 활용할 수 있다!


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

  • 자바 8에서 메서드를 값으로 취급할 수 있는 기능은 스트림과 같은 다른 자바 8 기능을 토대로 제공한다.

  1. 메소드 참조

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

      /*
      	다행히 File 클래스는 이미 isHidden() 메소드를 제공한다.
      	아래 코드처럼 FileFilter 객체 내부에 위치한 isHidden의 결과를
      	File.listFiles() 메소드로 전달하는 방법으로 숨겨진 필터를 필터링 할 수 있다.
      */
      
      File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
      		public boolean accept(File file) {
      				return file.isHidden();
      		}
      });
    • 자바 8에서는 아래 코드처럼 간단하게 구현할 수 있다.

      File[] hiddenFiles = new File(".").listFiles(File::isHidden);
    • 메소드 참조(:: 이 메소드를 값으로 사용하라)를 이용해 더 이상 메소드는 이급 값이 아닌 일급 값이다.

      • 기존에 객체 참조(new로 인한 생성)를 이용해 객체를 주고 받았듯 똑같은 상황이다.

  1. 람다 : 익명 함수
    • 람다(또는 익명 함수)를 포함하여 함수도 값으로 취급할 수 있다.

코드 넘겨주기 : 예제

모든 예제는 한빛미디어 웹페이지에서 내려받을 수 있다.

  • 특정 학목을 선택해서 반환하는 동작을 필터(filter)라고 한다.

  • 필터 사용 예제

    • 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부터는 코드를 인수로 넘겨줄 수 있으므로, filter 메소드를 중복으로 구현할 필요가 없다.
      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;
      }
    • 따라서 아래와 같이 메소드를 호출할 수 있고, 메소드를 전달할 수 있음을 증명했다.
      //초록 사과 골라내기
      filterApples(inventory, Apple::isGreenApple);
      
      //무거운 사과 골라내기
      filterApples(inventory, Apple::isHeavyApple);
    • 위 코드에서 Predicate란?
      • 인수로 값을 받아 truefalse로 반환하는 함수를 predicate라고 한다.
      • Function<Apple, Boolean> 같이 구현할 수도 있지만, Predicate이 더 표준적이다.
        • 또한 boolean을 Boolean으로 변환하는 과정도 없어서 더 효율적이다!

메소드 전달에서 람다로

  • 메소드를 값으로 전달하는 것을 분명히 유용한 기능이나, 한 두번만 사용할 메서드를 정의하는건 귀찮다.
  • 자바 8에서는 이 문제를 람다식(또는 익명 함수)를 이용해 새롭게 수정할 수 있다.
  • 람다식 사용 예제
    //초록 사과 골라내기
    filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
    
    //무거운 사과 골라내기
    filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
  • 하지만, 람다가 몇 줄 이상으로 길어진다면(즉, 복잡한 동작을 수행한다면)?

    → 코드가 수행하는 일을 잘 설명하는 이름을 가진 메소드를 정의하고, 이전처럼 메소드 참조를 활용하자.

    코드의 명확성이 우선시 되어야 한다.

여기까지는 멀티코어 CPU가 자바 8 설계자들의 가정에 없을 때의 이야기였다.


//위에서 쓴 람다식 사용 예제
filterApples(inventory, (Apple a) -> a.getWeight() > 150 );

//새롭게 제시된 기능의 함수 원형
static <T> Collcetion<T> filter(Collection<T> c, Predicate<T> p);

//무거운 사과를 골라내도록 스트림 사용법
filter(inventory, (Apple a) -> a.getWeight > 150 );
  • 설계자들은 병렬성이라는 중요성 때문에 ‘람다식 사용 예제’같은 설계를 쓰지 않는다.
    • 대신 자바 8에서는 filter와 비슷한 동작을 수행하는 연산집합을 포함한 스트림 API를 제공한다.
    • 이외에도 컬렉션과 스트림 간에 변환할 수 있는 메소드(map, reduce 등)도 제공한다.

🔥 스트림

  • 거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용하지만, 모든 문제가 해결되는 것은 아니다.
  • 리스트에서 고가의 트랜잭션(=거래)만 필터링한 뒤, 통화별로 그룹화해서 결과를 내는 예제
    Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
    
    for (Transaction transaction : transactions) {         //트랜잭션 리스트 반복
    		if (transaction.getPrice() > 1000) {               //고가의 트랜잭션
    				Currency currency = transaction.getCurrency(); //통화 추출
    				
    				List<Transaction> transactionsForCurrency = 
    						transactionsByCurrencies.get(currency);
    				if (transactionsForCurrency == null) {         //해당 통화 그룹이 없으면 그룹 생성 후 추가
    						transactionsForCurrency = new ArrayList<>();
    						transactionsForCurrencies.put(currency, transactionsForCurrency);
    				}
    				
    				transactionsForCurrency.add(transaction);      //현재 탐색된 트랜잭션을 해당 통화 그룹에 추가
    		}
    }
  • 위 코드는 중첩된 제어 흐름이 많을 뿐더러 가독성도 좋지 않다. → 이를 스트림 API를 이용해보자.
    import static java.util.stream.Collectors.groupingBy;
    
    Map<Currency, List<Transaction>> transactionsByCurrencies =
    		transaction.stream()
    				.filter((Transaction t) -> t.getPrice() > 1000)  //고가의 트랜잭션 필터링
    				.collect(groupingBy(Transaction::getCurrency));  //통화로 그룹핑

스트림 API를 이용하면 컬렉션 API와는 상당히 다른 방식으로 데이터를 처리할 수 있다는 사실만 기억하자.


  • 가장 쉽게 와닿는 차이는 반복 과정에서 느낄 수 있다.
    • 컬렉션 : 반복 과정을 직접 처리
      • for-each문에서 각 요소를 반복하며 작업을 수행
      • 외부 반복
    • 스트림 : 반복 루프를 신경쓰지 않아도 내부에서 모든 데이터가 처리됨
      • 내부 반복
    • 이 내용을 개인적으로 정리한 링크를 첨부합니다.

  • 컬렉션을 이용했을 때 다른 문제는 거대한 자료를 처리할 때 생긴다.
    • 이는 현재 멀티 코어 CPU를 가진 우리의 컴퓨터에게는 적합하지 않은 처리 방식이다.
    • 따라서 스트림을 이용하여 각각의 코어에 일을 할당하여 병렬 처리를 하자.

멀티스레딩은 어렵다.

  • 이전 자바 버전에서 제공하는 스레드 API로 멀티스레딩 코드를 구현해서 병렬성을 이용하는 것은 쉽지 않다.
    • 각각의 스레드가 동시에 공유된 데이터에 접근하고, 데이터를 갱신할 수 있다.
    • 하지만, 스레드를 잘 제어하지 못하면 데이터를 손상시킬 수도 있다.

  • 자바 8은 스트림 API로 아래 두 가지 문제를 모두 해결한다.
    • ‘컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제’
    • ‘멀티코어 활용의 어려움’

  • 이전 사과 필터링 예제에서 반복되는 패턴이 많은 문제 → 라이브러리에서 반복되는 패턴을 제공한다면?
    • 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링하거나 (ex. 무게에 따른 사과 선택)
    • 데이터를 추출하거나 (ex. 리스트에서 각 사과의 무게 필드 추출)
    • 데이터를 그룹화하는 등의 기능 (ex. 숫자 리스트에서 홀수와 짝수로 그룹핑..등등)

  • 또한 위에서 제시한 동작들을 쉽게 병렬화할 수 있다면?

    • 포크-조인 프레임워크

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

    • 조금 이상하게 들릴 수 있지만, 컬렉션을 가장 빨리 필터링하는 방법?
      → 컬렉션을 스트림으로 바꾸고, 병렬 처리 후 다시 리스트로 복원하는 방법이다.


    • 예제 코드

      //순차 처리 방식 코드
      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());

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


디폴트 메소드와 자바 모듈

  • 요즘은 외부에서 만들어진 컴포넌트를 이용해 시스템을 구축하는 경향이 있다.
  • 자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공한다.
    • 모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며, 문서화와 모듈 확인 작업이 용이해졌다.
  • 또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메소드를 지원한다.
    • 디폴트 메소드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라,
    • 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능이다.

  • 예제 코드 - 자바 8 이전, 이후 비교
List<Apple> heavyApples1 = inventory.stream()
                    .filter((Apple) a) -> a.getWeight() > 150)
                    .collect(toList());
List<Apple> heavyApples2 = inventory.parallelStream()
                    .filter((Apple) a) -> a.getWeight() > 150)
                    .collect(toList());
  • 자바 8 이전 : List가 stream 메소드를 지원하지 않는다.
    • 따라서 Collection 인터페이스에 stream 메소드를 추가하고, ArrayList 클래스에서 구현해야한다.
  • 자바 8 이후 : 구현 클래스에서 구현하지 않아도 되는 메소드를 인터페이스에 추가할 수 있는 기능을 제공
    • 메소드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함된다. (=디폴트 메소드)

🌱 하나의 클래스에서 여러 인터페이스를 구현할 수 있다.
= 여러 인터페이스에 다중 디폴트 메소드가 존재할 수 있다
= 다중 상속이 허용된다??

→ 엄밈히 말하면 다중 상속은 아니지만, 어느 정도는 ‘그렇다’.
→ 9장에서 악명 높은 다이아몬드 상속 문제에 대해 다뤄보자.


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

함수형 프로그래밍의 핵심적인 두 아이디어

  1. 메서드와 람다를 일급값으로 사용하는 것.
  2. 가변 공유 상태가 없는 병렬 실행을 이용해 효율적이고 안전하게 함수나 메서드를 호출하는 것

  • 일반적인 함수형 언어도 프로그램을 돕는 여러 장치를 제공한다.
    • ex) 명시적으로 서술형의 데이터 형식을 이용해 null 을 회피하는 기법
  • 자바 8에서는 NullPointerException을 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다.
    • Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체다.
    • 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.
  • 또한 (구조적) 패턴 매칭 기법도 있다.
    • switch를 확장한 것으로, 데이터 형식 분류와 분석을 한 번에 수행할 수 있다.
  • 왜 자바의 switch문에는 문자열과 기본값만 이용할 수 있을까?
    • 함수형 언어는 보통 패턴 매칭을 포함한 다양한 데이터 형식을 switch에 사용할 수 있다.
    • 일반적으로 객체지향 설계에서 클래스 패밀리를 방문할 때 방문자 패턴을 이용해 각 객체를 방문한 다음 원하는 작업을 수행한다.
      • 패턴 매칭을 사용시 아래와 같은 에러를 검출할 수 있도록 한다.
        "Brakes 클래스는 Car 클래스를 구성하는 클래스 중 하나입니다. 
         Brakes를 어떻게 처리해야 할지 설정하지 않았습니다."
profile
백엔드 개발자로 등 따숩고 배 부르게 되는 그 날까지

2개의 댓글

comment-user-thumbnail
2022년 12월 10일

잘보고갑니당

답글 달기
comment-user-thumbnail
2022년 12월 11일

아직 안 읽어 봤는데 좋은 내용이 많은 책이네요 👏
잘 봤습니다.

답글 달기