※ 이 글은 '모던 자바 인 액션' 책을 참고하여 작성되었습니다. 해당 책을 먼저 읽고 이해하신 후에 이 글을 읽으시는 것을 추천합니다.
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이 sum 변수 값을 읽는다.
- 스레드 2가 sum 변수 값을 읽는다.
- 스레드 1이 sum 변수에 3을 더한다.
- 스레드 2가 sum 변수에 5를 더한다.
- 스레드 1이 sum 변수에 103을 저장한다.
- 스레드 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://hanbit.co.kr/support/supplement_survey.html?pcode=B4926602499