[JAVA] Java 8 (1)

Seongje kim, 15 October 2020

이번 포스트는 Java 8의 새로운 기능에 대한 1부 내용입니다.

Java의 변화


이전 버전들과 비교하여 Java 8에서는 멀티 코어 CPU가 대충화되면서 대용량의 데이터를 효과적으로 처리하기 위한 병렬 프로세싱 환경을 더 쉽게 관리할 수 있고, 에러가 덜 발생할 수 있는 방향으로 발전하였다. 병렬 실행 환경을 쉽게 관리할 수 있고 에러가 덜 발생할 수 있도록 Java는 스레드 풀, 병렬 실행 컬렉션, Fork/Join 프레임워크와 같은 기능을 제공하지만 여전히 Java 개발자들이 활용하기에 쉽지가 않았으며 이를 해결하기 위해서 Java 8에서는 이 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공한다.

Java 8에서 대표적으로 새롭게 추가된 기능은 아래와 같다.

스트림(Stream) API


Java 8은 데이터베이스에서 쿼리를 처리하는 것과 같이 병렬 연산을 지원하는 스트림 API를 제공한다. 즉, 멀티 코어 CPU를 이용하는 것보다 훨씬 비용이 비싸고 에러 위험이 많은 synchronized를 더 이상 사용하지 않아도 된다.

스트림 처리

스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임으로써 스트림을 처리하는 프로그램은 입력 스트림에서 데이터를 하나씩 읽어서 출력 스트림에 하나씩 기록한다. 이는 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있음을 알 수 있다.

간단히 말하면 스트림 API는 어떤 공장의 전체 조립 라인과 같이 작업의 단위로써 어떤 항목을 연속적으로 제공하는 기능을 제공한다. 핵심은 기존에 한 번에 한 항목씩 처리하였던 작업의 단위를 고수준으로 추상화시켜 일련의 스트림으로 처리가 가능해졌다는 것이다. 또한 스트림 파이프라인을 이용해 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다.

결과적으로 병렬 연산 처리를 위해 스레드의 synchronized를 사용하지 않고도 병렬성을 얻을 수 있게 된 것이다.

병렬성과 공유 가변 데이터

그렇다면 결국 스트림 메서드로 전달되는 코드는 다른 코드와 동시에 실행되더라도 안전하게 실행됨을 보장한다는 것은 당연하다.

만일 어떤 함수가 공유된 가변 데이터에 접근하지 않는다면 병렬 실행에 문제가 없다. 이러한 함수를 순수(Pure), 부작용이 없는(Side-effect-free), 상태가 없는(Stateless) 함수라고 부른다.

물론 순수 함수가 아닌 공유 가변 데이터에 접근하는 함수들을 기존처럼 synchronized를 사용하여 임계 영역을 만듦으로써 공유된 가변 데이터를 보호하는 규칙을 만들 수 있지만 일반적으로 시스템 성능에 악영향을 미치며 코어의 멀티 프로세싱면에서 비싼 비용을 감수해야 하는 단점을 가진다. 따라서 Java 8은 스트림 API를 활용하여 기존 스레드 API보다 쉽고 효율적인 병렬성을 제공한다.

컬렉션 처리의 모호함과 반복적인 코드 문제

만일 기본 자바 컬렉션에 특정 데이터를 필터링하여 저장해야 한다면 반복문과 조건문을 이용(외부 반복)하였지만 스트림 API에서는 모든 데이터가 라이브러리 내부에서 처리(내부 반복)된다. 이로써 스트림 API를 활용하면 추가적인 반복문이나 조건문이 없이도 구현이 가능해지며 컬렉션을 처리하면서 발생하는 모호함 및 반복적인 코드 문제를 해결할 수 있다.

람다(Lambda) 표현식과 동작 파라미터화


스트림 API의 특징에서 볼 수 있듯이, 이는 함수형 프로그램의 핵심적인 사항이며 Java 8이 함수형 프로그래밍을 지원함을 의미한다. 따라서 Java 8에서는 함수형 프로그래밍에 위력을 발휘하는 람다식과 메서드 레퍼런스 기능을 새롭게 제공한다.

