Language/Java

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

SooJae 2023. 3. 9. 20:12
반응형

모던 자바 인 액션 책

※ 이 글은 '모던 자바 인 액션' 책을 참고하여 작성되었습니다. 해당 책을 먼저 읽고 이해하신 후에 이 글을 읽으시는 것을 추천합니다.

 

1.1 역사의 흐름은 무엇인가 

자바 1.0 

  • 스레드와 스레드락
  • 메모리 모델 지원, 그러나 저수준 기능 활용이 힘들다

※ 메모리 모델이란 : https://parkcheolu.tistory.com/14

 

자바 5

  • 스레드 풀
  • 병렬 실행 컬렉션

※ 스레드 풀이란: https://jenkov.com/tutorials/java-concurrency/thread-pools.html

 

자바 7

  • 병렬 실행에 도움을 줄 수 있는 Fork / Join 프레임워크, 그러나 활용이 힘들다

※ Fork/Join Framework in Java: https://warpgate3.tistory.com/entry/ForkJoin-Framework-in-Java

※ Java Fork and Join using ForkJoinPool: https://jenkov.com/tutorials/java-util-concurrent/java-fork-and-join-forkjoinpool.html

 

자바 8

  • 스트림 API, 메서드 참조와 람다, 인터페이스의 디폴트 메서드
  • 자바 8 기법은 함수형 프로그래밍에서 위력을 발휘한다.

 

 

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

1.2.2 스트림 처리

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

리눅스의 스트림: https://love-every-moment.tistory.com/52

  • 자바 8 에는 java.util.stream 패키지에 스트림 API 가 추가되었다
  • 우리가 하려는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리 할 수 있다
  • 스레드라는 복잡한 작업을 사용하지 않으면서 공짜로 병렬성을 얻을 수 있다

 

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

  • 자바 8에서는 메서드를 다른 메서드의 인수로 넘겨주는(일급 객체) 기능을 제공하며, 이를 동작 파라미터화(behavior parameterization)라고 한다.

 

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

  • 공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드로 전달하는 두 가지 기능은 함수형 패러다임의 핵심적인 사항이다. (명령형 프로그래밍 패러다임에서는 일련의 가변 상태로 프로그램을 정의한다.)

 

1.3 자바 함수 

  • 프로그래밍 언어에서 함수(function)라는 용어는 메서드(method) 특히 정적 메서드(Static method)와 같은 의미로 사용된다. 자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다
  • 객체(객체의 참조)도 값이다. new 또는 팩토리 메서드 또는 라이브러리 함수를 이용해서 객체의 값을 얻을 수 있다. 객체 참조는 클래스의 인스턴스(Instance)를 가리킨다
  • 프로그래밍 언어의 핵심은 값을 바꾸는 것이고 이 값을 일급 값 (또는 시민) 이라고 부른다
  • 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값의 구조를 표현하는 데 도움될 수 있다.
  • 메서드, 클래스 등은 이급 자바 시민에 해당한다.
  • 만약 메서드와 클래스가 일급 시민으로 만들 수 있다면, 즉 런타임에 메서드를 전달할 수 있다면 프로그래밍에 유용하게 활용할 수 있다.(자바스크립트에서는 가능하지롱)

 

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

// 자바 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의 메서드 참조 (method reference) :: ('이 메서드를 값으로 사용하라'의 의미)를 이용해서 listFiles 에 직접 전달할 수 있다. 
  • 람다 : 익명함수 
    • 예를 들어 (int x) -> x + 1, 즉 'x 라는 인수로 호출하면 x + 1을 반환'하는 동작을 수행하도록 코드를 구현할 수 있다
    • 이용할 수 있는 편리한 클래스나 메서드가 없을 때 람다 문법을 이용해 더 간결하게 코드를 구현 가능하다

 

자바 8 이후 코드 예시

