CHAP01 - Modern-Java-in-Action/Online-Study GitHub Wiki

1.1 주목할 만한 이야기

Java 8은 역사상 가장 큰 변화가 있었다. Java 9 (리액티브 프로그래밍인 병렬 실행기법 지원. RxJava)는 획기적이거나 생산성이 바뀔정도는 아니었고 자바 10에서는 type inference 관련해 약간의 변화가 있었다.

  • Reactive Programing이란? 케빈 유투브
    • 클라이언트-서버 동기식 구조가 대부분이었는데, 규모가 커짐에 따라 복잡한 비동기식 요청을 처리해야되는 상황이 왔다. 리액티브 프로그래밍은 이것을 효과적으로 처리하기 위해 도입되었다.
    • 정의: 변화의 전파와 데이터 흐름과 관련된 선언적 프로그래밍 패러다임.
      • 원래 자바는 imperative 프로그래밍 사용했었음.
      • imperative (명령형)과 declarative (선언적) 프로그래밍 차이는?
        • declarative: 추상화 하는것 (e.g., 스트림) (blog)

8버전 이전의 코드와 비교

//자바 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));
  • 가장 많이 사용되는 자바 버전

  • 자바 버전들 설명

    • JDK, J2EE, J2SE 차이가 뭐죠?
      • JDK - 기본 자바 kit, 클라이언트 사이드
      • J2EE - 서버 사이드
      • J2SE(=오늘날 Java SE) - Swing, Applets, etc
    • 자바 8 이전 - 코어들을 사용하기 위해 쓰레드를 잘 활용하라고 하는데, 실상 쓰레드를 다루는 일은 error-prone하고 어렵다.
    • 자바 5 - 쓰레드풀이나 동시적인 컬렉션들과 같은 블록들을 추가
    • 자바 7 - fork/join 프레임워크와 병렬성 등장으로 좀 더 실용적이지만 여전히 어렵.
    • 자바 8 부터 - 사용하기 쉬운 병렬성 등장
  • 가장 큰 변화를 가진 자바 8의 중요한 추가사항들

    • Streams API
      • 스트림을 사용함으로써, synchronized를 사용하지 않아도 된다.
    • Techniques for passing code to methods
      • behavior parameterization을 구현할 수 있고, 떠오르는 새로운 컴퓨터 아키텍처로인 함수형 프로그래밍에서 위력을 발휘한다.
    • Default methods in interface
      • 인터페이스 내부에서도 로직이 포함된 메소드를 선언 가능
      • 도입 이유: 하위 호환성

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

수백개의 프로그래밍 언어가 있고 살아남기 위해서는 진화가 필요하다. **C나 C++**의 경우는 오래됐고 프로그래밍 안정성이 부족하지만 runtime footprint가 작다는 강점이 있어 OS를 개발하거나 임베디드 시스템에서 아직도 많이 사용되고 있다. 그리고 Algol, COBOL, Pascal 같은 진화 하지 못한 언어들은 오늘날 살아남지 못하였다. Java가 익숙하지 않았던 테라바이트 단위의 빅데이터를 처리하기 위해 새로운 개념 도입도 필요하게 되었다 (예, 병렬프로세싱) . 즉, 살아남기 위해서는 진화가 필요하다.

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

자바는 캡슐화와 같은 객체지향 모델과 write-once and run-anywhere의 컨셉 덕분엔 입지를 발전해왔다.

이 밑으로 자바 8에서 제공하는 기능의 3가지 프로그래밍 개념을 자세히 배워보자.

1.2.2 스트림 프로세싱

  • 스트림 = 한 번에 하나씩 생성되는 일련의 데이터 항목
  • 자세한 부분은 4~7장에서 설명한다.
  • 유닉스 기반도 스트림 프로세싱하고 비슷
