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

김성혁·2022년 2월 14일
0

모던 자바 인 액션

목록 보기
1/7
post-thumbnail

자바 역사를 통틀어 가장 큰 변화가 자바 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));

자바 8에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공합니다.

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

  • 스트림 API 병렬 연산을 지원하는 스트림이라는 API를 제공합니다.
  • 메서드에 코드를 전달하는 기법 새롭고 간결한 방식으로 동작 파라미터화를 구현할 수 있습니다. 이는 함수형 프로그래밍에서 위력을 발휘한다.
  • 인터페이스의 디폴트 메서드

👨🏻‍💻 스트림 처리

스트림이란?

  • 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임
  • 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 출력 스트림으로 데이터를 한 개씩 기록합니다.
  • 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있습니다.

스트림 API

  • 자바 8에는 java.util.stream 패키지에 스트림 API가 추가되었습니다.
  • Stream는 T 형식으로 구성된 일련의 항목을 의미
  • 우리가 하려는 작업을 (데이터베이스 질의처럼) 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있게 되었습니다.
  • 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있습니다. 스레드라는 복잡한 작업을 사용하지 않으면서도 병렬성을 얻을 수 있습니다.

👨🏻‍💻 동작 파라미터화로 메서드에 코드 전달하기

동작 파라미터화란?

  • 코드 일부를 API로 전달하는 기능
  • 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공합니다.

스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초한다.

👨🏻‍💻 병렬성과 공유 가변 데이터

스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 합니다.

  • 공유된 가변 데이터에 접근하지 않아야 합니다.
    • 공유된 변수나 객체가 있으면 병렬성에 문제가 발생
    • 공유되지 않은 가변 데이터, 메서드, 함수 코드를 다른 메서드에 전달하는 두 가지 기능은 함수형 프로그래밍 패러다임의 핵심적인 사항

p. 47 synchronized는 시스템 성능에 악영향을 미친다.

→ 다중 프로세싱 코어에서 synchronized를 사용하면 (다중 처리 코어에서는 코드가 순차적으로 실행되어야 하므로 병렬이라는 목적을 무력화시키면서) 생각보다 훨씬 더 비싼 대가를 치러야 할 수 있다.

👨🏻‍💻 메서드와 람다를 일급 시민으로

자바 8에서 함수를 새로운 값의 형식으로 추가했습니다. (이급 시민을 일급 시민으로 바꿀 수 있는 기능)

프로그래밍 언어의 핵심은 값을 바꾸는 것입니다.

메서드 참조

디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정하자. 다행히 File 클래스는 이미 isHidden 메서드를 제공한다.

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

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

람다 : 익명 함수

  • 직접 메서드를 정의할 수도 있지만, 이용할 수 있는 편리한 클래스나 메서드가 없을 때 새로운 람다 문법을 이용하면 더 간결하게 코드를 구현할 수 있습니다.

  • 프레디케이트(predicate)란?
    - 인수로 값을 받아 true나 false를 반환하는 함수
    - 메서드가 p라는 이름의 프레디케이트 파라미터로 전달됨.

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

👨🏻‍💻 메서드 전달에서 람다로

재사용성이 없는 메서드는 익명 함수 또는 람다로 해결할 수 있습니다.

하지만 람다가 몇 줄 이상으로 길어진다면, 익명 람다 보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 참조를 활용하는 것이 바람직합니다. 코드의 명확성이 우선시되어야 합니다.


👨🏻‍💻 스트림

리스트에서 고가의 트랜잭션(거래)만 필터링한 다음에 통화로 결과를 그룹화해야 한다고 가정하자.

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);
		}
		transactionForCurrency.add(transaction);
	}
}		 

스트림 API 사용
import static java.util.stream.Collectors.groupingBy;

Map<Currency, List<Transaction>> transactionByCurrencies = 
	transactions.stream()
					.filter((Transaction t) -> t.getPrice() > 1000)
					.collect(groupingBy(Transaction::getCurrency));
  • 외부 반복: for each 루프를 이용한 각 요소의 반복 작업
  • 내부 반복: 스트림 API와 같이 라이브러리 내부에서 모든 데이터를 처리
  • 스트림 API를 이용하면 내부 반복을 통해 컬렉션 API와는 상당히 다른 방식으로 데이터를 처리할 수 있습니다.

멀티스레딩의 문제점

  • 멀티스레딩 환경에서 각각의 스레드는 동시에 공유된 데이터에 접근하고, 데이터를 갱신할 수 있습니다.
  • 결과적으로 스레드를 잘 제어하지 못하면 원치 않는 방식으로 데이터가 바뀔 수 있습니다.

스트림 API 사용

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

컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다. 스트림은 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심이다.


👨🏻‍💻 디폴트 메서드와 자바 모듈

자바의 변화 과정 속 개발자들이 겪는 어려움 중 하나는 기존 인터페이스의 변경이었습니다. 인터페이스를 업데이트하려면 해당 인터페이스를 구현하는 모든 클래스도 업데이트해야 하므로 불가능에 가까웠습니다. 이 문제를 해결하고자 나온 것이 디폴트 메서드입니다.

어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까?

  • 디폴트 메서드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능
  • 기존의 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있습니다.
  • default 키워드

그런데 고민해야 할 문제가 한 가지 있습니다. 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있다는 것은 결국 다중 상속이 허용된다는 의미일까?

👨🏻‍💻 요약

  • 함수형 프로그래밍의 핵심
    • 메서드와 람다를 일급값으로 사용하는 것
    • 가변 공유 상태가 없는 병렬 실행을 이용해서 효율적이고 안전하게 함수나 메서드를 호출

NullPointer 예외를 피할 수 있도록 도와주는 Optional 클래스의 등장

  • Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체

0개의 댓글