티스토리 뷰
역사의 흐름은 무엇인가?
멀티코어 CPU가 대중화 되는 등 하드웨어의 발전이 자바의 변화를 부추겼다. 자바 8이 등장하기 이전에는 유휴 코어를 사용하기 위해 스레드를 사용하려는 시도가 있었지만, 스레드를 사용하면 많은 관리상의 문제가 발생했다. 자바는 병령 실행 환경을 쉽게 관리하고 에러가 덜 발생할 수 있도록 진화하였다. 하지만 여러 개발자가 협업하여 프로그램을 만드는 이상 쉽지 않았다.
하지만 자바 8은 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공한다. 이러한 방법은 이전과는 새로운 방법이기에 사용법을 익혀야한다. 그리고 자바 9에서는 리액티브 프로그래밍이라는 병렬 실행 기법을 지원한다. 리액티브 프로그래밍은 실행 환경이 한정적이지만, 고성능 시스템에서 인기를 얻고 있는 RxJava를 표준적인 방식으로 지원한다.
자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 아래 3가지 기능을 제공한다.
- 스트림 API
- 메서드에 코드를 전달하는 법
- 인터페이스의 디폴트 메서드
자바 8은 DB의 질의 언어에서 표현식을 처리하는 것 처럼 병렬 연산을 지원하는 스트림이라는 API를 제공한다.
자바 8은 메서드에 코드를 전달하여 새롭고 간결한 방식으로 동작 파라미터화(Behavior parameterization)을 구현한다.
왜 아직도 자바는 변화하는가?
자바는 잘 설계된 객체지향형 언어로, 다양한 라이브러리를 제공하고 JVM 바이트 코드로 컴파일하기 때문에 각광받았다. 하지만 스마트 폰의 보급과 SNS의 활성화로 빅데이터라는 문제에 직면하였고, 멀티코어 컴퓨팅 환경이나 컴퓨팅 클러스터를 이용해서 빅데이터를 처리할 필요성이 높아졌다.
이러한 요구에 대응하여 자바 8이 나타났다. 자바 8에 추가된 기능은 기존 자바에는 없던 완전히 새로운 개념이지만, 현재 시장에서 요구하는 기능을 효과적으로 제공한다.
스트림 처리
스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다. 이론적으로 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 마찬가지로 출력 스트림으로 데이터를 한 개씩 기록한다.
자바 8에는 java.util.stream 패키지에 스트림 API가 추가되었다. 스트림 패키지에 정의된 Stream<T>는 T 형식으로 구성된 일련의 항목을 의미한다. 스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만, 이제는 작업을 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다. 또 스트림 파이프 라인을 사용하여 여러 CPU 코어에 쉽게 할당할 수 있어, 스레드에 비해 효율적으로 병렬성을 얻을 수 있다.
동작 파라미터화로 메서드에 코드 전달하기
자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다. 자바의 sort 메서드에 Comparator 객체를 넘기지 않고, 메서드를 인수로 넘겨주어 코드를 단순하게 만들어준다.
병렬성과 공유 가변 데이터
자바 8 이전에는 병렬성을 얻기 위해 멀티 스레드를 사용하였다. 하지만 공유 객체, 변수에 접근하게 되면 여러가지 문제가 발생할 수 있어 synchronized를 이용했다. 하지만 이는 매우 비효율적이고 시스템 성능에 악영향을 미친다. 하지만 위에서 말했듯이 자바 8의 스트림 API를 사용하면 병렬성은 공짜로 얻을 수 있다.
자바 함수
프로그래밍 언어에서 함수(Function)라는 용어는 메서드(Method) 특히 정적 메서드와 같은 의미로 사용된다. 자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않는 함수를 의미한다. 자바 8 에서는 함수를 새로운 값의 형식으로 추가했다.
프로그래밍 언어의 핵심은 값을 바꾸는 것이며, 이러한 값을 일급 값(또는 시민)이라 한다. 자바 8 이전 클래스와 메서드는 그 자체로 값이 될 수 없었다. 하지만 자바 8 설계자는 메서드와 클래스를 일급 시민으로 만들기 위해 기능을 추가했다.
메서드와 람다를 일급 시민으로
메서드 참조
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isHidden();
}
}
위와 같이 FileFilter를 인스턴스화할 필요 없이, 이미 isHidden이라는 함수는 준비 되어 있으므로 메서드 참조(::)를 이용하여 listFiles에 직접 전달할 수 있다.
File[] hiddenFiles = new File(".").listFiles(File::isHidden);
람다: 익명 함수
메서드 참조 통해 메서드를 일급 값으로 취급할 뿐 아니라 람다를 포함하여 함수도 값으로 취급할 수 있다. 예를 들어 add라는 메서드 대신 (int x) -> x + 1과 같이 람다 함수를 정의하고 전달할 수 있다.
예제
public class Apple {
private int weight;
private Color color;
public Apple(int weight, Color color) {
this.weight = weight;
this.color = color;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
public static boolean isGreenApple(Apple apple) {
return Color.GREEN == apple.getColor();
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
@Override
public String toString() {
return "Apple{" +
"weight=" + weight +
", color=" + color +
'}';
}
}
public class practice1 {
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> predicate) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (predicate.test(apple)) {
result.add(apple);
}
}
return result;
}
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(160, Color.GREEN),
new Apple(140, Color.GREEN),
new Apple(180, Color.RED));
System.out.println(filterApples(inventory, Apple::isGreenApple));
System.out.println(filterApples(inventory, Apple::isHeavyApple));
}
}
Apple 클래스 내부에 isGreenColor, isHeavyApple 함수를 정의했다. 그리고 아래 메인 메소드를 보면 메서드 참조를 통해 아주 간단하게 메서드를 호출하였다. 하지만 일일이 isGreenColor, isHeavyApple와 같은 함수를 정의하기는 번거로울 것 같다. 이는 람다를 통해 해결할 수 있다.
메서드 전달에서 람다로
위 isGreenColor, isHeavyApple를 간단하게 람다로 구현할 수 있다.
filterApples(inventory, (Apple a) -> Color.GREEN == a.getColor());
filterApples(inventory, (Apple a) -> a.getWeight() >= 150);
filterApples(inventory, (Apple a) -> Color.RED == a.getColor() || a.getWeight() < 180);
스트림
자바 애플리케이션은 컬렉션을 만들고 활용한다. 하지만 컬렉션으로 모든 것이 해결되는 것은 아니다. 예를 들어, 리스트에서 필터링이 요구된다면 컬렉션은 반복문을 통해서 이를 구현해야하며, 이를 외부반복이라한다. 하지만 스트림 API에서는 라이브러리 내부에서 모든 데이터가 처리된다. 이를 내부반복이라한다.
자바 8은 스트림 API로 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제 그리고 멀티코어 활용 어려움이라는 두 가지 문제를 모두 해결했다. 데이터를 필터링하거나, 추출하거나, 그룹화하는 과정이 라이브러리 내부에서 진행되고, 이러한 동작들이 쉽게 병렬화할 수 있다.
컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면, 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다는 것을 기억해야한다. 스트림은 스트림 내의 요소를 쉽게 병렬로 처리할 수 있는 환경을 제공한다는 것이 핵심이다.
위 예제를 스트림으로 처리하면 아래와 같다.
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.stream().filter((Apple a) -> a.getWeight() > 150))
.collect(toList());
디폴트 메서드와 자바 모듈
자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 모듈 덕분에 JAR 같은 컴포넌트에 구조를 적용할 수 있으며 문서화와 모듈 확인 작업이 용이해졌다.
또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다. 디폴트 메서드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능이다.
자바 8 이전의 컬렉션은 stream이나 parallelStream 메서드를 지원하지 않는다. 따라서 Collection 인터페이스에 메서드를 추가하는 방안을 생각할 수 있다. 하지만 인터페이스에 새로운 메서드를 추가하게 된다면 모든 구현체가 이 메서드를 구현해야 한다. 따라서 기존의 구현을 고치지 않고 공개된 인터페이스를 변경해야한다.
이를 위해 자바 8은 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있는 기능을 제공한다. 이 기능이 디폴트 메서드이고 디폴트 메서드는 클래스 구현이 아니라 인터페이스의 일부로 포함된다.
함수형 프로그래밍에서 가져온 유용한 아이디어
자바 8에서는 NullPointerException을 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다. 이는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체이다. Optional<T>는 값이 없는 상황을 어떻게 처리할지 명시적으로 구현하는 메서드를 가지고있다.
'Java' 카테고리의 다른 글
리스트 순회 중 발생하는 ConcurrentModificationException에 대해 (0) | 2021.07.25 |
---|---|
[모던 자바 인 액션] 스트림 (0) | 2021.06.24 |
[모던 자바 인 액션] 3. 람다 표현식 (0) | 2021.06.21 |
[모던 자바 인 액션] 2. 동작 파라미터화 코드 전달하기 (0) | 2021.06.17 |
자바와 파이썬의 for문 차이 (0) | 2021.06.09 |