티스토리 뷰

책 소개

[요약] 자바 8 인 액션

Devsong26 2024. 6. 30. 09:38

동작 파라미터화 코드 전달하기

  • 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달(일급 객체)
  • 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있음
  • 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달 가능 (ex, Predicate, Function, Supply, Consumer)

 

람다 표현식

  • 동작 파라미터화를 이용하면 더 유연하고 재사용할 수 있는 코드를 만들 수 있음
  • 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것
    • 보통의 메서드와 달리 이름이 없으므로 익명이라 표현
    • 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부름
    • 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있음(일급 객체)
    • 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없음
  • 람다 표현식은 다음과 같음
    • (Parameter) -> expression
    • (Parameter) -> { statements; }
  • 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스다.
  • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급 가능
  • 함수 디스크립터는 추상 메서드 시그니처
  • 실행 어라운드 패턴은 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 가짐

  • 대상 형식은 어떤 콘텍스트에서 기대되는 람다 표현식의 형식
  • 대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있음
  • 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있음
  • 자유 변수는 람다식의 파라미터로 넘겨진 변수가 아닌 외부 변수이며 람다식은 이를 사용할 수 있는데 이와 같은 동작을 람다 캡처링이라고 부름
  • 메서드 레퍼런스는 특정 메서드만을 호출하는 람다의 축약형
  • 메서드 레퍼런스를 만드는 방법
    • 정적 메서드 레퍼런스: Integer::parseInt
    • 다양한 형식의 인스턴스 메서드 레퍼런스: String::length
    • 기존 객체의 인스턴스 메서드 레퍼런스
  • ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 레퍼런스를 만들 수 있음
  • 람다 표현식은 익명 함수의 일종이며 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있음

 

스트림 소개

  • 스트림을 이용하면 멀티 스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있음
  • 스트림은 선언형으로 코드를 구현할 수 있음
  • filter(또는 sorted, map, collect) 같은 연산은 고수준 빌딩 블록으로 이루어져 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 사용할 수 있음
  • 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요가 없음
  • 스트림 API는 매우 비싼 연산
  • 자바 8 스트림 API의 특징
    • 선언형: 더 간결하고 가독성이 좋아짐
    • 조립할 수 있음: 유연성이 좋아짐
    • 병렬화: 성능이 좋아짐
  • 스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
  • 컬렉션의 주제는 데이터고 스트림의 주제는 계산
  • 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비
  • 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원
  • 대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환
  • 자바의 기존 컬렉션과 새로운 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공하며,
    '연속된' 이라는 표현은 순서와 상관없이 아무 값에나 접속하는 것이 아니라 순차적으로 값에 접근하는 것을 의미
  • 데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이
  • 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 함
  • 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조
  • 스트림은 생상자와 소비자 관계를 형성
  • 반복자와 마찬가지로 스트림도 한 번만 탐색할 수 있으며 탐색된 스트림의 요소는 소비됨
  • 외부 반복은 사용자가 직접 요소를 반복하는 것(for-each)
  • 내부 반복은 반복을 알아서 처리하고 결과 스트림 값을 어딘가에 저장해주는 것
  • 스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택
  • 외부 반복에서는 병렬성을 스스로 관리해야 함
  • 연결할 수 있는 스트림 연산을 중간 연산
  • 스트림을 닫는 연산을 최종 연산
  • 중간 연산은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않음(Lazy)
  • 쇼트서킷은 논리 연산에서 불필요한 계산을 생략하는 최적화 기법
  • 루프 퓨전은 filter와 map 처럼 서로 다른 연산이 한 과정으로 병합되는 것을 말함
  • 최종 연산은 스트림 파이프라인에서 결과를 도출
  • 스트림의 이용 과정은 아래와 같고 빌더 패턴과 비슷함
    • 질의를 수행할 (컬렉션 같은) 데이터 소스
    • 스트림 파이프라인을 구성할 중간 연산 연결
    • 스트림 파이프라인을 실행하고 결과를 만들 최종 연산

 

스트림 활용

  • 스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct라는 메서드도 지원
  • 소스가 정렬되어 있지 않았다면 limit의 결과도 정렬되지 않은 상태로 반환
  • 스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원
  • flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑
  • flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행
  • 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 이용
  • allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사
  • noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인
  • findAny 메서드는 현재 스트림에서 임의의 요소를 반환
  • Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스
  • 리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출하는 것
  • map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보내며, 내부 상태를 갖지 않는 연산
  • 스트림에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정되어 있음 
  • 기본형 특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하진 않음
  • range 메서드는 시작값과 종료값이 결과에 포함되지 않음
  • rangeClosed는 시작값과 종료값이 결과에 포함
  • Stream.iterate와 Stream.generate를 이용하여 크기가 고정되지 않은 무한 스트림을 만들 수 있음
  • 값을 계산하는 데 필요한 상태를 저장하거나 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장하는 메서드를 상태 있는 연산이라고 함

 