class Test {
	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.4 스트림 

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

하지만 리스트에서 고가의 트랜잭션만 필터링할 경우 다음 코드처럼 많은 기본코드를 구현해야한다.

class LegacyTest {
  private static void groupTransaction() {
    // 그룹화된 트랜잭션을 더할 Map 생성
    Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
    // 트랜잭션의 리스트를 반복
    for (Transaction transcation : 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.toList; 
class LegacyTest {
  private static void groupTransaction() {
    Map<Currency, List<Transaction>> transactionsByCurrencies = 
        transactions.stream()
            .filter((Transaction t) -> t.getPrice() > 1000) // 고가의 트랜잭션 필터링
            .collection(groupingBy(Transaction::getCurrency)); //통화로 그룹화함
  }
}

 

컬렉션

  • for-each루프를 이용해 반복 과정을 직접 처리함. -> 외부 반복
  • 기본적으로 단일 CPU만 이용하여 순차적으로 처리한다.

 

스트림

  • 라이브러리 내부에서 수행함. -> 내부 반복
  • 기본적으로 멀티 CPU를 이용하여 병렬로 처리한다.

 

1.4.1 멀티 스레딩은 어렵다.

  • 멀티 스레딩 모델은 순차적인 모델보다 다루기 어렵다.
  • 두 스레드가 적절하게 제어되지 않은 상황에서 공유된 sum 변수에 숫자를 더하면 다음과 같은 문제가 있다. (책의 그림을 보는 것을 추천)

 

구성 환경

스레드 1, 스레드 2, 변수 sum (값 100)

  1. 스레드 1이 sum 변수 값을 읽는다.
  2. 스레드 2가 sum 변수 값을 읽는다.
  3. 스레드 1이 sum 변수에 3을 더한다.
  4. 스레드 2가 sum 변수에 5를 더한다.
  5. 스레드 1이 sum 변수에 103을 저장한다.
  6. 스레드 2가 sum 변수에 105를 저장한다. 결과 : 108이 아닌 105가 저장된다.

 

  • 자바 8은 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 그리고 멀티코어 활용 어려움을 해결하기 위해 스트림 API를 도입했다.

 

자바의 병렬성과 공유되지 않은 가변 상태

  • 흔히 자바의 병렬성은 어렵고, synchronized는 쉽게 에러를 일으킨다고 생각한다.
  • 자바 8은 이를 해결하기 위해 두가지 방식을 사용한다.
    • 라이브러리에서 분할 처리를 한다. 큰 스트림을 병렬 처리할 수 있도록 작은 스트림으로 분할한다.
    • filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용하지 않는다면 가변 공유 객체를 통해 병렬성을 누릴 수 있다.
      -> 여러 스레드가 동시에 가변 객체에 접근하더라도, 메서드가 상호작용하지 않기 때문에 안전하지만, 만약 전달되는 메서드가 공유되는 가변 객체를 변경한다면 문제가 발생한다. 이런 경우에는 스레드 동기화 기법을 사용하여 해결해야 한다.
  • 함수형 프로그래밍에서 함수형이란 '함수를 일급 값으로 사용한다.'는 의미를 가지고 있지만 부가적으로 '프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다' 라는 의미도 포함한다.

 

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

  • 자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다.
  • 모듈 덕분에 JAR 같은 컴포넌트 구조를 적용할 수 있다.
  • 문서화와 모듈 확인 작업이 용이해졌다.

※ 자바 9 모듈이란: https://mslim8803.tistory.com/39

 

  • 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.
  • 디폴트 메서드는 특정 프로그램을 구현하는데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 것이다.
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());

 

  • 자바 8 이전에는 Stream()이나 parallelStream() 메서드를 지원하지 않았다.
  • 이 기능을 추가하려면 Collection 인터페이스에 해당 메서드를 추가하고 ArrayList 등과 같은 구현체에 메서드를 구현해야 했다.
  • 하지만 이미 컬렉션 API의 인터페이스를 구현하는 많은 컬렉션 프레임워크가 존재했다. 인터페이스에 새로운 메서드를 추가한다면 인터페이스를 구현하는 모든 클래스는 새로 추가된 메서드를 구현해야 했다.
  • 자바 8은 클래스에서 구현하지 않아도 되도록 메서드를 인터페이스에 추가했다. 이를 디폴트 메서드라고 부른다.

※ 인터페이스 vs 추상 클래스:https://yaboong.github.io/java/2018/09/25/interface-vs-abstract-in-java8/

※ 다이아몬드 상속 문제: https://siyoon210.tistory.com/125

 

1.6 함수형 프로그래밍에서 가져온 다른 유용한 아이디어함수형 언어도 프로그램을 돕는 여러 장치를 제공한다.

  • 일례로 명시적으로 서술형의 데이터를 이용해 null을 회피하는 기법이 있다. 자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다. Optional<T>는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.

※ null이 왜 나쁜가: https://www.mimul.com/blog/why-null-is-bad/

 

  • 구조적 패턴 매칭 기법도 있다. 자바에서는 if-then-else나 switch문을 이용하지만 다른 언어에서는 패턴 매칭으로 더 정확한 비교를 구현할 수 있다는 사실을 증명했다.
  • 아쉽게도 자바 8은 패턴 매칭을 완벽하게 지원하지 않는다.

※ (구조적) 패턴 매칭: https://0391kjy.tistory.com/8

 

 


REFERENCE

모던 자바 인 액션 - https://search.shopping.naver.com/book/search?bookTabType=ALL&pageIndex=1&pageSize=40&query=%EB%AA%A8%EB%8D%98%20%EC%9E%90%EB%B0%94%20%EC%9D%B8%20%EC%95%A1%EC%85%98&sort=REL 

 

모던 자바 인 액션 소스 코드 - https://hanbit.co.kr/support/supplement_survey.html?pcode=B4926602499

반응형