자바/모던 자바 인 액션

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

Rudtjs 2022. 8. 25. 08:26

 

 

 

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

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

자바 8이 등장하기 이전에는 나머지 코어를 활용하려면 스레드를 사용했다. 하지만 스레드를 사용하면 관리하기 어렵고 많은 문제가 발생할 수 있었다. 자바는 이러한 병렬 실행 환경을 쉽게 관리하고 에러가 덜 발생하는 방향으로 진화하려고 노력했다.

  • 자바 1.0 : 스레드, 락, 메모리 모델
  • 자바  5 : 스레드 풀, 병렬 실행 컬렉션
  • 자바 7 : 포크/조인 프레임워크 

 

이렇게 많은 프레임워크가 추가됐지만, 개발의 활용하기는 쉽지 않았다.

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

  • 자바 8 : 스트림 API, 메서드에 코드를 전달하는 기법, 인터페이스의 디폴트 메서드 

 

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

기존 언어가 재빠르게 진화하지 않고 특정 분야의 장점마저 잃는다면  자연스럽게 다른 언어를 선택하고 기존 언어는 도태될 것이다. 

 

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

 

하지만 멀티코어 컴퓨터나 컴퓨팅 클러스터를 이용해서 빅 데이터를 효과적으로 처리할 필요성이 커졌다. 즉, 병렬 프로세싱을 활용해야 하는데 자바 8 이전 버전으로는 대응할 수 없었다.

 

이러한 문제를 해결하기 위해 자바 8에서는 완전히 새로운 개념과 기술들을 도입하여 현재 시장에서 요구하는 기능을 효과적으로 제공한다. 

 

 

1.2.1 스트림 처리

자바 8에서는 java.util.stream 패키지에 스트림 API가 추가되었다. 스트림 API의 핵심은 한 번에 한 항목씩 처리했던 작업을 추상화하여 일련의 스트림으로 만들어 처리하는 것이다.

 

스트림이란? 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 한 개씩 기록한다. 

 

유닉스의 명령어로 예를 들면

cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
  1. cat명령어로 file1, file2를 연결해서 스트림을 생성
  2. tr은 스트림의 문자를 번역 
  3. sort는 스트림의 행을 정렬
  4. tail -3은 스트림의 마지막 3개의 행을 제공

자동차 공장으로 비유하면 자동차라는 스트림을 처리하는데, 각 cat, tr, sort, tail이라는 작업장에서 처리를 한다는 것이다. 각 작업장에서는 자동차를 받아서 수리한 다음에, 다음 작업장에서 다른 작업을 처리할 수 있도록 넘겨준다. 자동차를 물리적인 순서로 한 개씩 운반하지만 각각의 작업장에서는 동시에 작업을 처리한다.

 

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

자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.

자바 8 이전에서는 메서드를 다른 메서드로 전달할 방법이 없었기 때문에 Comparator 객체를 만들어서 넘겨주었다. 하지만 이 방법은 너무 복잡하고 기존 동작을 단순하게 재활용한다는 측면에서도 맞지 않았다.

 

자바 8에서는 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공하는데, 이 기능을 동작 파라미터화라고 부른다.

 

 

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

세 번째 프로그래밍의 개념은 "병렬성을 얻는 대신 무엇을 포기해야 할까?"이다. 

보통 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터에 접근하지 않아야 한다. 하지만 공유된 변수나 객체가 있으면 병렬성에 문제가 발생한다.

 

자바 8 스트림을 이용하면 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있다. 자세한 설명은 뒤에 가서 하겠다.

 

 

1.3 자바 함수

프로그래밍 언어에서 함수(function)이라는 용어는 메서드(method) 특히 정적 메서드와 같은 의미로 사용한다.

 

자바 8 에서는 스트림과 연계될 수 있도록 함수를 만들었다. 어떻게 값을 사용하는지 알아보자.

  • int: 42, double: 3.14 등 기본값
  • new 또는 팩토리 메서드 또는 라이브러리 함수를 이용해 얻을 수 있는 객체 값

이런 식으로 객체를 얻을 수 있는데 함수는 왜 필요할까? 

