[Java8 In Action] #16 결론 그리고 자바의 미래 |
Java8 In Action 내용을 보며 정리한 내용입니다.
정리자는 기존에 Java8 을 한차례 rough 하게 공부한 적이 있고, Kotlin 역시 공부한 적이 있습니다.
위의 prerequisite 가 있는 상태에서 추가적인 내용만 정리한 내용이므로, 제대로 공부를 하고 싶다면 책을 구매해서 보길 권장합니다!
16.1. 자바 8의 기능 리뷰
-
자바 8에 큰 변화가 생긴 이유는 두 가지 추세 때문이다.
한 가지 추세는 멀티코어 프로세서의 파워를 충분히 활용해야 한다는 것. 무어의 법칙에 따라 실리콘 기술이 발전하면서 개별 CPU 코어의 속도가 빨라지고 있다. 즉, 코드를 병렬로 실행해야 더 빠르게 코드를 실행할 수 있다.
데이터 소스를 이용해서 주어진 조건과 일치하는 모든 데이터를 추출하고, 결과에 어떤 연산을 적용하는 등 선언형으로 데이터를 처리하는 방식, 즉 간결하게 데이터 컬렉션을 다루는 추세다. 간결하게 데이터 컬렉션을 처리하려면 불변값을 생산할 수 있는 불변 객체와 불변 컬렉션이 필요하다.
필드를 변화하고 반복자를 적용하는 기존의 객체지향, 명령형 언어로는 이러한 추세를 만족시키기 어렵다.
한 코어에서 데이터를 변화시키고 다른 코어에서 이 데이터를 읽으려면 비싼 비용을 치러야 할 뿐 아니라 잠금 관련 버그도 많이 발생한다.
함수형 프로그래밍을 사용하면 이 두 가지 추세를 모두 달성할 수 있다.
16.1.1. 동작 파라미터화(람다와 메서드 레퍼런스)
-
함수형 프로그래밍에서 지원하는 메서드로 코드 블록을 전달하는 기법을 자바 8에서도 제공한다.
람다 코드를 전달할 수도 있고, 메서드 레퍼런스를 전달할 수도 있다.
메서드로 전달되는 값은 Function<T, R>, Predicate<T>, BiFunction<T, U, R> 등의 형식을 가지며 메서드를 수신한 코드에서는 apply, test 등의 메서드로 코드를 실행할 수 있다.
16.1.2. 스트림
-
스트림 API 는 연산을 파이프라인이라는 게으른 형식의 연산으로 구성한다.
그리고 한 번의 탐색으로 파이프라인의 모든 연산을 수행한다.
큰 데이터집합일수록 스트림의 데이터 처리 방식이 효율적이며, 또한 메모리 캐시 등의 관점에서도 커다란 데이터 집합일수록 탐색 횟수를 최소화하는 것이 아주 중요하다.
-
멀티코어 CPU 를 활용해서 병렬로 요소를 처리하는 기능도 매우 중요하다.
스트림의 parallel 메서드는 스트림을 병렬로 처리하도록 지정하는 역할을 한다.
상태 변화는 병렬성의 가장 큰 걸림돌이다.
따라서 함수형 개념은 map, filter 등의 연산을 활용하는 스트림의 병렬 처리의 핵심으로 자리 잡았다.
16.1.3. CompletableFuture
-
자바5부터 Future 인터페이스를 제공한다.
Future 를 이용하면 여러 작업이 동시에 실행될 수 있도록 다른 스레드나 코어로 작업을 할당할 수 있다. ( 즉 멀티코어를 활용할 수 있다. )
다른 작업을 생성한 기존 작업에서 결과가 필요할 때는 get 메서드를 호출해서 생성된 Future 가 완료(즉, 결과값을 계산) 할 때까지 기다릴 수 있다.
-
CompletableFuture 는 Future 와 관련된 공통 디자인 패턴을 함수형 프로그래밍으로 간결하게 표현할 수 있도록 thenCompose, thenCombine, allOf 등을 제공한다.
따라서 명령형에서 발생하는 불필요한 코드를 피할 수 있다.
16.1.4. Optional
-
T 형식의 값을 반환하거나 값이 없음을 의미하는 Optional.empty 라는 정적 메서드를 반환할 수 있는 Optional<T> 클래스를 제공한다.
-
map, filter, ifPresent 등이 제공되어 스트림 클래스가 제공하는 것과 비슷한 동작으로 계산을 연결할 때 사용할 수 있다.
또한 값이 없는 상황을 사용자 코드에서 확인하는 것이 아니라 라이브러리에서 확인할 수 있다.
값을 내부적으로 검사하는 것은 시스템 라이브러리가 내부 반복을 하느냐 아니면 외부 반복을 하느냐와 같은 의미를 갖는다.
16.1.5. 디폴트 메서드
-
인터페이스에 새로운 기능을 추가했을 떄 기존의 모든 고객(인터페이스를 구현하는 클래스)이 새로 추가된 기능을 구현하지 않을 수 있게 되었다.
16.2. 자바의 미래
16.2.1. 컬렉션
16.2.2. 형식 시스템 개선
-
선언 사이트 변종
List<? extends Number> numbers = new ArrayList<Integer>();
제네릭의 서브 형식을 와일드카드로 지정할 수 있는 유연성을 주는 것이다.
-
지역 변수 형식 추론(local variable type inference)
var myMap = new HashMap<String, List<String>>();
위와 같은 녀석을 말한다.
16.2.3. 패턴 매칭
-
보통 함수형 언어는 switch 를 개선한 기능인 패턴 매칭을 제공한다.
패턴 매칭을 이용하면 ‘이 값이 주어진 클래스의 인스턴스인가?’ 라는 질문을 직접 할 수 있고, 객체의 필드가 어떤 값을 가지고 있는지도 재귀적으로 물을 수 있다.
-
전통적인 객체지향 디자인에서는 switch 문 대신 방문자 패턴(visitor pattern)등을 사용할 것을 권장한다.
방문자 패턴에서는 데이터 형식에 종속된 제어 흐름이 switch 가 아닌 메서드에 의해 결정된다.
16.2.4. 풍부한 형식의 제네릭
-
구체화된 제네릭
자바5에서 제네릭을 소개했을 때 제네릭이 기존 JVM 과 호환성을 유지해야 했다.
결과적으로 ArrayList<String> 이나 ArrayList<Integer> 모두 런타임 표현이 같게 되었다.
이를 제네릭 다형성의 삭제 모델(generic polymorphism erasure model) 이라 한다.
이 때문에 약간의 런타임 비용을 지불하게 되었으며 제네릭 형식의 파라미터로 객체만 사용할 수 있게 되었다.
-
GC 때문에 generic 에 primitive type 이 들어갈 수 없다.
가비지 컬렉션이 필드가 레퍼런스인지 기본형인지 알 수 있도록 충분한 형식 정보를 런타임에 유지해야 ArrayList<int> 같은 녀석들을 쓸 수 있는데..
이렇게 형식 유지하는 것을 제네릭 다형성 구체화 모델 ( reified model of generic polymorphism ) 또는 구체화된 제네릭 ( reified generic ) 이라고 부른다.
구체화란 암묵적인 어떤 것을 명시적으로 바꾼다는 의미이다.
-
제네릭이 함수 형식에 제공하는 문법적 유연성
-
기본형 특화와 제네릭
16.2.5. 더 근본적인 불변성 지원
16.2.6. 값 형식
-
컴파일러가 Integer 와 int 를 같은 값으로 취급할 수는 없을까?
탈출 분석(escape analysis), 즉 언박싱 동작이 괜찮은지 결정하는 컴파일러 최적화 기법이 있지만 이는 자바 1.1 이후에 제공되는 객체에만 적용된다.
-
변수 형식: 모든 것을 기본형이나 객체형으로 양분하지 않는다.
값 형식(value type)은 불변이며 레퍼런스 식별자를 포함하지 않는다.
기본형값은 넓은 의미에서 값 형식의 일종이다.
값 형식에서는 하드웨어가 비트 단위로 비교해서 int 가 같은지 검사하는 것처럼 == 도 기본적으로 요소 단위의 비교로 값이 같은지 확인한다.
C# 의 struct 와 비슷하다.
값 형식에는 레퍼런스 식별자가 없으므로 저장 공간을 적게 차지한다.
값 형식은 데이터 접근뿐만 아니라 하드웨어 캐시 활용에도 좋은 성능을 제공할 가능성이 크다.
값 형식에는 참조 식별자가 없으므로 컴파일러가 자유롭게 값 형식을 박싱하거나 언박싱할 수 있다.
한 함수에서 complex 를 다른 함수의 인수로 전달하면 컴파일러가 자연스럽게 두 개의 double 로 전달할 것이다.
하지만 더 큰 값 형식을 인수로 전달하면 컴파일러가 인수를 박싱한 다음에 이러한 변환을 눈치 채지 못할 만큼 자연스럽게 변환해서 레퍼런스로 사용자에게 전달할 수 있다.
이미 비슷한 기법이 C# 에 적용되어 있다.
현재 자바에 값 형식을 추가하는 논의가 진행 중이다.
-
박싱, 제네릭, 값 형식: 상호 의존 문제
16.3. 결론
-
자바 역사상 가장 큰 변화가 자바 8에 적용되었다.
그래도 자바 8만큼 큰 변화를 꼽으라면 10년 전에 자바 5에서 제네릭을 선택한 것이다.
자바 8은 멋진 변화의 바람을 일으켰다. 진짜 변화는 이제부터다!
기타
-
JVM 의 동적 형식 언어를 지원할 수 있도록 JDK7 에 invokedynamic 이라는 명령어가 추가되었다.
invokedynamic 은 메서드를 호출할 때 더 깊은 수준의 재전송과 동적 언어에 의존하는 로직이 대상 호출을 결정할 수 있는 기능을 제공한다.
def add(a,b) { a + b }
실제 호출한 메서드를 결정하는 언어 종속적 로직을 구현하는 부트스트랩 메서드의 형태로 구성된다.
부트스트랩 메서드는 연결된 호출 사이트(call site)를 반환한다.
두 개의 int 로 add 메서드를 호출하면 이후로 이러지는 호출에도 두 개의 int 가 전달된다.
결과적으로 매 호출마다 호출할 메서드를 다시 찾을 필요가 없다.
호출 사이트는 언제 호출 연결을 다시 계산해야 하는지 정의하는 로직을 포함할 수 있다.
invokedynamic 으로 람다 표현식을 바이트코드로 변환하는 작업을 런타임까지 고의로 지연했다.
즉, 이 같은 방식으로 invokedynamic 을 사용해서 람다 표현식을 구현하는 코드의 생성을 런타임으로 미룰 수 있다.
이런 설계로 다음의 장점들을 얻게 된다.
람다 표현식의 바디를 바이트코드로 변환하는 작업이 독립적으로 유지된다. 따라서 변환작업이 동적으로 바뀌거나 나중에 JVM 구현에서 이를 더 최적화하거나 변환 작업을 고칠 수 있다. 변환 작업은 독립적이므로 바이트코드의 과거버전 호환성을 염려할 필요가 없다.
람다 덕분에 추가적인 필드나 정적 초기자 등의 오버헤드가 사라진다.
상태 없는(캡처하지 않는) 람다에서 람다 객체 인스턴스를 만들고, 캐시하고, 같은 결과를 반환할 수 있다. 자바 8 이전에도 사람들은 이런 방식을 사용했다. 예를 들어 정적 final 변수에 특정 Comparator 인스턴스를 선언할 수 있다.
람다를 처음 실행할 때만 변환과 결과 연결 작업이 실행되므로 추가적인 성능 비용이 들지 않는다. 즉, 두 번째 호출부터는 이전 호출에서 연결된 구현을 바로 이용할 수 있다.
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] 언어 변경(language change)했을 때 동작 실험 (0) | 2019.01.08 |
---|---|
[android] Camera2 API 를 알아보자 (4) | 2019.01.06 |
[Java8 In Action] #15 OOP 와 FP 의 조합 : 자바8과 스칼라 비교 (0) | 2019.01.04 |
[Java8 In Action] #14 함수형 프로그래밍 기법 (0) | 2019.01.03 |
[Java8 In Action] #13 함수형 관점으로 생각하기 (0) | 2019.01.02 |
댓글