스트림으로 데이터 수집

  • Collectors에서 제공하는 메서드의 기능
    • 스트림 요소를 하나의 값으로 리듀스하고 요약
    • 요소 그룹화
    • 요소 분할
  • 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용되며 이를 요약 연산이라고 부름
  • 함등 함수는 자신을 그대로 반환함
  • 리듀싱 컬렉터는 절대 Optional.empty()를 반환하지 않으므로 안전한 코드
  • 분할은 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능
  • Characteristics는 collect 메서드가 어떤 최적화를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합을 제공
  • supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수
  • accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환
  • finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 함
  • combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의
  • collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법을 인수로 갖는 최종 연산
  • 스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최소값, 최대값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있음
  • 미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나 partitioningBy로 스트림의 요소를 분할할 수 있음
  • 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있음

 

병렬 데이터 처리와 성능

  • 병렬 스트림이란 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림
  • 병렬화를 이용하려면 스트림을 재귀적으로 분할해야 하고, 각 서브스트림을 서로 다른 스레드의 리듀싱 연산으로 할당하고, 이들 결과를 하나의 값으로 합쳐야 함
  • 코어 간에 데이터 전송 시간보다 훨씬 오래 걸리는 작업만 병렬로 다른 코어에서 수행하는 것이 바람직함
  • 병렬 스트림이 올바로 동작하려면 공유된 가변 상태를 피해야 함
  • 병렬 스트림 사용 시 주의 사항
    • 박싱을 주의하라. 자동 박싱과 언박싱은 성능을 크게 저하시킬 수 있는 요소
    • 순차 스트림보다 병렬 스트림에서 성능이 떨어지는 연산이 있음
    • 스트림에서 수행하는 전체 파이프라인 연산 비용을 고려해야 함
      • 처리해야 할 요소 수가 N이고 하나의 요소를 처리하는 데 드는 비용을 Q라고 하면 전체 스트림 파이프라인 처리 비용은 N * Q로 예상할 수 있으며, Q가 높아진다는 것은 병렬 스트림으로 성능을 개선할 수 있는 가능성이 있음을 의미
    • 소량의 데이터에서는 병렬 스트림이 도움 되지 않음
    • 스트림을 구성하는 자료구조가 적절한지 확인
    • 스트림의 특성과 파이프라인의 중간 연산이 스트림의 특성을 어떻게 바꾸는지에 따라 분해 과정의 성능이 달라질 수 있음
    • 최종 연산의 병합과정 비용을 살펴보라.
  • 스트림 소스와 분해성
소스 분해성
ArrayList 훌륭함
LinkedList 나쁨
IntStream.range 훌륭함
Stream.iterate 나쁨
HashSet 좋음
TreeSet 좋음
  • 포크/조인 프레임워크의 포크/조인 과정

  • join 메서드를 태스크에 호출하면 태스크가 생산하는 결과가 준비될 때까지 호출자를 블록시키므로 두 서브태스크가 모두 시작된 다음에 join을 호출해야 함
  • RecursiveTask 내에서는 ForkJoinPool의 invoke 메서드를 사용하지 말아야 함
  • 서브태스크에 fork 메서드를 호출해서 ForkJoinPool의 일정을 조절할 수 있음
  • 포크/조인 프레임워크를 이용하는 병렬 계산은 디버깅하기 어려움
  • 병렬 스트림에서 살펴본 것처럼 멀티코어에 포크/조인 프레임워크를 사용하는 것이 순차 처리보다 무조건 빠를 거라는 생각은 버려야 함
  • 코어 개수와 관계없이 적절한 크기로 분할된 많은 태스크를 포킹하는 것이 바람직함
  • 작업 훔치기 기법에서는 ForkJoinPool의 모든 스레드를 거의 공정하게 분할
  • 다른 스레드는 바쁘게 일하고 있는데 할일이 없어진 스레드가 있으면 유휴 상태로 빠지지 않고 다른 스레드 큐의 꼬리에서 작업을 훔쳐옴
  • Spliterator는 분할할 수 있는 반복자
  • 내부 반복을 이용하면 명시적으로 다른 스레드를 사용하지 않고도 스트림을 병렬로 처리할 수 있음
  • 간단하게 스트림을 병렬로 처리할 수 있지만 항상 병렬 처리가 빠른 것은 아님
  • 병렬 스트림으로 데이터 집합을 병렬 실행할 때 특히 처리해야할 데이터가 아주 많거나 각 요소를 처리하는 데 오랜 시간이 걸릴 때 성능을 높일 수 있음
  • 가능하면 기본형 특화 스트림을 사용하는 등 올바른 자료구조 선택이 어떤 연산을 병렬로 처리하는 것보다 성능적으로 더 큰 영향을 미칠 수 있음
  • 포크/조인 프레임워크에서는 병렬화할 수 있는 태스크를 작은 태스크로 분할한 다음에 분할된 태스크를 각각의 스레드로 실행하며 서브태스크 각각의 결과를 합쳐서 최종 결과를 생산

 