메서드에 코드를 전달하는 기법 (메서드 레퍼런스와 람다)

동작 파라미터화란 특정 메서드를 다른 메서드의 인수로 넘겨주는 기능을 말한다. 이전 버전에서도 익명 클래스를 이용하여 동작 파라미터를 구현할 수 있었지만 재사용성이 떨어지고, 코드가 불필요하게 길어진다는 단점을 가지고 있었다. 하지만 Java 8에 새롭게 추가된 메서드에 코드를 전달하는 기법을 이용하면 보다 새롭고 간결한 방식으로 동작 파라미터를 구현할 수 있으며, 재사용성 및 코드 가독성에 이점을 얻을 수 있다.

메서드에 코드를 전달하는 기법은 메서드 레퍼런스와 람다가 있다.

메서드와 람다를 일급 시민으로

프로그래밍 언어의 핵심은 값을 바꾸는 것이다. 이 값을 일급 값 또는 일급 시민이라고 부르며, 일급 시민이 되기 위한 조건을 아래와 같다.

  1. 변수나 데이터 구조 안에 담을 수 있다.
  2. 파라미터로 전달 할 수 있다.
  3. 반환 값으로 사용할 수 있다.
  4. 할당에 사용된 이름과 관계없이 고유한 구별이 가능하다.
  5. 동적으로 프로퍼티 할당이 가능하다.

이전 버전까지의 Java에서의 메서드와 클래스 등은 이급 시민에 해당되지만 Java 8에서는 이와 같은 이급 시민을 일급 시민으로 바꿀 수 있게 되었다. 따라서 메서드와 람다를 일급 시민으로 만듦으로써 쉽고 간결하게 동작 파라미터화를 구현할 수 있는 것이다.

  • 메서드 레퍼런스
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});

위 코드는 기존 자바의 익명 클래스 기능을 이용하여 현재 디렉토리에 숨겨진 파일을 필터링하는 예제 코드이다. 구현체를 포함한 익명 클래스를 만들어 isHidden() 이라는 함수를 전달하는 것을 볼 수 있다.

File[] hiddenFiles = new File(".").listFiles(File::isHidden);

하지만 Java 8에서는 위와 같이 메서드 레퍼런스(‘::’, 메서드를 값으로 사용하라는 의미)를 이용하여 익명 클래스로 구현하여 전달하던 함수를 값으로써 직접 전달할 수 있다.

  • 람다(익명 함수)
(int x, int y) -> x + y

람다 또한 값으로 취급할 수 있다. 위와 같이 람다는 불필요한 코드를 줄여주고, 코드 가독성을 높일뿐만아니라 만일 한번만 사용될 함수라면 따로 정의하지 않아도 된다는 장점을 가진다. 단, 람다 표현식은 인터페이스에 메서드가 하나인 것들만 적용할 수 있다. (이것은 인터페이스의 default 메서드가 생긴 이유 중 하나이다.)

디폴드(default) 메서드


디폴트 메서드는 더 쉽게 변화할 수 있는 인터페이스를 만들 수 있도록 Java 8에 새롭게 추가된 기능이다. 이는 특정 인터페이스의 구현 클래스에서 구현하지 않아도 되는 메서드를 해당 인터페이스가 포함할 수 있는 것을 말한다.

따라서 디폴트 메서드의 등장으로 이전의 Java 버전에서 상위 계층에서 구현체를 가지려고 할 경우, 추상화 또는 상속의 목적에 관련이 없고 동작의 구현이 목적인 인터페이스를 사용함으로써 얻는 장점이 더 많음에도 불구하고 어쩔 수 없이 추상 클래스를 사용해야 했던 불편함에서 벗어날 수 있게 되었다. 단, 디폴트 메서드가 선언된 인터페이스는 다중 상속을 이점을 가질 수 없다.