해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
1.1 왜 아직도 자바는 변화하는가
1.1.2 스트림 처리
- 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모음이다.
- 간단한 예로 자동차 생산 공장은 여러 자동차로 구성된 스트림을 처리한다.
- 각각의 작업장에서 자동차를 받아서 수리.
- 다음 작업장에서 다른 작업을 처리할 수 있도록 넘겨준다.
- 조립라인은 자동차를 물리적인 순서로 한 개씩 운반하지만 각각의 작업장에서는 동시에 작업을 처리한다.
- 기존에는 한 번에 한 항목을 처리 했지만 자바 8애서는 고수준으로 추상화해서 일련의 스트림으로 만들어 처리한다.
- 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당하는 부가적인 이득이 있고 스레드라는 복잡한 작업을 사용하지 않고 병렬성을 얻을 수 있다.
1.1.3 동작 파라미터화로 메서드에 코드 전달하기
- 자바 8에서는 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공한다. 이러한 기능을 이론적으로 동작 파라미터화(behavior parameterization)이라 한다.
1.1.4 병렬성과 공유 가변 데이터
스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 한다.
이와 같이 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터(shared mutable data)에 접근 하지 않아야 한다. 이런 함수를
- 순수(pure)함수
- 부작용 없는(side-effect-free)함수
- 상태 없는(stateless)함수
라 부른다.
함수형 프로그래밍 패러다임의 핵심적인 사항으로 공유되지 않는 가변 데이터(no shared mutable data), 메서드와 함수 코드를 다른 메서드로 전달하는 기능이다.
명령형 프로그래밍(imperative programming)패러다임은 일련의 가변 상태로 프로그램을 정의한다. 공유되지 않는 가변 데이터 요구사항을 준수하는 메서드는 인수를 결과로 변환하는 동작만 수행한다. 즉, 수학적인 함수처럼 정해진 기능만 수행하며 다른 부작용은 일으키지 않는다.
1.2 자바 함수
- 프로그래밍 언어에서 함수(function)라는 용어는 메서드 특히 정적 메서드(static method)와 같은의미로 사용. 자바의 함수는 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미.
- 자바 8에서는 함수를 새로운 값의 형식으로 추가. 멀티코어에서 병렬 프로그래밍을 활용할 수 있는 스트림과 연계될 수 있도록 함수를 만들었다.
- 프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 이 값을 일급 값 혹은 일급 시민이라고 부른다
예) 자바 일급 시민
- int 형식, double 형식
- new 또는 팩토리 메서드, 라이브러리 함수를 이용한 객체의 값
- 객체 레퍼런스
- 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스)가 값의 구조를 표현하는데 도움은 되지만 모든 구조체를 자유롭게 전달할 수 없다. 그 자체로 값이 될 수 없기 때문이다. 이렇게 전달할 수 없는 구조체는 이급 시민이다.
예) 자바 이급 시민
- 클래스
- 메서드
- 따라서 자바 8 설계자들은 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.
일급 시민
일급 시민 혹은 일급 객체는 아래 3가지 조건을 충족해야 한다.
- 변수나 데이터에 할당 할 수 있어야 한다.
- 객체의 인자로 넘길 수 있어야 한다.
- 객체의 리턴값으로 리턴 할수 있어야 한다. 우리가 흔히 사용하는 자바스크립트가 일급 시민에 해당 된다. 함수를 변수에 담을 수 있고 함수 자체를 매개변수 인자로 전달 할 수 있고 함수에서 함수를 리턴 할 수 있다.
1.2.1 메서드와 람다를 일급 시민으로
- 메서드 레퍼런스
/*
listFiles메서드의 매개변수인 FileFilter는 추상 인터페이스이다.
FileFilter 인터페이스에서 accept 메서드를 설정하였기 때문에 필수 구현 메서드이다.
*/
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isHidden(); // 숨겨진 파일 필터링
}
});
위 코드는 파일이 숨겨져 있는지 여부를 알려주는 코드이다. 해당 코드를
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
와 같이 메서드 레퍼런스를 이용하면 메서드를 이급값이 아닌 일급값으로 처리 할 수 있다.
- 메서드 레퍼런스는 ::(이 메서드를 값으로 사용하라는 의미)를 사용.
- 람다: 익명 함수 이용할 수 있는 편리한 클래스나 메서드가 없을 때 새로운 람다 문법을 이용하면 더 간결하게 코드를 구현 할 수 있다. 람다 문법 형식으로 구현된 프로그램을 함수형 프로그래밍 즉 "함수를 일급값으로 넘겨주는 프로그램을 구현한다"라고 한다.
1.2.2 코드 넘겨주기
public static List<Apple> filterGreenApples(List<Apple> inventory) {
/*
new 연산자 부분에 타입을 작성하지 않았는데
작성하지 않아도 왼쪽에 있는 타입 선언부에 있는 타입을 토대로 타입이 지정된다.
이를 타입 추론이라고 한다.
*/
List<Apple> result = new ArrayList<>;
for(Apple apple : inventory) {
if("green".equals(apple.getColor()) ) {
result.add(apple);
}
}
return result;
}
위 코드는 녹색 사과만 필터링 하는 코드이다. 만약 여기서 무게를 필터링 하고 싶다고 한다면 흔히 생각 혹은 하는 방법이 복사&붙혀넣기 방식으로
public static List<Apple> filterHeavyApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>;
for(Apple apple : inventory) {
if(apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
와 같이 별도의 메소드를 작성할 것이다. 그러나 여기서 달라지는 부분은 핵심 로직(녹색 사과 필터링, 무게 필터링 로직) 을 제외한 나머지 코드는 동일한 코드이다.
위 코드를 자바 8에 맞춰 구현 한다면
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);
}
public 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.2.3 메서드 전달에서 람다로
예제는 "1.2.2 코드 넘겨주기"을 응용하였다. 앞 전에 메서드 레퍼런스을 익명함수 또는 람다로 구현하게 되면
filterApples(inventory, (Apple a) -> "green".equals(a.getColor()) );
filterApples(inventory, (Apple a) -> a.getWeight() > 150);
살짝 응용을 하자면
filterApples(inventory, (Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()));
처럼 구현 할 수 있다.
하지만 람다가 몇 줄 이상으로 길어진다면(즉, 복잡한 동작을 수행하는 상황) 익명 람다보다는 코드가 수행하는 일을 잘 설명하는 이름을 가진 메서드를 정의하고 메서드 레퍼런스를 활용하는 것이 바람직하다. 코드의 명확성이 우선시 되어야 한다.
1.3 스트림
- 거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다.
- 컬렉션에서는 반복 과정을 직접 처리해야 했다. 즉, for-each 루프를 이용해서 각 요소를 반복하면서 작업을 수행했다. 이런 방식의 반복을 외부 반복(external iteration)이라고 한다. 반면 스트림 API를 이용하면 루프를 신경쓸 필요가 없다. 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다. 이를 내부 반복(internal iteration)이라고 한다.
- 이론적으로 8개 코어를 가진 컴퓨터라면 8개 코어를 활용해서 병렬로 작업을 수행하여 단일 CPU 컴퓨터에 비해 8배 빨리 작업을 처리할 수 있다.
리스트에서 고가의 트랜잭션(거래)만 필터링한 다음에 통화로 결과를 그룹화 한다는 가정의 코드.
// 그룹화된 트랜잭션을 더함 Map 생성.
Map<Currency, List<Tran>> tranByCurrencies = new HashMap<>();
// 트랜잭션 리스트 반복.
for(Tran tran : trans) {
// 고가의 트랜잭션을 필터링.
if(tran.getPrice() > 1000) {
// 트랜잭션의 통화 추출.
Currency currency = tran.getCurrency();
List<Tran> tranForCurrency = tranByCurrencies.get(currency);
// 현재 통화의 그룹화된 맵에 항목이 없으면 새로 만든다.
if(tranForcurrency == null) {
tranForcurrency = new ArrayList<>();
tranByCurrencies.put(currency, tranForCurrency);
}
// 현재 탐색된 트랜잭션을 같은 통화의 트랜잭션 리스트에 추가한다.
tranForCurrency.add(tran);
}
}
위 코드를 스트림 API로 구현하면
import static java.util.stream.Collectors.toList;
Map<Currency, List<Tran>> tranByCurrencies = trans.stream()
.filter((Tran t) -> t.getPrice() > 1000)
.collect(groupingBy(Tran::getCurrency));
1.3.1 멀티스레딩은 어렵다
이전 자바 버전에서 제공하는 스레드 API로 멀티스레딩 코드를 구현해서 병렬성을 이용하는 것은 쉽지 않다.
멀티스레딩 환경에서 각각의 스레드는 동시에 공유된 데이터에 접근하고, 데이터를 갱신할 수 있다. 결과적으로 스레드를 잘 제어하지 못하면 원치 않는 방식으로 데이터가 바뀔 수 있다. 멀티스레딩 모델은 순차적인 모델보다 다루기가 어렵다. (전통적으로 멀티스레딩 환경에서는 synchronized를 자주 활용한다. synchronized를 활용해도 많은 미묘한 버그가 발생한다. 자바 8에서는 synchronized가 필요 없는 함수형 프로그래밍 형식의 스트림 기반 병렬성을 이용하도록 권고하며 데이터 접근 방법을 제어 하는게 아니라 어떻게 데이터를 분할할지 고민하게 된다.)
자바 8에서는 스트림 API(java.util.stream)로
- 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제
- 멀티코어 활용 어려움
을 해결했다.
기존의 컬렉션에서는 데이터를 처리할 때 반복 되는 패턴을 주어진 조건에 따라
- 데이터를 필터링(filtering)하거나(예로 무게에 따라 사과를 선택.)
- 데이터를 추출(extracting)하거나(예로 리스트에서 각 사과의 무게 필드를 추출.)
- 데이터를 그룹화(grouping)(예로 숫자 리스트의 숫자를 홀수와 짝수로 그룹화.)
등의 기능이 있다.
리스트를 필터링 한다는 가정하에 스트림의 병렬화는
- 포크 (혹은 포킹 단계(forking step)) 리스트 데이터를 각 CPU에 배분.
- 필터 배분 받은 각 CPU에서 리스트 데이터를 필터 처리.
- 결과 합침 각 CPU에서 처리한 결과를 하나로 정리
흐름을 가지게 된다.
컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다.
스트림은 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심이다.
improt static java.util.stream.Collectors.toList;
List heavyApples = inventory.stream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
위 방식은 순차 처리 방식의 코드이다.
improt static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.parallelStream()
.filter((Apple a) -> a.getWeight() > 150)
.collect(toList());
위 방식은 병렬 처리 방식의 코드이다.
stream, parallelStream 메서드 중 어떤 메서드를 쓰느냐에 따라서 순차, 병렬 처리 방식인지 지정 할 수 있다.
자바의 병렬성과 공유되지 않는 가변 상태
자바는 병렬 처리를 위해 두 가지 요술방망이를 제공한다.
- 라이브러리에서 분할 처리 한다. 큰 스트림을 병렬로 처리할 수 있도록 작은 스트림으로 분할한꽁짜로 병렬성을 누릴 수 있다. 함수형 프로그래밍에서 함수형이란 '함수를 일급값으로 사용한다'라는 의미도 있지만 부가적으로 '프로그램이 실행되는 동안 컴포넌트 간에 상효작용이 일어나지 않는다'라는 의미도 포함한다.
- filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 꽁짜로 병렬성을 누릴 수 있다. 함수형 프로그래밍에서 함수형이란 “함수를 일급값으로 사용한다”라폴트 메서드
1.4 디폴트 메서드
- 자바 8은 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스가 포함할 수 있는 기능이 제공. 즉 인터페이스도 메소드를 가질 수 있다. 이를 디폴트 메서드(default method) 라고 부른다
- 프로그래머가 직접 디폴트 메서드를 구현하는 상황은 흔치 않다. 디폴트 메서드는 특정 프로그램을 구현하는데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변할수 있는 환경을 제공하는 기능이다.
- 디폴트 메서드를 이용하면 기존의 코드를 건드리지 않고도 원래의 인터페이스 설계를 자유롭게 확장할 수 있다. 자바 8에서는 인터페이스 규격명세에 default라는 새로운 키워드를 지원한다.
인터페이스 구현체는 다수의 인터페이스를 구현 할 수 있다. 그러므로 여러 인터페이스에 다중 디폴트 메소드가 존재할 수 있다는 것은 완벽하진 않지만 다중 상속을 의미하기도 한다. 다중 상속은 피해야 할 대상이므로 신중하게 인터페이스를 설계 해야 한다. (C++의 다이아몬드 상속 문제)
요약
- 언어 생태계의 모든 언어는 변화해서 살아남거나 그대로 머물면서 사라지게 되는 상황에 놓인다. 지금은 자바의 위치가 견고하지만 코볼과 같은 언어의 선례를 떠올리면 자바가 영원히 지배적인 위치를 유지할 수 있는것은 아닐 수 있다.
- 자바 8은 프로그램을 더 효과적이고 간결하게 구현할 수 있는 새로운 개념과 기능을 제공한다.
- 기존의 자바 프로그래밍 기법으로는 멀티코어 프로세서를 온전히 활용하기 어렵다.
- 함수는 일급값이다. 메서드를 어떻게 함수형 값으로 넘겨주는지, 익명 함수(람다)를 어떻게 구현하는지 기억하자.
- 자바 8의 스트림 개념 중 일부는 컬렉션에서 가져운 것이다. 스트림과 컬렉션을 적절하게 활용하면 스트림의 인수를 병렬로 처리할 수 있으며 더 가독성이 좋은 코드를 구현할 수 있다.
- 인터페이스에서 디폴트 메서드를 이용하면 메서드 바디를 제공할 수 있으므로 인터페이스를 구현하는 다른 클래스에서 해당 메서드를 구현하지 않아도 된다.
- 함수형 프로그래밍에서 null 처리 방법과 패턴 매칭 활용 등 흥미로운 기법을 발견할 수 있었다.
자바 8을 눈여겨봐야 하는 이유
기존 자바는 듀얼 혹은 쿼드 코어 이상을 지원하는 CPU 중 하나의 코어만을 사용한다.
나머지 코어를 활용하기 위해 스레드를 사용 해야 하지만 스레드는 관리가 어렵고 많은 문제가 발생 할 수 있는 단점이 있다. 이를 위해 자바는
- 자바 1.0 스레드, 락, 메모리 모델 지원
- 자바 5 스레드 풀, 병렬 실행 컬렉션
- 자바 7 병렬 실행에 도움을 줄 수 있는 포크/조인 프레임워크 제공
등을 지원하고 제공 하였지만 그럼에도 불과하고 쓰기 어려움.
자바 8을 구성하는 핵심 사항으로는
- 스트림 API
- 메서드에 코드를 전달하는 기법
- 인터페이스의 디폴트 메서드
자바 8은 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 스트림 API를 제공한다. 스트림 API는 최적의 저수준 실행 방법을 선택하는 방식으로 동작한다. 즉 스트림을 이용하면 에러를 자주 일으키며 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다.
조금 다른 관점으로 보면 스트림 API 덕분에
- 메서드에 코드를 전달하는 간결 기법(메서드 레퍼런스와 람다)
- 인터페이스의 디폴트 메서드 추가
스트림 API 때문에 메소드에 코드 전달 기법이 생겼다고 추리하는 것은 해당 기법의 활용성을 제한할 수 있는 위험한 생각이다. 메서드에 코드 전달 기법을 이용하면 새롭고 간결한 동작 파라미터화(behavior prameterization)를 구현할 수 있다.
메서드에 코드를 전달(결과를 반환하고 다른 자료구조로 전달 할 수도 있음.)기법은 함수형 프로그래밍에서 위력을 발휘한다.
참고
synchronized
- 멀티코어 CPU의 각 코어는 별도의 캐시(빠른 메모리)를 포함하고 있다. 락을 사용하면 이러한 캐시가 동기화되어야 하므로 속도가 느린 캐시 일관성 프로토콜 인터코어 통신(cache-coherency-protocol intercore communication)이 이루어진다.