// 유닉스 명령어 파이프(|)를 통해 연결 
// 파일 두개를 합쳐 모든 문자를 소문자로 바꾸고 정렬후 뒤 3줄을 가져온다.
// 명령을 순차적이 아닌 cat, tr, sort, tail 각각 병렬로 처리한다.
cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
// 코드 설명: 두파일을 연결해서 스트림 생성 -> 문자를 번역 -> 행 정렬 -> 마지막 3줄
  • 또 다른 비슷한 사례: 자동차 조립 라인
  • 자바 스트림 특징: 스레드 이용한 복잡한 작업을 추가하지 않고도 병렬성을 얻을 수 있다.

1.2.3 동작 파라미터화(Behavior Parameterization)를 이용해 메서드에 코드 보내기

  • 자바 8 이전에는 메서드를 다른 메서드로 전달할 방법이 없었다.
  • 2, 3장에서 자세히 다룬다.

참고, 오늘날 서브시스템을 담는 큰시스템 디자인이 주로 사용되는데, 디폴트메소드와 모듈들을 도입해서 해결하였다.

1.2.4 병렬성(Parallelism)과 공유 가변 데이터 (Shared Mutable Data)

  • 안전하게 동시에 코드를 실행하기 위해서는 서로 공유된 데이터에 접근하지 않아야 한다.
  • 공유된 변수나 객체가 있으면 병렬성에 문제가 발생한다.
  • 접근할 수 없게 만든 함수를 pure, side-effect-free, stateless 함수라고 부른다.
  • 18, 19장에서 자세히 다룬다.

1.2.5 자바 진화의 필요성

  • 자바가 진화하면서...
  • 편리함 준다.
    • 제너릭이 나오면서, 리스트 유형 파악이 가능하고 컴파일을 할때 더많은 에러를 검출할 수 있는 편리함을 주었다.
  • (OOP의) 틀에 박힌 iterator 대신 (함수형의) for-each 루프를 사용할 수 있게 되었다.
  • 객체지향과 함수형 프로그래밍의 두가지 장점을 모두 활용할 수 있게 되었다.
  • 요약, 편리해짐.

1.3 자바에서의 함수

  • 프로그래밍에서 함수는 메서드 특히 static 메서드와 같은 의미로 사용된다. (이해 x)
  • 이에더해, 자바에서는 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다.
  • 함수와 메서드 용어적인 차이가 있다.

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

  • 기존 자바에서 메서드를 전달할 수 있는 방법은 없었다.
  • 메서드를 값으로 취급할 수 있는 기능은, 스트림 같은 다른 자바 8기능의 토대를 제공했다.
  • 8버전에서 메서드가 2급값이 아닌 1급 값이 되었다.

그 방법들을 확인해보자.

메서드 참조 예제 코드

// 8 버전 이전
// FileFilter를 방법이 없어, 인스턴스화하였다.
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});

// 8 버전 
// ::은 메소드 참조 
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
  • 여기선 넘겨지는 isHidden을 메서드가 아닌 함수라고 부른다.

1.3.2 코드 전달 예제

  • Predicate 사용으로 중복 코드가 사라지는 예제.
// 초록색 사과리스트만 가져올때  
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;
}

// 위같이 복붙하는 코드는 버그가 있다면 모든 코드 수정이 필요하게 된다.
// 아래와 같이 짤경우 추가가 용이하다. 
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);
}
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);

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 || RED.equals(a.getColor()) );

1.4 스트림

  • 스트림 API를 사용하면 컬렉션 API와는 상당히 다른 방식으로 데이터를 처리할 수 있다.
  • 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다. (internal iteration)
// 리스트에서 1000 이상 거래만 필터링한 다음 통화로 결과를 그룹화할 경우
// 많은 boilerplate를 사용하게 된다.
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<>();
            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));

1.4.1 멀티쓰레딩의 어려움

  • 멀티 스레딩 환경에서 각각의 스레드는 동시에 공유된 데이터에 접근하고 데이터를 갱신할 수 있었다. 이로써 스레드를 잘 제어하지 못하면 데이터 값이 이상한 방향으로 바뀔 수 있는 상황이 생긴다.
  • 스트림API를 사용하면서 반복적인 코드문제와 어려운 멀티코어 활용 문제를 해결하였다.
  • Forking Step - 두 CPU각 각각 앞뒤를 분담해 처리한다.
  • 컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점, 스트림은 데이터에 어떤 계산을 할것인지 묘사하는것에 중점을 둔다.
  • 컬렉션을 가장 빠르게 처리하는 방법 - 컬렉션을을 스트림으로 바꾸고 병렬로 처리한 다음, 리스트로 다시 복원한다.
  • 7장에서 병렬 데이터 처리와 성능 자세히 다룬다.
