본문으로 바로가기

Java8

category Programming/Java 2020. 1. 29. 20:16
    반응형

    1. 자바가 변화하는 이유

    Java8에서는 기존 버전들과 비교해서 가장 큰 변화가 있었습니다. 이는 현재 프로그래밍 생태계의 변화와 연관이 있습니다. 하드웨어 적으로 멀티코어 CPU가 대중화 되었고 빅데이터가 화두로 떠오르면서 대용량의 데이터를 효과적으로 처리하고자 하는 욕구가 강해졌습니다. 즉, 프로그래머들이 병렬 프로세싱을 활용하고자 하였지만 기존 자바로는 이 부분을 충분히 대응하기가 어려워진 것입니다.

    물론 이전 버전의 자바에서도 스레드를 이용하여 유휴코어들을 활용할 수 있었습니다. 하지만 스레드를 사용하면 관리가 어렵고 많은 문제가 발생할 수 있다는 단점이 존재했습니다. 자바는 이러한 병렬 실행 환경을 쉽게 관리할 수 있고 에러가 덜 발생할 수 있는 방향으로 진화하려고 노력했습니다. (스레드풀, 병렬 실행 컬렉션, 포크/조인 프레임워크 와 같은 기능을 제공) 하지만 여전히 개발자가 활용하기에는 쉽지가 않았고 이를 해결해기 위해 Java8에서는 병렬 실행을 새롭고 단순한 방식으로 접근할 수 있는 방법을 제공하게 되었습니다.

    2. 새롭게 추가된 기능

    이제 Java8에서 추가된 기능을 간략하게 정리해보겠습니다.

    스트림 API메서드에 코드를 전달하는 기법(메서드 레퍼런스와 람다)인터페이스의 디폴트 메서드

    <Java8에 추가된 기능>

    Java8에서는 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공합니다. 데이터베이스 질의 언어에서 고수준 언어로 원하는 동작을 표현하면, 구현에서 최적의 저수준 실행 방법을 선택하는 방식으로 동작합니다. 즉, 에러를 자주 일으키며 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 synchronized를 사용하지 않아도 됩니다.

    조금 다른 관점에서 보면 결국 스트림API 덕분에 메서드에 코드를 전달하는 기법인터페이스의 디폴트 메서드가 추가 되었음을 알 수 있습니다.

    메서드에 코드를 전달하는 기법을 이용하면 새롭고 간결한 방식으로 동작 파라미터화(behavior parameterizaion)을 구현할 수 있습니다. 이전 버전의 자바에서도 익명클래스를 이용하여 동작파라미터를 구현할 수 있었지만 재사용이 힘들었고 코드가 불필요하게 장황하게 지는 단점이 있었습니다. Java8에서는 메서드 레퍼런스와 람다를 이용하여 위의 단점을 해결하였습니다.

    특히 위 기능은 함수형 프로그래밍(functional-style programming) 에서 위력을 발휘합니다.

    다음으로 추가된 기능의 모태가 되는 세가지 프로그래밍 개념에 대해 자세히 살펴보겠습니다.

    3-1. 스트림 처리(Stream Processing)

    스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임을 의미합니다. 프로그램은 입력 스트림에서 데이터를 한 개씩 읽어 들이며 출력 스트림으로 데이터를 한 개씩 기록합니다. 즉, 어떤 프로그램의 출력 스트림은 다른 프로그램의 입력 스트림이 될 수 있습니다.

    일례로 유닉스나 리눅스의 많은 프로그램은 표준 입력에서 데이터를 읽은 다음 , 데이터를 처리하고 결과를 표준 출력으로 기록합니다.

    cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

    위 예제는 파일의 단어를 소문자로 바꾼 다음 사전순으로 단어를 정렬했을 때 가장 마지막에 위치한 세 단어를 출력하는 프로그램입니다. sort는 여러 행의 스트림을 입력으로 받아 여러 행의 스트림을 출력으로 만들어냅니다. 유닉스에서는 여러 명령을 병렬로 실행하기 때문에 cat이나 tr이 완료되지 않은 시점에서 sort가 행을 처리하기 시작할 수 있습니다. 이는 자동차 생산 공장 라인에 비유할 수 있습니다. 조립라인은 정해진 순서로 한 개씩 운반하지만 각각의 작업장에서는 동시에 작업을 처리하는 것과 같다고 생각할 수 있습니다.

    Java8에는 java.util.stream 패키지에 스트림 API가 추가되었습니다.

    Stream // T 형식으로 구성된 일련의 항목

    간단하게 말하여 스트림 API는 조립라인 처럼 어떤 항목을 연속으로 제공하는 어떤 기능이라고 설명할 수 있습니다. 유닉스 예제에서 파이프라인을 구성했던 것 처럼 스트림 API는 파이프라인을 만드는 데 필요한 많은 메서드를 제공합니다.

    스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만 작업을 고수준으로 추상화해 일련의 스트림으로 만들어 처리할 수 있다는 것입니다. 또한 스트림 파이프라인을 이용해 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있다는 부가적인 이득도 얻을 수 있습니다. 스레드라는 복잡한 작업을 사용하지 않고도 병렬성을 얻을 수 있게 된 것입니다.

    3-2. 동작 파라미터화로 메서드에 코드 전달하기

    Java8에서는 코드 일부를 API로 전달할 수 있습니다. 즉, 메서드를 다른 메서드의 인수로 넘겨주는 기능을 제공합니다. 이러한 기능을 동작 파라미터화(behaivor parameteriztion) 이라고 부릅니다.

    이를 통해 익명 클래스를 활용하던 것과 비교하여 재사용성이 증가하고 코드가 더 간결해지는 이점을 얻을 수 있습니다.

    3-3. 병렬성과 공유 가변 데이터

    스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수 있어야 합니다. 보통 다른 코드와 동시에 실행하더라도 안전하게 실행할 수 있는 코드를 만들려면 공유된 가변 데이터(shared mutable data)에 접근하지 않아야 합니다. 이러한 함수를 순수(pure)함수, 부작용 없는(side-effect-free) 함수, 상태없는(stateless) 함수라고 부릅니다.

    물론 기존처럼 synchronized를 이용해 공유된 가변 데이터를 보호하는 규칙을 만들 수 있습니다. 하지만 일반적으로 synchronized는 시스템 성능에 악영향을 미치고 다중 프로세싱 코어에서 비싼 비용을 치러야하기 때문에 기피하는 기능 중 하나입니다. Java8에서 스트림을 이용하면 기존 자바 스레드 API보다 쉽게 병렬성을 활용할 수 있습니다.

    위에서 말한 부분은 익히 아시는 함수형 프로그램의 핵심적인 사항들입니다. 자바가 함수형 패러다임을 지원하려고 노력했다고 볼 수도 있겠네요.

    4-1. 자바 함수

    프로그래밍 언어에서 함수 == 메서드 == 정적메서드 와 같은 의미로 사용됩니다. 자바의 함수는 이에 더해 수학적인 함수처럼 사용되며 부작용을 일으키지 않은 함수를 의미합니다. Java8에서는 함수를 새로운 값을 형태로 추가했습니다.

    프로그래밍 언어의 핵심은 값을 바꾸는 것입니다. 이 값을 일급값(일급시민, 일급객체) 이라고 합니다. 프로그래머들에게는 일급시민이라는 용어가 더 익숙할 것입니다. 일급시민이 되기 위한 조건은 아래와 같습니다.

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

    자바에서 메서드와 클래스 등은 이급시민에 해당합니다. Java8에서는 이와 같은 이급시민을 일급시민으로 바꿀 수 있는 기능을 추가했습니다.

    4-2. 메서드와 람다를 일급 시민으로

    [ 메서드 레퍼런스(method reference) ]

    위 기능은 디렉터리에서 숨겨진 파일을 필터링하는 예제를 통해 설명해보겠습니다. 기존 자바의 익명클래스 기능을 이용하여 해당 기능을 작성해보겠습니다.

    File[] hiddenFiles = new File(".").listFiles(new FileFilter(){ public boolean accept(File file){ return file.isHidden(); }});**

    이제 해당 코드를 Java8의 방식으로 바꿔 보겠습니다.

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

    isHidden 이라는 함수는 이미 준비되어 있으므로 익명클래스로 구현하던 부분을 메서드 레퍼런스(:: , 메서드를 값으로 사용하라는 의미)를 이용하여 listFiles에 직접 전달할 수 있습니다. 여기서 메서드라는 용어대신 함수라는 용어를 사용했다는 것도 중요한 부분입니다.

    [ 람다: 익명함수 ]

    Java8에서는 람다(익명함수)를 포함아여 함수도 값으로 취급할 수 있습니다.

    1(int x) -> x + 1 // x라는 인수로 호출하면 x + 1 을 반환

    위 처럼 표현할 수 있습니다.

    이를 활용하면 한번만 사용될 함수를 따로 정의하지 않아도 된다는 장점을 가질 수 있습니다.

    5. 스트림

    기존 자바 컬렉션에 특정 데이터만 필터링하여 저장해야한다면 반복문과 조건문을 이용하여 필터링 후 특정 컬렉션에 저장하는 방법을 사용했습니다. 이를 외부 반복(external iteration)이라고 합니다.

    Java8에서는 스트림 API를 이용하여 라이브러리 내부에서 모든 데이터가 처리되게 할 수 있습니다. 이를 내부 반복(internal iteration) 이라고 합니다. 이를 리스트에서 고가의 거래만 필터링하는 예제로 작성해보겠습니다.

    import static java.util.stream.Collectors.toList; 
      Map<Currency, List<Transaction>> transactionByCurrencies =  transactions.stream() .filter((Transaction t) -> t.getPrice() > 1000) // 고가의 거래 필터링 .collect(groupingBy(Transaction::getCurrency)); // 통화로 그룹핑**

    추가적인 반복문이나 조건문 없이도 위와 같이 구현할 수 있습니다.

    이와 같이 스트림 API의 활용으로 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드문제멀티코어 활용의 어려움이라는 두가지 문제를 해결할 수 있습니다. 자세한 예제는 추 후에 스트림 API에 대해 자세히 설명하는 포스팅에서 보충하겠습니다.

    6. 디폴트 메서드

    디폴트 메서드는 더 쉽게 변화할 수 있는 인터페이스를 만들 수 있도록 추가한 기능입니다.

    이는 구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스가 포함할 수 있는 기능입니다. 이 기능을 위해 Java8에는 default 라는 새로운 키워드를 인터페이스 규격명세에 추가되어 있습니다. 하나의 예로 이전 버전의 자바에서는 List를 구현하는 모든 클래스가 sort 메서드를 구현해야 했지만 Java8부터는 디폴트 sort를 구현하지 않아도 됩니다.

      default void sort(Comparator<? super E> c){ Collections.sort(this, c);}

    7. 마무리

    그 외에도 NullPointer 예외를 피할 수 있는 Optional 클래스도 제공합니다. 이는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체입니다.

    지금까지 Java8이 현재의 형태로 릴리즈된 환경과 추가된 기능 및 개념에 대해 정리해봤습니다. 다음 포스팅 부터는 해당 기능들을 좀 더 자세히 보고 예제코드를 소개해 드리도록 하겠습니다.

    수정하거나 추가되어야 할 내용이 있다면 언제든 댓글이나 메일로 말씀해주세요 🙂

    항상 마무리가 어렵네요… 그러면 다음시간에 만나요 뿅

    반응형