ch1. 자바 8, 9, 10, 11 무슨 일이 일어나고 있는가?

    1. 역사의 흐름은 무엇인가?

    Java8의 등장

    • 자연어에 더 가까운 방식의 코드
    Collections.sort(inventory, new Comparator<Apple>(){
    	public int compare(Apple a1, Apple a2){
        	return a1.getWeight().compareTo(a2.getWeight());
        }
    });
    // Java8
    inventory.sort(comparing(Apple::getWeight));
    • 하드웨어적 변화
      • 멀티코어 CPU의 대중화
      • 대부분의 자바 프로그램은 코어 중 하나만을 사용했음
        • 나머지 코어를 활용하려면 스레드를 이용해야하는데 에러가 발생하기 쉬움
      • Java8에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공함
        • 스트림 API
          • 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 API
          • 고수의 언어 → 최적의 저수준 실행 방법을 선택하는 방식으로 동작
          • 스트림을 이용하면 synchronized를 사용하지 않아도 됨
        • 메서드에 코드를 전달하는 기법
          • 메서드 참조와 람다
          • 새롭고 간결한 방식으로 동작 파라미터화를 구현할 수 있음
          • 함수형 프로그래밍에서 위력을 발휘함
        • 인터페이스의 디폴트 메소드

     

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

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

    • 시작: 잘 설계된 객체 지향 언어
      • 스레드, 락을 이용한 동시성 제어
      • 코드를 JVM 바이트 코드로 컴파일함
        • 모든 브라우저에서 가상 머신 코드를 지원하기 때문에 인터넷 애플릿 프로그램의 주요 언어가 됨
      • 임베디드 컴퓨팅 분야
    • '빅데이터'
      • 멀티코어 컴퓨터나 컴퓨팅 클러스터를 이용해서 빅테이터를 효과적으로 처리할 필요성이 커짐
      • 병렬 프로세싱을 활용해야하는데 지금까지의 자바로는 충분히 대응할 수 없었음

    2.2 스트림 처리

    • 스트림: 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임
      • ex) In Unix
        • cat file1 file 2 | tr "[A-Z]" "[a-z]" | sort | tail -3
        • file1, file2에 있는 단어를 모두 소문자로 바꾼 후 정렬, 뒤에서부터 3번째 행부터 출력
          • cat: 두 파일을 연결해서 스트림을 생성함
          • tr: 스트림의 문자를 번역함
          • sort: 스트림의 행을 정렬함
          • tail -3: 스트림의 마지막 3개 행을 제공
          • |: 명령 연결 가능
    • Java 8, java.util.stream 패키지에 스트림 API가 추가 됨
    • Stream<T>
      • T 형식으로 구성된 일련의 항목을 의미
      • 파이프라인을 구성했던 것처럼 스트림 API는 파이프라인을 만드는데 필요한 많은 메서드를 제공함
      • 데이터베이스 질의처럼 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있음
      • 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있음
      • 스레드를 사용하지 않으면서 공짜로 병렬성을 얻을 수 있음

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

    • 동작 파라미터화: 코드의 일부를 API로 전달하는 기능
    • ex) 2013UK0001, 2014US0002, ...  등의 형식을 갖는 송장ID가 있다고 가정함. 고객 ID 또는 국가 코드 순으로 정렬해야함
      • compareUsingCustomerID 메소드를 sort의 인수로 전달
    public int compareUsingCustomerID(String inv1, String inv2){
    	...
    }

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

    • 기존: synchronized를 이용해서 공유된 가변 데이터를 보호하는 규칙을 만들 수 있음
      • 다중 프로세싱 코어에서는 코드가 순차적으로 실행되어야하므로
      • synchronzied를 사용하면 병렬이라는 목적을 무력화 시키면서 시스템에 악영향을 미칠 수 있음
    • Java 8: 스트림을 이용해 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있음

     

    3. 자바 함수

    • 함수: method / static method / 수학적인 함수 (부작용을 일으키지 않는 함수)
    • 스트림과 연계될 수 있도록 함수를 새로운 값의 형식으로 추가

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

    • 일급값: 그 자체로 값이 될 수 있는 것들 (int, float, ...)
    • 메서드를 값으로 취급할 수 있음
    • 메서드 참조
      • :: , 이 메서드를 값으로 사용하라는 의미
      • 메서드는 코드를 포함하고 있으므로 코드를 마음대로 전달할 수 있음
      • ex) 디렉토리에서 모든 숨겨진 파일을  필터링함
    // Java8 이전
    File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    	public boolean accept(File file){
        	return file.isHidden();
        }
    });
    // Java8
    File[] hiddenFiles = new File(".").listFiles(File::isHidden);

    람다 : 익명 함수

    • 람다(익명 함수)를 값으로 취급할 수 있음
    • ex) (int x) -> x + 1
      • x라는 인수를 호출하면 x+1을 반환

    3.2 코드 넘겨주기: 예제

    // 초록 사과 필터
    public static boolean isGreenApple(Apple apple){
    	return GREEN.equals(apple.getColor());
    }
    public static boolean isHeavyApple(Apple apple){
    	return apple.getWeight() > 150; // ⓐ
    }
    
    // ⓐ를 명확하게 하기 위해 추가함 (java.util.function에서 임포트)
    public interface Predicate<T>{
    	boolean test(T t);
    }
    
    // 메서드가 p라는 이름의 Predicate 파라미터로 전달됨
    static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> 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);

    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()));

     

    4. 스트림

    • 거의 모든 자바 애플리케이션은 컬랙션을 만들고 활용함 
      • 많은 요소를 가진 목록들을 for-each문으로 순회하면 오랜 시간이 걸릴 수 있음
      • 서로 다른 CPU 코어에 작업을 각각 할당해서 처리 시간을 줄일 수 있다면 좋을 것임
    • Ex) 리스트에서 고가의 트랜잭션만 필터링한 다음에 통화로 결과를 그룹화
    // 그룹화된 트랜잭션을 더할 Map 생성
    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));
    • 스트림 API는 내부 반복을 이용함

    4.1 멀티스레딩은 어렵다

    • Java8
      • 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 해결
        • 기존의 컬렉션에서는 데이터를 처리할 때 반복되는 패턴이 너무 많았음
        • 라이브러리에서 반복되는 패턴을 제공 (필터링, 추출, 그룹화 등)
      • 멀티코어 활용 어려움 해결
        • 과정
          • 1) forking step: 처리해야할 task를 여러 CPU로 나누는 과정
          • 2) 각 CPU에서 task를 마침
          • 3) 그 결과를 합침
        • 스트림 API와 컬렉션 API는 비슷한 방식으로 동작함
          • 순차적인 데이터 항목 접근 방법을 제공
          • 컬렉션: 데이터를 어떻게 저장하고 접근할지에 중점을 둠
          • 스트림: 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둠, 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심
        • 컬렉션을 필터링하는 가장 빠른 방법: 컬렉션 --> 스트림 --> 병렬 처리 --> 리스트로 다시 복원
    // 순차 처리 방식의 코드
    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());
    • 자바의 병렬성과 공유되지 않은 가변 상태
      • 라이브러리에서 분할 처리: 큰 스트림을 작은 스트림으로 분할
      • filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있음. 상호작용하지 않는다는 것은 프로그래머 입장에서는 상당히 자연스러운 일임
      • 함수형 프로그래밍에서 함수형이란 '함수를 일급값으로 사용한다'는 의미도 있지만, 부가적으로 '프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다'라는 의미도 포함
    • 기존 인터페이스의 변경 → 디폴트 메서드로 해결
      • Collections.sort(list, comparator)은 이론적으로 list.sort(comparator)이 맞음

     

    5. 디폴트 메서드와 자바 모듈

    • 외부에서 만들어진 컴포넌트를 이용해 시스템을 구축하는 경향이 있음
    • 지금까지의 자바
      • 평범한 자바 패키지 집합을 포함하는 JAR 파일을 제공하는 것이 전부
      • 패키지의 인터페이스를 바꿔야하는 상황에서는 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야하므로 헬게이트 오픈
    • Java8
      • 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메소드 지원
    • Java9
      • 모듈 시스템
        • 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있음
        • 모듈 덕에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인 작업이 용이해짐

    디폴트 메소드

    • 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능
    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());
    • Java8: 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능 제공
      • 디폴트 메서드를 이용해 기존의 코드를 건드리지 않고 원래의 인터페이스 설계를 자유롭게 확장할 수 있음
      • Ex) List.sort 메소드 호출
    // List 인터페이스에 디폴트 메서드 정의가 추가되었음
    default void sort(Comparator<? super E> c){
    	Collections.sort(this, c);
    }
    • 다중 상속?
      • 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있음
      • 엄밀히 다중 상속은 아니지만 어느 정도는 비슷한 부분이 있음
      • 다이아몬드 상속 문제: 손자 객체에 할머니 객체가 2개 생기는 것

     

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

    • 자바에 적용된 함수형 프로그래밍의 핵심적인 두 아이디어
      • 메서드와 람다를 일급값으로 사용
      • 가변 공유 상태가 없는 병렬 실행을 이용해 효율적이고 안전하게 함수나 메서드를 호출
    • Optional<T> 클래스: NullPointer 예외를 피할 수 있도록 도와주는 클래스
      • 값을 가지거나 가지지 않을 수 있는 컨테이너 객체
      • 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있기 때문에 NullPointer 예외를 피할 수 있음
    • 패턴 매칭
      • if-then-else보다 패턴 매칭을 이용했을 때 더 정확한 비교를 구현할 수 있으나, Java 8에서는 지원하지 않음.
      • switch를 확장한 것으로 데이터 형식 분류와 분석을 한 번에 수행할 수 있음
      • 함수형 언어는 패턴 매칭을 포함한 다양한 데이터 형식을 switch에 사용할 수 있음
      • 예시 
        • f(0) = 1
        • f(n) = n * f(n-1) 그렇지 않으면
    // [Scala]
    // 트리로 구성된 수식을 단순화 하는 프로그램
    // expr: 수식 
    def simplifyExpression(expr: Expr): Expr = expr match {
    	case BinOp("+", e, Number(0)) => e // 0 추가
    	case BinOp("-", e, Number(0)) => e // 0 빼기
    	case BinOp("*", e, Number(0)) => e // 1로 곱하기
    	case BinOp("/", e, Number(0)) => e // 1로 나누기
        case _ => expr // 표현식을 단순화할 수 없음
    }

    댓글