// 순차처리 방식 
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().filter((Apple a) -> a.getWeight() > 150)
    .collect(toList());

1.5 디폴트 메서드와(Default methods) 자바 모듈

  • 외부에서 만들어진 컴포넌트를 기반으로 많이 시스템을 구축한다.
  • 인터페이스 변경시, 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야 했다. (고통스러운 작업)
  • 이 문제를 디폴트 메서드가 해결해준다. (13장에서 자세히)
  • 자바 9의 모듈 시스템은 모듈을 정의하는 문법 제공한다.
    • JAR 같은 컴포넌트에 구조를 적용가능. (14장에서 자세히)
  • 미래에 프로그램이 쉽게 변화할 수 잇는 환경을 제공하는 기능이라 생각하면 된다.

사례로, 아래 코드는 List는 자바 8 이전에 stream이나 parallelStream 메서드를 지원하지 않아 컴파일이 되지 않는다. 이를 해결하기 위해, Collection에 위 2개를 default 메서드로 추가하였다.

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 인터페이스에 sort 디폴트 메서드가 추가되었다.

// 예시 코드 
default void sort(Comparator<? super E> c) {
    Collections.sort(this, c);
}

여러 인터페이스를 상속받을때 동일한 다중 디폴트 메서드가 존재할 수 있기에, 이것은 다중상속이 허용된다는 얘기인가? 어느정도 그렇다. 이 얘기는 9장에서 다이아몬드 상속 문제 피하는 방법에서 다룬다.

1.6 함수형 프로그래밍으로 부터온 좋은 생각들

  • 살펴본 함수형 핵심적 2가지 특징
    • 메서드와 람다를 1급값으로 사용.
    • 가변 공유 상태가 없는 병렬 실행을 이용해 안전하게 함수나 메서드 호출 가능.
    • 예를들어, Stream API는 이 2가지를 다 활용한다.
  • 하스켈과 같은 함수형 언어는 null을 회피하는 기능이 있다.
  • 널참조를 발명한것은 뼈아픈 실수 (Tony Hoare)
  • 이에 대응하기 위한, 자바의 선택 Optional (11장에서 자세히)
  • null에 대응하기위한 다른 방법 패턴매칭이 있는데, 자바에서는 아직 불완전 (19장에서 자세히)

정리

  • 자바 8의 핵심적인 추가는 흥미로운 새로운 컨셉들과 기능을 간결하고 효율적으로 작성하게 한다.
  • 8버전 이전에는 멀티코어 프로세서는 지원이 되지 않았다.
  • 함수는 일급 시민이다; 메서드가 어떻게 함수형 값으로 전달이 되었었고, 익명 함수(람다)가 어떻게 작성되었는지 기억하자.
  • 자바 8에서 스트림은 컬렉션의 많은 면모를 일반화하였다. 일반화하였지만, 스트림은 더 읽기 쉬운 코드를 제공하고 스트림의 요소들이 병렬로 처리되게 해준다.
  • 자바는, 큰 컴포넌트 기반의 프로그래밍이나 발전하는 시스템의 인터페이스를 잘 다루지 못하였다. 그러나 지금은 자바9에서 시스템을 구조화하기 위해 모듈을 명시할 수 있고, 모든 구현부 클래스들의 수정없이 인터페이스만 디폴트 메서드를 추가함 으로써 손쉽게변경이 가능하게 되었다.
  • 함수형 프로그래밍으로 부터 온 개념들이 널과 패턴매칭을 다룰 수 있게 도와주었다.
⚠️ **GitHub.com Fallback** ⚠️