리팩토링, 테스팅, 디버깅

  • 익명 클래스에서 사용한 this와 super는 람다 표현식에서 다른 의미를 가짐
  • 익명 클래스에서 this는 익명 클래스 자신을 가리키지만 람다에서 this는 람다를 감싸는 클래스를 가리킴
  • 익명 클래스는 감싸고 있는 클래스의 변수를 가릴 수 있음
  • 익명 클래스를 람다 표현식으로 바꾸면 컨텍스트 오버로딩에 따른 모호함이 초래될 수 있음
  • 익명 클래스는 인스턴스화할 때 명시적으로 형식이 정해지는 반면 람다의 형식은 컨텍스트에 따라 달라지기 때문
  • 전략 패턴은 한 유형의 알고리즘을 보유한 상태에서 런타임에 적절한 알고리즘을 선택하는 기법
  • 알고리즘의 개요를 제시한 다음에 알고리즘의 일부를 고칠 수 있는 유연함을 제공해야 할 때 템플릿 메서드 디자인 패턴을 사용
  • 어떤 이벤트가 발생했을 때 한 객체가 다른 객체 리스트에 자동으로 알림을 보내야 하는 상황에서 옵저버 디자인 패턴을 사용
  • 한 객체가 어떤 작업을 처리한 다음에 다른 객체로 결과를 전달하고, 다른 객체도 해야 할 작업을 처리한 다음에 또 다른 객체로 전달하는 식을 의무체인이라고 함

 

디폴트 메서드

  • 델리게이션, 즉 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋음
  • 디폴트 메서드의 상속 해석 규칙 
    • 1. 클래스가 항상 이김
    • 2. 1번 규칙 이외에 상황에서는 서브 인터페이스가 이김
    • 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 함
  • 자바 8의 인터페이스는 구현 코드를 포함하는 디폴트 메서드, 정적 메서드를 정의할 수 있음
  • 디폴트 메서드의 정의는 default 키워드로 시작하며 일반 클래스 메서드처럼 바디를 가짐
  • 공개된 인터페이스에 추상 메서드를 추가하면 소스 호환성이 깨짐
  • 디폴트 메서드 덕분에 라이브러리 설계자가 API를 바꿔도 기존 버전과 호환성을 유지할 수 있음
  • 선택형 메서드와 동작 다중 상속에도 디폴트 메서드를 사용할 수 있음
  • 클래스가 같은 시그니처를 갖는 여러 디폴트 메서드를 상속하면서 생기는 충돌 문제를 해결하는 규칙이 있음
  • 두 메서드의 시그니처가 같고, 상속관계로도 충돌 문제를 해결할 수 없을 때는 디폴트 메서드를 사용하는 클래스에서 메서드를 오버라이드해서 어떤 디폴트 메서드를 호출할지 명시적으로 결정해야 함

 

null 대신 Optional

  • 역사적으로 프로그래밍 언어에서는 null 레퍼런스로 값이 없는 상황을 표현
  • 자바 8에서는 값이 있거나 없음을 표현할 수 있는 클래스 java.util.Optional<T>를 제공
  • 팩토리 메서드 Optional.empty, Optional.of, Optional.ofNullable 등을 이용해서 Optional 객체를 만들 수 있음
  • Optional 클래스는 스트림과 비슷한 연산을 수행하는 map, flatMap, filter 등의 메서드를 제공
  • Optional로 값이 없는 상황을 적절하게 처리하도록 강제할 수 있어 예상치 못한 null 예외를 방지할 수 있음
  • Optional을 활용하면 더 좋은 API를 설계할 수 있어 사용자는 메서드의 시그니처만 보고도 Optional값이 사용되거나 반환되는지 예측할 수 있음

 

