티스토리 뷰
동작 파라미터화 코드 전달하기
- 동작 파라미터화에서는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달(일급 객체)
- 동작 파라미터화를 이용하면 변화하는 요구사항에 더 잘 대응할 수 있는 코드를 구현할 수 있으며 나중에 엔지니어링 비용을 줄일 수 있음
- 코드 전달 기법을 이용하면 동작을 메서드의 인수로 전달 가능 (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를 이용하면 단순히 값을 바꾸는 것 이상의 복잡한 동작을 수행할 수 있으며 자신만의 커스텀 날짜 변환 기능을 정의할 수 있음
- 날짜와 시간 객체를 특정 포맷으로 출력하고 파싱하는 포매터를 정의할 수 있음
- 패턴을 이용하거나 프로그램으로 포매터를 만들 수 있으며 포매터는 스레드 안정성을 보장
'책 소개' 카테고리의 다른 글
[요약] 도메인 주도 설계로 시작하는 마이크로서비스 개발 (0) | 2024.07.16 |
---|---|
[요약] 자바 ORM 표준 JPA 프로그래밍 (0) | 2024.07.11 |
[요약] 테스트 주도 개발 시작하기 (0) | 2024.06.25 |
[요약] 도메인 주도 개발 시작하기 (0) | 2024.06.22 |
마음챙김 (0) | 2023.11.07 |