1. 역사의 흐름은 무엇인가?
Java8의 등장
- 자연어에 더 가까운 방식의 코드
Collections.sort(inventory, new Comparator<Apple>(){
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
// Java8
inventory.sort(comparing(Apple::getWeight));
- 하드웨어적 변화
- 멀티코어 CPU의 대중화
- 대부분의 자바 프로그램은 코어 중 하나만을 사용했음
- 나머지 코어를 활용하려면 스레드를 이용해야하는데 에러가 발생하기 쉬움
- Java8에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공함
- 스트림 API
- 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 API
- 고수의 언어 → 최적의 저수준 실행 방법을 선택하는 방식으로 동작
- 스트림을 이용하면 synchronized를 사용하지 않아도 됨
- 메서드에 코드를 전달하는 기법
- 메서드 참조와 람다
- 새롭고 간결한 방식으로 동작 파라미터화를 구현할 수 있음
- 함수형 프로그래밍에서 위력을 발휘함
- 인터페이스의 디폴트 메소드
- 스트림 API
2. 왜 아직도 자바는 변화하는가?
2.1. 프로그래밍 언어 생태계에서 자바의 위치
- 시작: 잘 설계된 객체 지향 언어
- 스레드, 락을 이용한 동시성 제어
- 코드를 JVM 바이트 코드로 컴파일함
- 모든 브라우저에서 가상 머신 코드를 지원하기 때문에 인터넷 애플릿 프로그램의 주요 언어가 됨
- 임베디드 컴퓨팅 분야
- '빅데이터'
- 멀티코어 컴퓨터나 컴퓨팅 클러스터를 이용해서 빅테이터를 효과적으로 처리할 필요성이 커짐
- 병렬 프로세싱을 활용해야하는데 지금까지의 자바로는 충분히 대응할 수 없었음
2.2 스트림 처리
- 스트림: 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임
- ex) In Unix
- cat file1 file 2 | tr "[A-Z]" "[a-z]" | sort | tail -3
- file1, file2에 있는 단어를 모두 소문자로 바꾼 후 정렬, 뒤에서부터 3번째 행부터 출력
- cat: 두 파일을 연결해서 스트림을 생성함
- tr: 스트림의 문자를 번역함
- sort: 스트림의 행을 정렬함
- tail -3: 스트림의 마지막 3개 행을 제공
- |: 명령 연결 가능
- ex) In Unix
- Java 8, java.util.stream 패키지에 스트림 API가 추가 됨
- Stream<T>
- T 형식으로 구성된 일련의 항목을 의미
- 파이프라인을 구성했던 것처럼 스트림 API는 파이프라인을 만드는데 필요한 많은 메서드를 제공함
- 데이터베이스 질의처럼 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있음
- 스트림 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있음
- 스레드를 사용하지 않으면서 공짜로 병렬성을 얻을 수 있음
2.3 동작 파라미터화로 메서드에 코드 전달하기
- 동작 파라미터화: 코드의 일부를 API로 전달하는 기능
- ex) 2013UK0001, 2014US0002, ... 등의 형식을 갖는 송장ID가 있다고 가정함. 고객 ID 또는 국가 코드 순으로 정렬해야함
- compareUsingCustomerID 메소드를 sort의 인수로 전달
public int compareUsingCustomerID(String inv1, String inv2){
...
}
2.4 병렬성과 공유 가변 데이터
- 기존: synchronized를 이용해서 공유된 가변 데이터를 보호하는 규칙을 만들 수 있음
- 다중 프로세싱 코어에서는 코드가 순차적으로 실행되어야하므로
- synchronzied를 사용하면 병렬이라는 목적을 무력화 시키면서 시스템에 악영향을 미칠 수 있음
- Java 8: 스트림을 이용해 기존의 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있음
3. 자바 함수
- 함수: method / static method / 수학적인 함수 (부작용을 일으키지 않는 함수)
- 스트림과 연계될 수 있도록 함수를 새로운 값의 형식으로 추가
3.1 메서드와 람다를 일급 시민으로
- 일급값: 그 자체로 값이 될 수 있는 것들 (int, float, ...)
- 메서드를 값으로 취급할 수 있음
- 메서드 참조
- :: , 이 메서드를 값으로 사용하라는 의미
- 메서드는 코드를 포함하고 있으므로 코드를 마음대로 전달할 수 있음
- ex) 디렉토리에서 모든 숨겨진 파일을 필터링함
// Java8 이전
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file){
return file.isHidden();
}
});
// Java8
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
람다 : 익명 함수
- 람다(익명 함수)를 값으로 취급할 수 있음
- ex) (int x) -> x + 1
- x라는 인수를 호출하면 x+1을 반환
3.2 코드 넘겨주기: 예제
// 초록 사과 필터
public static boolean isGreenApple(Apple apple){
return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple){
return apple.getWeight() > 150; // ⓐ
}
// ⓐ를 명확하게 하기 위해 추가함 (java.util.function에서 임포트)
public interface Predicate<T>{
boolean test(T t);
}
// 메서드가 p라는 이름의 Predicate 파라미터로 전달됨
static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p){
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
if (p.test(apple)) { // 사과는 p가 제시하는 조건에 맞는가?
result.add(apple);
}
}
return result;
}
filterApples(inventory, Apple::isGreenApple);
filterApples(inventory, Apple::isHeavyApple);
3.3 메서드 전달에서 람다로
filterApples(inventory, (Apple a)->GREEN.equals(a.getColor()));
filterApples(inventory, (Apple a)->a.getWeight() > 150);
filterApples(inventory, (Apple a)->a.getWeight() < 80 || RED.equals(a.getColor()));
4. 스트림
- 거의 모든 자바 애플리케이션은 컬랙션을 만들고 활용함
- 많은 요소를 가진 목록들을 for-each문으로 순회하면 오랜 시간이 걸릴 수 있음
- 서로 다른 CPU 코어에 작업을 각각 할당해서 처리 시간을 줄일 수 있다면 좋을 것임
- Ex) 리스트에서 고가의 트랜잭션만 필터링한 다음에 통화로 결과를 그룹화
// 그룹화된 트랜잭션을 더할 Map 생성
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
// 트랜잭션의 리스트를 순회
for (Transaction transaction : 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.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().filter((Transaction t)->t.getPrice() > 1000).collect(groupingBy(Transaction::getCurrency));
- 스트림 API는 내부 반복을 이용함
4.1 멀티스레딩은 어렵다
- Java8
- 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 해결
- 기존의 컬렉션에서는 데이터를 처리할 때 반복되는 패턴이 너무 많았음
- 라이브러리에서 반복되는 패턴을 제공 (필터링, 추출, 그룹화 등)
- 멀티코어 활용 어려움 해결
- 과정
- 1) forking step: 처리해야할 task를 여러 CPU로 나누는 과정
- 2) 각 CPU에서 task를 마침
- 3) 그 결과를 합침
- 스트림 API와 컬렉션 API는 비슷한 방식으로 동작함
- 순차적인 데이터 항목 접근 방법을 제공
- 컬렉션: 데이터를 어떻게 저장하고 접근할지에 중점을 둠
- 스트림: 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둠, 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심
- 컬렉션을 필터링하는 가장 빠른 방법: 컬렉션 --> 스트림 --> 병렬 처리 --> 리스트로 다시 복원
- 과정
- 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 해결
// 순차 처리 방식의 코드
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.stream().filter((Apple a)->a.getWeight() > 150).collect(toList());
// 병렬 처리 방식의 코드
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.parallelStream().filter((Apple a)->a.getWeight() > 150).collect(toList());
- 자바의 병렬성과 공유되지 않은 가변 상태
- 라이브러리에서 분할 처리: 큰 스트림을 작은 스트림으로 분할
- filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있음. 상호작용하지 않는다는 것은 프로그래머 입장에서는 상당히 자연스러운 일임
- 함수형 프로그래밍에서 함수형이란 '함수를 일급값으로 사용한다'는 의미도 있지만, 부가적으로 '프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다'라는 의미도 포함
- 기존 인터페이스의 변경 → 디폴트 메서드로 해결
- Collections.sort(list, comparator)은 이론적으로 list.sort(comparator)이 맞음
5. 디폴트 메서드와 자바 모듈
- 외부에서 만들어진 컴포넌트를 이용해 시스템을 구축하는 경향이 있음
- 지금까지의 자바
- 평범한 자바 패키지 집합을 포함하는 JAR 파일을 제공하는 것이 전부
- 패키지의 인터페이스를 바꿔야하는 상황에서는 인터페이스를 구현하는 모든 클래스의 구현을 바꿔야하므로 헬게이트 오픈
- Java8
- 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메소드 지원
- Java9
- 모듈 시스템
- 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있음
- 모듈 덕에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인 작업이 용이해짐
- 모듈 시스템
디폴트 메소드
- 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능
List<Apple> heavyApples1 = inventory.stream().filter((Apple a)->a.getWeight() > 150).collect(toList());
List<Apple> heavyApples2 = inventory.parallelStream().filter((Apple a)->a.getWeight() > 150).collect(toList());
- Java8: 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능 제공
- 디폴트 메서드를 이용해 기존의 코드를 건드리지 않고 원래의 인터페이스 설계를 자유롭게 확장할 수 있음
- Ex) List.sort 메소드 호출
// List 인터페이스에 디폴트 메서드 정의가 추가되었음
default void sort(Comparator<? super E> c){
Collections.sort(this, c);
}
- 다중 상속?
- 여러 인터페이스에 다중 디폴트 메서드가 존재할 수 있음
- 엄밀히 다중 상속은 아니지만 어느 정도는 비슷한 부분이 있음
- 다이아몬드 상속 문제: 손자 객체에 할머니 객체가 2개 생기는 것
6. 함수형 프로그래밍에서 가져온 다른 유용한 아이디어
- 자바에 적용된 함수형 프로그래밍의 핵심적인 두 아이디어
- 메서드와 람다를 일급값으로 사용
- 가변 공유 상태가 없는 병렬 실행을 이용해 효율적이고 안전하게 함수나 메서드를 호출
- Optional<T> 클래스: NullPointer 예외를 피할 수 있도록 도와주는 클래스
- 값을 가지거나 가지지 않을 수 있는 컨테이너 객체
- 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 포함하고 있기 때문에 NullPointer 예외를 피할 수 있음
- 패턴 매칭
- if-then-else보다 패턴 매칭을 이용했을 때 더 정확한 비교를 구현할 수 있으나, Java 8에서는 지원하지 않음.
- switch를 확장한 것으로 데이터 형식 분류와 분석을 한 번에 수행할 수 있음
- 함수형 언어는 패턴 매칭을 포함한 다양한 데이터 형식을 switch에 사용할 수 있음
- 예시
- f(0) = 1
- f(n) = n * f(n-1) 그렇지 않으면
// [Scala]
// 트리로 구성된 수식을 단순화 하는 프로그램
// expr: 수식
def simplifyExpression(expr: Expr): Expr = expr match {
case BinOp("+", e, Number(0)) => e // 0 추가
case BinOp("-", e, Number(0)) => e // 0 빼기
case BinOp("*", e, Number(0)) => e // 1로 곱하기
case BinOp("/", e, Number(0)) => e // 1로 나누기
case _ => expr // 표현식을 단순화할 수 없음
}
'백엔드 > 모던자바인액션' 카테고리의 다른 글
ch15. CompletableFuture와 리액티브 프로그래밍 컨셉의 기초 (0) | 2023.03.23 |
---|---|
[ch4] 스트림 소개 (0) | 2022.09.19 |
[ch3] 람다 표현식 (2) (0) | 2022.09.03 |
[ch3] 람다 표현식 (1) (0) | 2022.09.02 |
[ch2] 동작 파라미터화 코드 전달하기 (0) | 2022.09.01 |
댓글