CompletableFuture: 조합할 수 있는 비동기 프로그래밍

  • 비동기 계산을 모델링하는 데 Future를 이용할 수 있으며, Future는 계산이 끝났을 때 결과에 접근할 수 있는 레퍼런스를 제공
  • 시간이 걸릴 수 있는 작업을 Future 내부로 설정하면 호출자 스레드가 결과를 기다리는 동안 다른 유용한 작업을 수행할 수 있음
  • Future 메서드만으로는 간결한 동시 실행 코드를 구현하기에 충분치 않음
  • Future를 개선하기 위해서는 아래와 같은 선언형 기능이 필요
    • 두 개의 비동기 계산 결과를 하나로 합친다.
    • Future 집합이 실행하는 모든 태스크의 완료를 기다린다.
    • Future 집합에서 가장 빨리 완료되는 태스크를 기다렸다가 결과를 얻는다.
    • 프로그램적으로 Future를 완료시킨다.
    • Future 완료 동작에 반응한다.
  • 동기 API는 메서드를 호출한 다음에 메서드가 계산을 완료할 때까지 기다리며 동기 API를 사용하는 상황을 블록 호출이라고 함
  • 비동기 API는 메서드가 즉시 반환되며 끝내지 못한 나머지 작업을 호출자 스레드와 동기적으로 실행할 수 있도록 다른 스레드에 할당하며, 비동기 API를 사용하는 상황을 논블록 호출이라고 함
  • completeExceptionally 메서드를 이용해서 CompletableFuture 내부에서 발생한 예외를 클라이언트로 전달해야 함
  • 스레드 풀의 최적값을 찾기 위한 대략적인 CPU 활용 비율을 구하는 공식
    • N(threads) = N(cpu) * U(cpu) * (1 + W/C)
    • N(cpu)는 Runtime.getRuntime().availableProcessors()가 반환하는 코어 수
    • U(cpu)는 0과 1 사이의 값을 갖는 CPU 활용 비율
    • W/C는 대기시간과 계산시간의 비율
  • 스레드 수가 너무 많으면 오히려 서버가 크래시될 수 있으므로 하나의 Executor에서 사용할 스레드의 최대 개수는 100 이하로 설정하는 것이 바람직
  • thenCompose 메서드는 첫 번째 CompletableFuture 연산의 결과를 두 번째 CompletableFuture 연산으로 전달하여 CompletableFuture를 조합할 수 있음
  • thenCombine은 독립적으로 실행된 두 개의 CompletableFuture의 결과를 합쳐야 하는 상황일 때 사용
  • thenCombineAsync 메서드에서는 BiFunction이 정의하는 조합 동작이 스레드 풀로 제출되면서 별도의 태스크에서 비동기적으로 수행 
  • thenAccept는 CompletableFuture의 계산이 끝나면 값을 소비할 때 사용되는 Consumer를 인자로 받음
  • thenAccpetAsync 메서드는 CompletableFuture가 완료된 스레드가 아니라 새로운 스레드를 이용해서 Consumer를 실행
  • allOf 메서드가 반환하는 CompletableFuture에 join을 호출하면 원래 스트림의 모든 CompletableFuture의 실행 완료를 기다릴 수 있음
  • anyOf 메서드는 CompletableFuture 배열을 입력으로 받아서 처음으로 완료한 CompletableFuture의 값으로 동작을 완료함

 

새로운 날짜와 시간 API

  • DateFormat은 스레드 안전하지 않아서 두 스레드가 동시에 하나의 포매터로 날짜를 파싱할 때 예기치 못한 결과가 일어날 수 있음
  • 불변 클래스는 함수형 프로그래밍 그리고 스레드 안정성과 도메인 모델의 일관성을 유지하는 데 좋은 특징
  • 기존의 java.util.DateFormat 클래스와 달리 모든 DateTimeFormatter는 스레드에서 안전하게 사용할 수 있는 클래스
  • 자바 8 이전 버전에서 제공하는 기존의 java.util.Date 클래스와 관련 클래스에서는 여러 불일치점들과 가변성, 어설픈 오프셋, 기본값, 잘못된 이름 결정 등의 설계 결함이 존재
  • 새로운 날짜와 시간 API에서 날짜와 시간 객체는 모두 불변
  • 새로운 API는 각각 사람과 기계가 편리하게 날짜와 시간 정보를 관리할 수 있도록 두 가지 표현 방식을 제공
  • 날짜와 시간 객체를 절대적인 방법과 상대적인 방법으로 처리할 수 있으며 기존 인스턴스를 변환하지 않도록 처리 결과로 새로운 인스턴스가 생성됨
  • TemporalAdjuster를 이용하면 단순히 값을 바꾸는 것 이상의 복잡한 동작을 수행할 수 있으며 자신만의 커스텀 날짜 변환 기능을 정의할 수 있음
  • 날짜와 시간 객체를 특정 포맷으로 출력하고 파싱하는 포매터를 정의할 수 있음
  • 패턴을 이용하거나 프로그램으로 포매터를 만들 수 있으며 포매터는 스레드 안정성을 보장