프로그래밍 놀이터/안드로이드, Java

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

돼지왕 왕돼지 2019. 1. 5. 15:07
반응형

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

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




반응형