본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java8 In Action] #16 결론 그리고 자바의 미래

by 돼지왕왕돼지 2019. 1. 5.

[Java8 In Action] #16 결론 그리고 자바의 미래


Java8 In Action 내용을 보며 정리한 내용입니다.

정리자는 기존에 Java8 을 한차례 rough 하게 공부한 적이 있고, Kotlin 역시 공부한 적이 있습니다.

위의 prerequisite 가 있는 상태에서 추가적인 내용만 정리한 내용이므로, 제대로 공부를 하고 싶다면 책을 구매해서 보길 권장합니다!


allof, apply, bifunction, completablefuture, Empty, escape analysis, Filter, function, GC, generic polymorphism erasure model, ifpresent, invokedynamic, java8 in action, LAMBDA, map, optional, Predicate, primitive type generic, reified generic, reified model of generic polymorphism, test, thenCombine, thenCompose, unboxing, visitor pattern, 구체화된 제네릭, 동작 파라미터화, 디폴트 메서드, 람다, 멀티 코어, 메서드 레퍼런스, 병렬 처리, 불변성, 스트림, 언박싱, 자바 8, 자바의 미래, 제네릭 다형성 구체화 모델, 제네릭 다형성의 삭제 모델, 탈출 분석, 파이프라인, 패턴 매칭



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 인스턴스를 선언할 수 있다.

    람다를 처음 실행할 때만 변환과 결과 연결 작업이 실행되므로 추가적인 성능 비용이 들지 않는다. 즉, 두 번째 호출부터는 이전 호출에서 연결된 구현을 바로 이용할 수 있다.




댓글0