프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 프로그래밍 언어에서는 이 값을 일급 값 또는 시민이라고 부른다.

 

자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스)가 값의 구조를 표현하는데 도움이 될 수 있다. 하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다. 이렇게 전달할 수 없는 구조체는 이 급 시민이라고 한다.

 

이 말을 꺼낸 이유는 이급 시민을 일급 시민으로 즉 런타임에 메서드를 전달할 수 있다면? 자바 8에서는 이 기능을 추가하여 프로그래밍을 더욱더 유용하게 바꿨다.

 

 

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

자바 8은 메서드를 값으로 취급할 수 있게 설계하였다.

 

1. 메서드 참조

디렉터리에서 모든 숨겨진 파일을 필터링한다고 가정해보자.

File[] hiddenFiles = new File(".").listFiles(new FileFilter()) {
	public boolean accept(File file) {
    	return file.isHidden();
    }
});

 

File 클래스에 이미 is Hidden이라는 메서드가 있지만 굳이 FileFilter로 감싼 다음에 FileFilter를 인스턴스화 했다.

 

하지만 자바 8에서는 다음처럼 바뀌었다.

 

File[] hiddenFiles = new File(".").listFiles(File::isHidden);

 

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

기존에 객체 참조를 이용해서 객체를 주고받았던 것처럼 자바 8에서는 메서드 참조를 만들어 전달할 수 있게 되었다.

 

람다 : 익명 함수

기명 메서드를 일급 값으로 취급할 뿐 아니라 람다를 포함하여 함수도 값으로 취급할 수 있다.

람다는 (int) -> x + 1, 즉  x인수를 호출하면 x + 1을 반환하는 동작이다. 람다식을 이용하면 더 간결하게 코드를 구현할 수 있다.

 

 

1.3.2 메서드 전달에서 람다로

복잡하지 않고 한 두 번 사용할 기능이라면 메서드를 정의하는 것이 아닌 람다를 이용한다.

 

fileterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );

fileterApples(inventory, (Apple a) -> a.getWeight() > 150 );

filterApples(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()));

 

1.4 스트림

거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다. 

컬렉션 API는 for-each 루프를 이용해서 각 반복 작업을 직접 처리해야 했다. 

 

스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다. (4장에서 자세히)

 

 

 

1.4.1 멀티스레딩은 어렵다.

스레드를 잘 제어하지 못하면 원치 않는 방식으로 데이터가 바뀔 수 있다.

하지만 스트림 API로  발생하는 문제점 두 가지를 해결했다.

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

자주 반복되는 패턴으로 주어진 조건에 따라 데이터를 필터링하거나, 데이터를 추출하거나, 데이터를 그룹화하는 기능들 등의 반복되는 패턴을 제공한다. 또한 이러한 동작들을 쉽게 병렬화할 수 있다.

 

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

 

 

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

자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화의 모듈 확인 작업이 용이해졌다. 또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.

 

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

자바 8은 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다. 메서드 본문은 클래스 구현이 아니라 인터페이스의 일부로 포함된다.(이를 디폴트 메서드라고 부른다.)

 

예를 들어 List에 직접 sort 메서드를 호출한다고 하면

default void sort(Comparator<? super E> C) {
  Collections.sort(this, c);
}

List를 구현하는 모든 클래스의 sort를 구현하지 않아도 된다.

 

 

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

함수형 언어에는 명시적으로 서술형의 데이터를 이용해 null을 회피하는 기법이 있다.

 

자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다. Optional<T>는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있다.

 

또한 패턴 매칭 기법도 있다. 패턴 매칭은 수학에서 다음 예제처럼 사용한다.

f(0) = 1
f(n) = n*f(n-1)

자바에서는 이를 if-then-else처럼 사용한다.

 

 

마치며

자바 8은 프로그램을 효과적으로 만들기 위해서 간결하게 구현할 수 있는 새로운 개념과 기능을 제공했다. 메서드를 어떻게 함수형 값으로 넘겨주는지, 람다, 스트림 등 이 기능들을 책을 읽고 글을 써보면서 알아가 보겠습니다.