반응형

1. 서론

이번 포스팅에서는 Chapter7의 병렬 데이터 처리와 성능 에 대해 진행하도록 하겠습니다.

 

 

2. 병렬 스트림

java 8 에서는 병렬 처리를 간편하게 제공하고 있습니다.

예로 컬렉션에서 parallelStream를 통해 간편히 병렬 스트림 처리가 가능합니다.

 

1) 순차 스트림을 병렬 스트림으로 변환하기

 

순차 스트림에 parallel 메서드를 통해 병렬스트림으로 변경이 가능합니다.

 

아래는 그 예제입니다.

 

public long parallelSum(long n) {
    return Stream.iterate(1L, i -> i +1)
            .limit(n)
            .parallel()
            .reduce(0L, Long::sum);
}

 

parallel 처리 동작방식은 각 쓰레드에게 분할한 청크를 전달하여 병렬로 수행하도록 하는 것입니다.

 

아래는 그림으로 수행과정을 나타낸 것입니다.

 

 

추가로, 병렬에서 순차 스트림으로 다시 바꿀때에는 sequential 메서드를 사용하면 됩니다.

 

만약, parallel과 sequential 두개를 모두 사용했을 때에는 최종적으로 호출된 메서드로 전체 스트림 파이프라인에 영향이 미치게 됩니다.

 

병렬스트림으로 사용하는 쓰레드는 ForkJoinPool 을 사용하며 갯수는 Runtime.getRuntime().availableProcessors() 의 반환값으로 결정됩니다.
전역적으로 쓰레드수를 변경하고 싶을때는 아래와 같이 시스템 설정을 해주시면 됩니다.
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12")

 

 

 

2) 병렬 스트림 효과적으로 사용하기

 

병렬 프로그래밍은 항상 유심히 문제점이 없는지 파악한 후 개발해야 합니다.

 

java에서 제공해주는 병렬 스트림도 마찬가지입니다.

parallel을 사용하는 부분은 올바른 처리가 이루어지는지와 성능은 올라갔는지 테스트를 해야합니다.

java의 스트림은 내부로직이 복잡하기 때문에, 무분별하게 사용하게 되면 성능이 더욱 악화가 될 수 있습니다.

 

Collectors를 통해 리듀싱 작업을 수행하는 경우에는 toConcurrentMap과 같이 ConcurrentMap 클래스가 리턴타입일때만 병렬로 수행이 됩니다.
결국, 다른 toList와 같은 리턴타입은 병렬로 수행되지 않으므로 parallelStream으로 처리할 경우 성능이 악화 될 수 있습니다.

 

병렬로 처리해야하는지에 대한 결정은 아래와 같은 부분이 도움이 될 수 있습니다.

 

  1. 확신이 서지 않는다면 직접 성능 측정.
  2. 박싱 타입 유의
  3. 순차스트림보다 병렬 스트림에서 성능이 떨어지는 연산 고려 - ex) limit과 같이 순서에 의존하는 연산.
  4. 스트림 전체 파이프라인 연산 비용 고려
  5. 소량 데이터의 경우 병렬 스트림 제외
  6. 스트림으로 구성하는 자료구조가 적절한지 고려
  7. 최종 연산의 병합 과정 비용 고려

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

3. 포크/조인 프레임워크

포크/조인 프레임워크는 자바 7에서 추가되었으며, 자바 8의 병렬스트림에서는 내부적으로 포크/조인 프레임워크로 처리하고 있습니다.

 

포크/조인 프레임워크는 재귀를 통해 병렬화 작업을 작게 분할한 다음 ForkJoinPool의 작업자 스레드에 분산 할당하는 방식입니다.

 

1) RecursiveTask 활용

 

스레드 풀을 이용하려면 RecursiveTask<R>의 서브클래스를 만들어야 합니다.

여기서 R은 제네릭으로 결과타입 또는 결과가 없을때의 타입을 의미합니다.

 

RecursiveTask를 구현하기 위해서는 추상메서드인 compute메서드를 구현해야합니다.

compute 메서드는 태스크를 서브태스크로 분할하는 로직과 더 이상 분할할 수 없을 때 개별 서브태스크의 결과를 생산할 알고리즘을 정의해야 합니다.

 

아래는 compute 메서드의 의사코드입니다.

 

if (태스크가 충분히 작거나 더 이상 분할 할 수 없으면) {
	순차적으로 태스크 계산
} else {
    태스크를 두 서브태스크로 분할
    태스크가 다시 서브태스크로 분할되도록 이 메서드를 재귀적으로 호출함
    모든 서브태스크의 연산이 완료될 때까지 기다림
    각 서브태스크의 결과를 합침
}

 

아래는 long[] 의 sum을 구하는 것을 포크/조인 프레임워크를 사용하는 예제입니다.

 

public static long forkJoinSum(long n) {
    long[] numbers = LongStream.rangeClosed(1, n).toArray();
    ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
    return new ForkJoinPool().invoke(task);
}

public class ForkJoinSumCalculator extends RecursiveTask<Long> {
    private final long[] numbers;
    private final int start;
    private final int end;
    public static final long THRESHOLD = 10_000;
    public ForkJoinSumCalculator(long[] numbers) {
        this(numbers, 0, numbers.length);
    }
    @Override
    protected Long compute() {
        int length = end - start;
        if(length <= THRESHOLD) {
            return computeSequentially();
        }
        ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length/2);
        leftTask.fork();
        
        ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length/2, end);
        
        long rightResult = rightTask.compute();
        long leftResult = leftTask.join();
        
        return leftResult + rightResult;
    }
    private long computeSequentially() {
        long sum = 0;
        for(int i = start; i< end; i++) {
            sum += numbers[i];
        }
        return sum;
    }
}

 

위 ForkJoinSumCalculator의 경우에는 배열의 길이가 10,000 보다 큰것은 반으로 자르면서 분할시키고 있습니다.

그리고 분할을 재귀로 계속 수행 후 결과를 모아서 반환하고 있습니다.

 

재귀를 통해 분할된 작업들은 ForkJoinPool에 넘겨져 병렬로 수행되어 집니다.

 

위 예제와 같이, 병렬로 수행 시 결과에 영향이 가지 않는 연산에서만 사용해야 합니다.

 

2) 포크/조인 프레임워크를 제대로 사용하는 방법

 

아래는 포크/조인 프레임워크를 효과적으로 사용하기 위해 알아야 할 점입니다.

 

  1. 두 서브 태스크가 모두 시작된 다음에 join을 호출해야 합니다.
  2. RecursiveTask 내에서는 ForkJoinPool의 invoke메서드 대신 fork나 compute 메서드를 직접 호출해야 합니다.
  3. 서브 태스크에서 fork나 compute를 통해 ForkJoinPool의 일정을 조절할 수 있습니다.
  4. 포크/조인 프레임워크를 이용하는 병렬계산은 디버깅하기 어렵습니다.
  5. 멀티코어에 포크/조인 프레임워크를 사용하는 것이 순차 처리보다 무조건 빠르지는 않습니다.

 

3) 작업 훔치기

 

멀티 쓰레드로 처리를 하다보면 고루 처리량을 할당했더라도, 각 쓰레드마다 완료 시점이 다릅니다.

이 경우, 노는 쓰레드가 발생하게되며 성능이 생각한것만큼 좋아지지 않게됩니다.

 

이를 위해, 포크/조인 프레임워크는 작업훔치기라는 기법을 사용하고 있습니다.

간단히 말해서, task들을 큐가 아닌 덱에 넣고 노는 쓰레드는 일하는 쓰레드의 덱의 꼬리에서 task가 있다면 훔쳐와 동작하는 것입니다.

 

그렇기 때문에, task는 적절히 작은 양으로 분배가 되도록 해야합니다.

 

4. Spliterator 인터페이스

자바 8은 Spliterator라는 새로운 인터페이스를 제공합니다.

 

Spliterator는 스트림을 분할하여 처리할때 사용하며,

자바 8은 컬렉션 프레임워크에 포함된 모든 자료구조에 사용할 수 있는 디폴트 Spliterator 구현을 제공하고 있습니다.

 

아래는 Spliterator 인터페이스에서 필수로 구현해야하는 메서드만을 모아놓은 명세입니다.

 

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
}

 

tryAdvance 는 Spliterator의 요소를 하나씩 순차적으로 소비하면서 탐색해야 할 요소가 남아 있으면 참을 반환합니다.

trySplit Spliterator의 일부 요소를 분할해서 두 번째 Spliterator를 생성하는 메서드입니다.

estimateSize 는 메서드로 탐색해야 할 요소 수 정보를 제공할 수 있습니다.

characteristicsSpliterator에서 정의한 int형으로 각 값은 의미하고 있는것이 있습니다. - ex) 16 = Spliterator.ORDERED

 

 

1) 분할 과정

 

Spliterator는 trySplit 메서드를 통해 스트림 요소를 재귀적으로 분할합니다.

 

아래는 Spliterator를 통해 분할하는 과정의 그림입니다.

 

 

이 과정은 characteristics 메서드로 정의하는 Spliterator의 특성에 영향을 받습니다.

 

2) Spliterator 특성

 

characteristics 메서드는 Spliterator 자체의 특성 집합을 포함하는 int를 반환합니다.

 

아래는 Spliterator 특성에 대한 표입니다.

 

특성 의미
ORDERED 리스트처럼 요소에 정해진 순서가 있으므로 순서에 유의해야 함.
DISTINCT x, y 두 요소를 방문했을 시 x.equals(y) 는 항상 false를 반환해야 함.
SORTED 탐색된 요소는 미리 정의된 정렬 순서를 따라야 함.
SIZED 크기가 알려진 소스로 Spliterator를 생성했으므로 estimateSize는 정확한 값을 반환함.
NON-NULL 탐색하는 모든 요소는 null이 아님.
IMMUTABLE Spliterator는 불변. 즉 탐색 중간에 추가, 삭제가 불가능함.
CONCURRENT 동기화 없이 Spliterator의 소스를 여러 쓰레드에서 동시에 고칠 수 없음.
SUBSIZED Spliterator 그리고 분할되는 모든 Spliterator는 SIZED 특성을 갖고 있음.

 

5. 마무리

이번 포스팅에서는 Chapter 7인 병렬 데이터 처리와 성능에 대해 진행하였습니다.

다음에는 Chapter 8인 컬렉션 API 개선에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(9) 리팩터링, 테스팅, 디버깅  (0) 2020.04.13
(8) 컬렉션 API 개선  (0) 2020.04.13
(6) 스트림으로 데이터 수집  (0) 2020.04.04
(5) 스트림 활용  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
반응형

1. 서론

이번 포스팅에서는 Chapter6의 스트림으로 데이터 수집 에 대해 진행하도록 하겠습니다.

 

2. 컬렉터란 무엇인가?

Collector 는 java.util.stream 에 있는 인터페이스로, 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합입니다.

앞장에서 많이 사용한 스트림 함수 중 collect는 이 Collector 인터페이스를 인수로 받습니다.

 

그럼 앞장에서 자주 인수로 전달한 Collectors는 Collector의 구현체일까요? 아쉽지만 아닙니다.

 

코드를 깊게 파보시면 바로 아시겠지만 Collector의 구현체는 CollectorImpl 입니다.

 

그럼 대체 Collectors는 왜 가능한걸까요?

 

코드를 자세히 보시면 Collectors는 일종의 유틸 클래스로 inner class로 CollectorImpl를 가지고 있습니다.

그리고 toList와 같은 정적 메서드가 호출될때마다 이 CollectorImpl를 만들어 반환하는 형태입니다.

 

 

1) 고급 리듀싱 기능을 수행하는 컬렉터

 

스트림 함수인 collect의 동작방식은 인자로 받은 Collector로 리듀싱 연산을 수행하여 결과를 반환합니다.

 

일반적으로 많이 사용하는 toList, toSet 등등을 모아둔 유틸 클래스가 Collectors로 보시면 됩니다.

 

일반적으로 Collectors는 앞절에서 살펴본 sum, max와 같은 연산이 아닌
요소를 어떤 자료구조에 담아 반환할지의 리듀싱 기능을 제공합니다.

책에서는 이를 고급 리듀싱기능으로 분류하였습니다.

 

2) 미리 정의된 컬렉터

 

Collectors는 정적 팩토리 메서드 패턴을 통해 자주 사용하는 CollectorImpl를 제공하고 있습니다.

책에서는 이를 미리 정의된 컬렉터라고 일컫습니다.

 

Collectors는 아래와 같이 크게 3가지를 제공합니다.

 

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할

3. 리듀싱과 요약

Collectors에서 제공하는 정적 메서드들은 결국 스트림의 최종 연산으로 취급이 되며, reduce 작업을 하여 만들어지게 됩니다.

 

Collectors에서 제공하는 reduce 역할을 수행하는 정적 메소드들에 대해 소개하겠습니다.

 

1) counting

 

이 메서드는 Collectors가 제공하는 컬렉터로 요소의 갯수를 세어 반환합니다.

 

아래는 예제입니다.

 

long howManyDishes = menu.stream().collect(Collectors.counting());

 

 

2) maxBy, minBy

 

maxBy, minByCollectors가 제공하는 컬렉터로 요소 중 최댓값, 최솟값을 찾을때 사용합니다.

최대인지 최소인지 구별하기 위해 인자로는 Comparator를 받습니다.

 

아래는 예제입니다.

 

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));

 

3) 요약 연산

 

Collectors는 합계, 평균을 위한 요약 연산도 제공한다.

 

메서드는 아래와 같습니다.

단, 인수는 언박싱인 int, long, double 타입으로 반환하는 함수형 인터페이스의 구현체 혹은 람다여야 합니다.

 

  • summingInt
  • summingDouble
  • summingLong
  • averagingInt
  • averagingDouble
  • averagingLong

사용 예제는 아래와 같습니다.

 

int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));

 

만약 한번의 collect API로 합계, 평균 등의 값을 한번에 수행해야 할 경우에는 아래와 같은 메서드를 사용하면 됩니다.

 

  • summarizingInt
  • summarizingDouble
  • summarizingLong

아래는 예제 및 결과입니다.

 

IntSummaryStatistics menuStatistics = menu.stream()
            .collect(Collectors.summarizingInt(Dish::getCalories));

System.out.println(menuStatistics);

/*
 * 결과
*/
IntSummaryStatistics{count=?, sum=?, average=?, min=?, max=?}

 

 

4) 문자열 연결

 

Collectors에서 제공하는 joining 메서드를 사용하면,

스트림에 각 요소에 toString 메서드를 호출하여 하나의 문자열로 연결하여 반환합니다.

 

추가로, joining 메서드는 문자열을 연결할 때 사용할 구분자를 인자로 받을 수있습니다.

 

아래는 예제입니다.

 

String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));

// pork, beef, chicken, ...

 

5) 범용 리듀싱 요약 연산

 

지금까지 알아본 메서드는 사실 Collectors.reducing의 메서드를 이용하여 특화된 값을 도출하도록 만들은 것입니다.

 

이는 단순히, 개발 편의와 가독성을 위해 제공한 것입니다.

 

그럼, 특화된것이 아니라 커스텀을 해야한다면 스스로 reducing 메서드를 사용해야 합니다.

 

때문에 reducing 메서드에 대해 알아보겠습니다.

 

reducing 메서드는 인자로 3개를 받고 있습니다.

 

  • 첫번째 인자 = 리듀싱 연산의 초기값입니다.
  • 두번째 인자 = 각 요소에 적용할 변환 함수입니다.
  • 세번째 인자 = 같은 종류의 요소를 하나의 요소로 합칠 BinaryOperator입니다.
reducing 메서드는 인자를 한개만 받을 수도 있습니다.
이 경우는 첫번째 인자가 스트림 요소의 첫번째가 되고, 두번째 인자는 자신을 그대로 반환하는 Function.identity()가 됩니다.
단, 스트림이 비어있는 경우가 있기 때문에 결과값으로는 Oprional이 씌어지게 됩니다.

 

아래는 summingInt 를 reducing을 통해 구현한 예제입니다.

 

int totalCalories = menu.stream().collect(Collectors.reducing(
        0, // 초기값
        Dish::getCalories, // 변환 함수
        Integer::sum // 누적 함수
));

 

 

 

 

반응형

 

 

 

 

 

4. 그룹화

데이터 처리를 하다보면 그룹핑이 필요한 경우가 있습니다.

이를 위해 Collectors 는 groupingBy와 같은 그룹핑을 할수 있도록 제공합니다.

 

아래는 각 Dish의 타입별로 그룹핑하는 예제입니다.

 

Map<Dish.Type, List<Dish>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(Dish::getType));
        
// 결과
{
    FISH=[prawns, salmon], 
    OTHER=[french fries, rice, season fruit, pizza], 
    MEAT=[pork, beef, chicken]
}

 

Dish::getType과 같이 그룹화되는 기준을 제공하는 함수를 분류함수라고 합니다.

 

만약 분류함수가 복잡하다면 코드블록을 사용하여 커스텀하면 됩니다.

 

아래는 각 칼로리값에 따라 분류를 나누는 예제입니다.

 

public enum CaloricLevel {DIET, NORMAL, FAT}

Map<Dish.Type, List<Dish>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(
                dish -> {
                    if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                    else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                    else return CaloricLevel.FAT;
                }));

 

 

1) 그룹화된 요소 조작

 

그룹화 진행 후에 각 그룹화된 요소를 조작해야 하는 경우가 있습니다.

이를 위해 Collectors.groupingBy는 추가로 Collector 인자를 받을수 있습니다.

 

이는, 첫번째 인자를 통해 그룹핑이 진행 된 후, 두번째 인자인 Collector를 적용하게 됩니다.

 

Collectors는 또 미리 자주 사용하는 mapping 함수를 제공합니다.

java 9 부터는 Collectors 에 filtering, flatMapping이 추가되었습니다.
[java docs 9] 참조 

 

아래는 타입별로 그룹핑한 요리 리스트가 아닌 요리명 리스트로 바꾼 예제입니다.

 

Map<Dish.Type, List<String>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(
                Dish::getType,
                Collectors.mapping(Dish::getName, Collectors.toList())
                ));

 

2) 다수준 그룹화

 

위에서 설명했듯이 groupingBy는 두번째 인수로 Collecto를 받습니다.

 

그 말은, 두번째 인수로 또 groupingBy를 받을 수 있습니다.

 

결국, n차원의 그룹화가 가능하다는 말로 직결됩니다.

 

아래는 2 수준으로 그룹핑하는 예제입니다.

 

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(
                Dish::getType,
                Collectors.groupingBy(
                        dish -> {
                            if (dish.getCalories() <= 400) {
                                return CaloricLevel.DIET;
                            } else if (dish.getCalories() <= 700) {
                                return CaloricLevel.NORMAL;
                            } else {
                                return CaloricLevel.FAT;
                            }
                        }
                )
        ));

// 결과
{
    FISH = {
        NORMAL=[salmon], 
        DIET=[prawns]
    }, 
    MEAT = {
        FAT=[pork], 
        NORMAL=[beef], 
        DIET=[chicken]
    }, 
    OTHER = {
        NORMAL=[french fries, pizza], 
        DIET=[rice, season fruit]
    }
}

 

3) 서브그룹으로 데이터 수집

 

groupingBy 의 두번째 인자로 Collector를 인자로 받는다는 것은 원하는 데이터 수집도 가능하다는 말이 됩니다.

 

대표적으로 아래와 같습니다.

 

  • counting = 그룹별 요소의 갯수를 수집
  • maxBy = 그룹별 주어진 기준의 최댓값을 가진 요소 수집
  • summingInt = 그룹별 합계 값 수집
maxBy의 경우 반환값이 optional로 감싸게 됩니다.
만약 empty가 없는 경우에는 Collectors.collectingAndThen 을 통해 Optional.get을 적용하여 감싸지 않은 형태로도 가능합니다.

 

5. 분할

스트림에서의 분할은 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능으로 분할 함수로 일컫습니다.

 

프레디케이트가 분류함수이기 때문에 키는 Boolean 값으로 그룹핑은 최대 2개가 됩니다.

 

Collectors에서는 partitioningBy 메서드로 이를 제공합니다.

 

아래는 Collectors.partitioningBy 예제입니다.

 

Map<Boolean, List<Dish>> partitionedMenu = 
        menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));

 

Collectors.partitioningBy 또한 groupingBy와 같이 두번째 인수로 Collector 를 받을 수 있어 다양한 커스텀 연산이 가능합니다.

 

6. Collector 인터페이스

Collector 인터페이스의 시그니처와 다섯개의 메서드 정의는 아래와 같습니다.

 

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    Set<Characteristics> characteristics();
}

 

각 제네릭은 아래와 같은 용도입니다.

  • T = 수집될 스트림 항목
  • A = 누적자로 수집 과정에서 중간 결과를 누적하는 객체 타입
  • R = 수집 연산의 결과 객체 타입 (대게 컬렉션 타입입니다.)

 

각 메서드의 용도는 아래와 같습니다.

 

1) supplier 메서드 : 새로운 결과 컨테이너 만들기

 

supplier 메서드는 빈 결과로 이루어진 Supplier를 반환해야 합니다.

즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터 함수입니다.

 

toList와 같은 컬렉터는 ArrayList::new 가 supplier입니다.

 

2) accumulator 메서드 : 결과 컨테이너에 요소 추가하기

 

acuumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환합니다.

 

함수의 반환값은 void입니다.

 

toList 컬렉터의 acuumulator는 List::add입니다.

 

3) finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기

 

finisher 메서드는 스트림 탐색이 끝나고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 합니다.

 

toList 컬렉터의 finisher는 누적자 객체가 이미 반환 형태이기 때문에 Function.identity() 입니다.

 

사실상, toList는 위 3가지로도 기능 구현이 가능합니다.
다만, 병렬성을 통한 최적화 판단 및 구현이 있어 내부적으로는 더욱 복잡하며
이를 위해 combiner 메서드와 characteristics 가 있습니다.

 

4) combiner 메서드 : 두 결과 컨테이너 병합

 

combiner는 스트림의 서로 다른 서브 파트를 병렬로 처리할때 누적자가 이 결과를 어떻게 처리할지 정의합니다.

 

toList 컬렉터에서는 비교적 간단합니다.

왜냐하면, 2개의 리스트를 이으기만 하면 되기 때문입니다.

 

아래는 toList 컬렉터의 combiner 입니다.

 

(left, right) -> { left.addAll(right); return left;

 

이 메서드를 통해 병렬로 수행이 가능하게 됩니다.

 

아래는 병렬화 리듀싱 과정을 간단하게 보여주는 그림입니다.

 

 

5) Characteristics 메서드

 

characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환합니다.

 

characteristics는 스트림을 병렬로 리듀스 할 것인지, 병렬로 한다면 어떤 최적화를 선택해야 하는지 힌트를 제공합니다.

 

Characteristics는 enum 클래스로 아래 3개의 값을 가지고 있습니다.

 

  • CONCURRENT = 다중 스레드에서 accumulator 함수를 동시에 호출할 수 잇으며 이 컬렉터는 병렬 리듀싱이 가능함.
    • 컬렉터 플래그에 UNORDERED를 함께 설정하지 않은 경우, 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱 가능
  • UNORDERED = 리듀싱 결과는 스트림 요소의 방문 순서나 누적순서에 영향을 받지 않음.
  • IDENTITY_FINISH = finisher 메서드가 반환하는 함수는 단순히 identify 이므로 생략 가능.

toList의 경우에는 IDENTITY_FINISH 입니다.

 

그 이유는 accumulator로 List::add를 사용하는데 병렬로 처리 시 순서가 엉킬수 있기 때문입니다.

그 말은, 누적 순서에도 영향이 있다는 말이 됨으로 CONCURRENT와 UNORDERED는 해당 사항이 되지 않습니다.

 

7. 마무리

 

이번 포스팅에서는 Chapter 6인 스트림으로 데이터 수집에 대해 진행하였습니다.

다음에는 Chapter 7인 병렬 데이터 처리와 성능에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(8) 컬렉션 API 개선  (0) 2020.04.13
(7) 병렬 데이터 처리와 성능  (0) 2020.04.11
(5) 스트림 활용  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
(3) 람다 표현식  (0) 2020.03.28
반응형

1. 서론

이번 포스팅에서는 Chapter5의 스트림 활용 에 대해 진행하도록 하겠습니다.

 

2. 필터링

java 8에서는 스트림의 요소를 선택하는 필터링 기능을 제공합니다.

 

소개할 기능으로는 filter와 distinct 입니다.

 

1) 프레디케이트로 필터링

 

스트림 인터페이스는 filter 메서드를 지원하고 있습니다.

이 메서드는 인자로 프레디케이트를 받고, 프레디케이트와 일치하는 요소를 포함하는 스트림을 반환합니다.

 

아래는 예제입니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    enum Type {
        MEAT, FISH, OTHER
    }
}

public static void main(String[] args) {
    List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT),
            new Dish("beef", false, 700, Dish.Type.MEAT),
            new Dish("chicken", false, 400, Dish.Type.MEAT),
            new Dish("french fries", true, 530, Dish.Type.OTHER),
            new Dish("rice", true, 350, Dish.Type.OTHER),
            new Dish("season fruit", true, 120, Dish.Type.OTHER),
            new Dish("pizza", true, 550, Dish.Type.OTHER),
            new Dish("prawns", false, 300, Dish.Type.FISH),
            new Dish("salmon", false, 450, Dish.Type.FISH)
    );

    List<Dish> vegetarianMenu = menu.stream()
            .filter(Dish::isVegetarian)
            .collect(Collectors.toList());
}

 

2) 고유 요소로 필터링

 

스트림에서는 distinct 메서드를 지원합니다.

 

이 메서드는 고유한 요소들로 이루어진 스트림을 반환합니다.

고유를 판단하는 기준은 java의 hashCode, equals로 결정됩니다.

 

아래는 예제입니다.

 

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
        .filter(i-> i %2 == 0)
        .distinct()
        .forEach(System.out::println);

 

3. 스트림 슬라이싱

스트림에서는 특정 요소를 선택하거나 스킵하는 방법을 제공하며 이를 슬라이싱이라고 일컫습니다.

 

1) 프레디케이트를 이용한 슬라이싱

 

자바 9에서는 스트림의 필터측면으로 특화된 takeWhile, dropWhile를 제공하고 있습니다.

 

takeWhile는 filter와 동일하게 작동하는것 같지만, 처음으로 프레디케이트가 거짓이 되면 종료하게 됩니다.

 

예제는 아래와 같습니다.

 

List<Dish> specialMenu = Arrays.asList(
        new Dish("season fruit", true, 120, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("rice", true, 350, Dish.Type.OTHER),
        new Dish("chicken", false, 400, Dish.Type.MEAT),
        new Dish("french fries", true, 530, Dish.Type.OTHER)
);

List<Dish> sliceMenu1 =
        specialMenu.stream()
        .takeWhile(dish -> dish.getCalories() < 320)
        .collect(Collectors.toList());

 

위 예제는 칼로리가 320보다 큰 rice를 만나는 시점까지만을 스트림으로 반환하게 됩니다.

filter를 통해 모든 요소에 대해 프레디케이트를 적용하게되어 성능에 큰 이슈가 있을 시 사용하면 됩니다.
단, 위와 같이 sort가 되어 있는것처럼 데이터의 상태를 자세히 보고 사용해야 합니다.

 

dropWhile 은 takeWhile과 반대로 프레디케이트가 거짓이 되는 지점까지 발견된 요소를 버립니다.

 

예제는 아래와 같습니다.

 

List<Dish> specialMenu = Arrays.asList(
        new Dish("season fruit", true, 120, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("rice", true, 350, Dish.Type.OTHER),
        new Dish("chicken", false, 400, Dish.Type.MEAT),
        new Dish("french fries", true, 530, Dish.Type.OTHER)
);

List<Dish> sliceMenu2 =
        specialMenu.stream()
        .dropWhile(dish -> dish.getCalories() < 320)
        .collect(Collectors.toList());

 

이번에는 거짓이 되는 rice 이전 요소는 모두 버리게 됩니다. 

 

 

2) 스트림 축소

 

스트림은 특정 요소갯수만을 가진 스트림을 반환하는 limit 메서드를 제공합니다.

ansiansi sql의 limit과 같이 생각하면 됩니다.

 

예제는 아래와 같습니다.

 

List<Dish> dishes = specialMenu.stream()
        .filter(dish -> dish.getCalories() > 300)
        .limit(3)
        .collect(Collectors.toList());

 

3) 요소 건너뛰기

 

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip 메서드를 제공합니다.

 

예제는 아래와 같습니다.

 

List<Dish> dishes = specialMenu.stream()
	    .filter(dish -> dish.getCalories() > 300)
	    .skip(2)
	    .collect(Collectors.toList());

 

4. 매핑

스트림에서는 자바 객체 타입을 변경하는 스트림을 반환하는 map, flatMap을 제공합니다.

 

1) 스트림의 각 요소에 함수 적용하기

 

스트림에 들어있는 모든 요소에 함수를 적용하고 싶다면 map을 사용하면 됩니다.

foreach도 가능하지만 foreach의 경우에는 반환값이 void인것을 염두하여 사용해야 합니다.
반면 map의 경우에는 스트림을 반환하기 때문에 다른 스트림 메서드를 붙일 수 있습니다.

 

예제는 아래와 같습니다.

 

List<Integer> dishNameLengths = menu.stream()
        .map(Dish::getName)
        .map(String::length)
        .collect(Collectors.toList());

 

2) 스트림 평면화

 

스트림 처리를 하다보면 2개이상의 스트림을 평면화된 스트림으로 만들어야 하는 경우가 있습니다.

 

이를 제공하는것이 flatMap입니다.

 

책에서는 {"Hello", "World"}와 같은 문자열 배열에서 고유문자로 이루어진 {"H", "e", "l", "o", "W", "r", "d"} 인 스트림을 만드는 예제가 있습니다.

 

처음으로 단순히 앞에서 배운 map, distinct를 사용하는 케이스를 생각할 수 있습니다.

 

코드는 아래와 같습니다.

 

List<String> words = Arrays.asList("Hello", "World");
words.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());

 

하지만 원하는 결과는 나오지 않게 됩니다.

이유로는 map으로 반환되는 스트림의 객체 타입은 String[]이지 String이 아니기 때문에 distinct가 제대로 동작하지 않기 때문입니다.

 

아래는 flatMap를 사용한 코드입니다.

 

List<String> words = Arrays.asList("Hello", "World");
words.stream()
        .map(word -> word.split(""))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());

 

이번에는 flatMap을 통해 Stream<String[]>을 Stream<String>으로 스트림 안에 있는

String 배열을 평면화시킨 스트림으로 변경한 후 distinct 메소드를 수행하도록 하였습니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

5. 검색과 매칭

스트림 API에서는 스트림에 특정 요소가 있는지 검색하는 기능도 제공하고 있습니다.

 

대표적으로는 아래와 같습니다.

 

  • allMatch
  • anyMatch
  • noneMatch
  • findFirst
  • findAny

1) 프레디케이트가 적어도 한 요소와 일치하는지 확인

 

주어진 스트림에서 적어도 한개 요소가 프레디케이트에서 참인지 확인하는 메서드로 anyMatch를 제공합니다.

확인 용도이기 때문에 반환값은 boolean 입니다.

 

예제는 아래와 같습니다.

 

if(menu.stream().anyMatch(Dish::isVegetarian)) {
    ...
}

 

2) 프레디케이트가 모든 요소와 일치하는지 검사

 

anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트에서 참인지 확인하는 allMatch가 있습니다.

 

예제는 아래와 같습니다.

 

boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000);

 

3) NONEMATCH

 

allMatch와 반대로 스트림의 요소가 모두 프레디케이트의 거짓인지 확인하는 noneMatch 메서드가 있습니다.

 

예제는 아래와 같습니다.

 

boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000);

 

4) 요소 검색

 

스트림에서 확인용 혹은 샘플링을 위해 임의의 한 요소만을 필요로 할때가 있습니다.

 

이를 위해 findAny 메서드를 지원하고 있습니다.

 

이 메서드는 주어진 스트림에서 임의로 한개를 반환합니다.

 

findAny는 주어진 스트림에 요소가 한개도 없을 수 있기 때문에 Optional로 감싸 반환합니다.

 

예제는 아래와 같습니다.

 

Optional<Dish> dish = menu.stream()
        .filter(Dish::isVegetarian)
        .findAny();

 

5) 첫 번째 요소 찾기

 

 findAny는 임의로 한개를 반환하지만 첫번째 요소가 필요한 경우가 있습니다.

이를 위해 findFirst 메서드를 지원하고 있습니다.

 

이는 스트림의 첫번째 요소를 반환하며 findAny와 같이 Optional로 감싼 반환값을 반환합니다.

 

예제는 아래와 같습니다.

 

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
        .map(n -> n * n)
        .filter(n -> n % 3 == 0)
        .findFirst();

 

6. 리듀싱

스트림에서는 모든 요소를 통해 값으로 도출하는 연산인 리듀싱을 지원합니다.

 

대표적으로 아래와 같은 일들을 할 수 있습니다.

 

  • 요소의 합/ 곱
  • 최댓값/ 최솟값

 

1) 요소의 합/ 곱

 

스트림 요소의 합 혹은 곱을 구할때는 아래와 같이 할 수 있습니다.

 

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum2 = numbers.stream().reduce(0, Integer::sum);
int multiple = numbers.stream().reduce(1, (a, b) -> a * b);

 

 

위에는 스트림의 합과 곱을 구하는 reduce 연산입니다.

reduce 연산의 경우 첫번째 인자로 초기값을 설정 가능합니다.

초깃값의 경우 생략이 가능합니다 .
단, 생략하는 경우 빈 스트림의 경우가 있기 때문에 반환값은 Optional을 감싼 형태가 됩니다.

 

두번째 인자로는 두 요소를 조합해 새로운 값을 만드는 BinaryOperator를 받습니다.

이 두 요소의 첫번째는 이전 BinaryOperator로 나온 결과값이며 두번째는 스트림의 값입니다.

 

결국, reduce는 점진적으로 스트림의 값에 BinaryOperator를 적용하여 값을 한개로 만들어 반환합니다.

 

2) 최댓값/ 최솟값

 

위와 같은 맥락으로 스트림 요소중에 최댓값과 최솟값도 도출할 수 있습니다.

 

코드는 아래와 같습니다.

 

int max = numbers.stream().reduce(0, Integer::max);
int min = numbers.stream().reduce(0, Integer::min);

 

 

7. 상태가 있는 스트림, 없는 스트림

스트림 연산을 보다보면 map, filter와 같이 이전 스트림의 값을 저장해야할 필요가 없는 메서드가 있습니다.

이를, 상태를 갖지 않는 스트림 연산이라고 일컫습니다.

 

하지만, reduce, sum, max, sorted, distinct와 같이 이전 스트림의 값을 알아야만 처리가 가능한 메서드가 있습니다.

이는, 상태를 갖는 스트림 연산이라고 일컫습니다.

 

이 상태를 갖는 연산의 경우에는 내부 버퍼에 값을 저장하고 사용하게 됩니다.

 

8. 숫자형 스트림

스트림은 모두 제네릭 타입을 가지고 있습니다.

제네릭에는 기본적으로 박싱타입만을 선언해야 합니다.

 

그로인해, 스트림 연산중에는 박싱 -> 언박싱의 오버헤드가 발생하게 됩니다.

스트림에서는 이를 위해 기본형 특화 스트림을 제공하고 있습니다.

 

1) 숫자 스트림으로 매핑

 

숫자 특화 스트림으로 변환시에는 mapToInt, mapToDouble, mapToLong 을 사용하면 됩니다.

3개 메서드는 각각 IntStream, DoubleStream, LongStream을 반환합니다.

 

위와 같은 메서드를 통해 아래와 같이 숫자에 특화된 스트림 메서드가 바로 가능하다는 점입니다.

  • sum
  • max
  • min
  • average

아래는 예제 코드입니다.

 

int calories = menu.stream().mapToInt(Dish::getCalories).sum();

 

2) 객체 스트림 복원

 

스트림은 반대로 기본 특화 스트림에서 박싱형 기본 스트림으로도 변경하는 boxed 메서드를 제공합니다.

 

예제는 아래와 같습니다.

 

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

 

3) 특화형 Optional

 

스트림과 같이 Optional도 기본 특화형이 있습니다.

 

대표적으로 OptionalInt, OptionalDouble, OptionalLong 이 있습니다.

 

사용 예제는 아래와 같습니다.

 

OptionalInt  maxCalories = menu.stream().mapToInt(Dish::getCalories).max();

 

4) 숫자 범위

 

개발을 하다보면 특정 범위의 숫자를 이용해야 하는경우가 있습니다.

대표적으로 for(int i=0; i< 100; i++) 와 같은 연산을 들 수 있습니다.

 

스트림에서도 이를 위해, range와 rangeClosed 함수를 제공합니다.

 

두 함수는 IntStream, LongStream에서 사용가능합니다.

 

두 함수 모두 2개의 인자를 받으며 첫번째 인자는 초기값, 두번째 인자는 종료값을 의미합니다.

 

차이 점으로는 range는 시작값과 종료값은 결과에 포함되지 않고, rangeClosed는 초기값, 종료값이 포함된다는 점입니다.

 

예제는 아래와 같습니다.

 

IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());

 

9. 스트림 만들기

스트림은 위와 같이 컬렉션에서 생성이 가능합니다.

 

하지만, 컬렉션 이외에도 일련의 값, 배열, 파일 등으로도 생성이 가능합니다.

 

이제 스트림 만드는 각 종류를 소개합니다.

 

1) 값으로 스트림 만들기

 

Stream.of를 통해 임의의 값으로 스트림을 만들 수 있습니다.

 

예제는 아래와 같습니다.

 

Stream<String> stream = Stream.of("Modern ", "Java ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

 

요소가 없는 빈 스트림을 만들자 할때는 Stream.empty를 사용하시면 됩니다.

 

Stream<String> emptyStream = Stream.empty();

 

2) null이 될 수 있는 객체로 스트림 만들기

 

개발을 하다보면 null 값도 스트림으로 만들어야 하는 경우가 생깁니다.

이를 위해, 자바 9에서는 ofNullable 메서드를 제공합니다.

 

예제는 아래와 같습니다.

 

Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));

 

3) 배열로 스트림 만들기

 

컬렉션이 아닌 배열에서도 Arrays.stream을 통해 스트림을 만들 수 있습니다.

 

예제는 아래와 같습니다.

 

int[] numbers  = {1, 2, 3, 4 ,5};
int sum = Arrays.stream(numbers).sum();

 

4) 파일로 스트림 만들기

 

파일 처리의 I/O 연산을 위해 사용하는 java.nio.file 에서도 스트림 처리가 가능하도록 업데이트 되었습니다.

 

예제는 아래와 같습니다.

 

long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
            .distinct()
            .count();
} catch (IOException e) {
    e.printStackTrace();
}

 

파일의 고유 단어들이 몇개인지 찾는 예제입니다.

 

5) 함수로 무한 스트림 만들기

 

스트림 API에서는 무한 스트림을 만들 수 있는 Stream.iterate와 Stream.generate를 제공합니다.

 

무한 스트림을 만들수 는 있지만, 보통 limit을 통해 무한으로 사용하는 것을 방지합니다.

 

1. Stream.iterate

 

iterate 정적 메서드는 초깃값과 람다를 인수로 받아 새로운 값을 무한으로 생성할 수 있습니다.

이러한 스트림을 언바운드 스트림이라고 일컫습니다.

 

아래는 예제 코드입니다.

 

Stream.iterate(0, n -> n + 2)
        .limit(10)
        .forEach(System.out::println);

 

자바 9에서는 iterate 함수에 프레디케이트를 지원합니다.

 

아래는 0부터 4씩 증가하며 100보다 작은 값으로 스트림을 생성하는 예제입니다.

 

Stream.iterate(0, n -> n < 100, n -> n + 4)
	.forEach(System.out::println);

 

두번째 인자로 프레디케이트가 들어간것을 볼 수 있습니다.

 

2. Stream.generate

 

generate는 iterate와 같이 무한 스트림을 생성합니다.

 

차이점으로는 인수로 Supplier를 받는 다는 점입니다.

 

결국, generate는 무한히 지정한 Supplier를 호출하여 반환된 값으로 스트림을 생성하게 됩니다.

 

이는 iterate와 큰 차이를 가져오게 됩니다.

이유는 바로 인자로 Supplier를 받기 때문입니다.

 

Supplier는 별도로 생성하여 인자로 줄 수 있기 때문에 커스텀이 가능하게 됩니다.

그 말은, iterate와 같이 스트림으로 만드는 요소의 값이 불변이 아니라 가변이 될 수 있게 되는 것입니다.

 

아래는 그 예제 코드입니다.

 

IntSupplier fib = new IntSupplier() {
    private int previous = 0;
    private int current = 1;
    @Override
    public int getAsInt() {
        int oldPrevious = this.previous;
        int nextValue = this.previous + this.current;
        this.previous = this.current;
        this.current = nextValue;
        return oldPrevious;
    }
};

IntStream.generate(fib).limit(10).forEach(System.out::println);

 

10. 마무리

이번 포스팅에서는 Chapter 5인 스트림 활용에 대해 진행하였습니다.

다음에는 Chapter 6 스트림으로 데이터 수집에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(7) 병렬 데이터 처리와 성능  (0) 2020.04.11
(6) 스트림으로 데이터 수집  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
(3) 람다 표현식  (0) 2020.03.28
(2) 동작 파라미터화 코드 전달하기  (0) 2020.03.21
반응형

1. 서론

 

이번 포스팅에서는 Chapter4의 스트림 소개 에 대해 진행하도록 하겠습니다.

 

2. 스트림이란 무엇인가?

스트림은 자바 8에서 추가된 새로운 기능입니다.

 

이 스트림을 이용하여 SQL과 같이 선언형으로 컬렉션 데이터를 처리할 수 있으며, 

부가적으로 투명하게 멀티쓰레드로 처리도 할 수 있습니다.

 

아래는 자바 7의 코드를 자바 8의 스트림으로 변경하는 예제입니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
	private final String name;
	private final boolean vegetarian;
	private final int calories;
	private final Type type;

	enum Type {
		MEAT, FISH, OTHER
	}
}

public static void main(String[] args) {
	List<Dish> menu = Arrays.asList(
				new Dish("pork", false, 800, Dish.Type.MEAT),
				new Dish("beef", false, 700, Dish.Type.MEAT),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("pizza", true, 550, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("salmon", false, 450, Dish.Type.FISH)
		);
	List<Dish> lowCaloricDishes = new ArrayList<>();
	for(Dish dish : menu) {
		if(dish.getCalories() < 400) {
			lowCaloricDishes.add(dish);
		}
	}
	Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
		@Override
		public int compare(Dish o1, Dish o2) {
			return Integer.compare(o1.getCalories(), o2.getCalories());
		}
	});
	List<String> lowCaloricDishesName = new ArrayList<>();
	for(Dish dish : lowCaloricDishes) {
		lowCaloricDishesName.add(dish.getName();
	}
}

 

위 코드는 400 칼로리보다 작은 음식들을 칼로리 순으로 정렬하여 이름만 담은 List를 만드는 자바7 코드입니다.

 

이것을 자바 8의 스트림을 사용하여 아래와 같이 변경할 수 있습니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
	private final String name;
	private final boolean vegetarian;
	private final int calories;
	private final Type type;

	enum Type {
		MEAT, FISH, OTHER
	}
}

public static void main(String[] args) {
	List<Dish> menu = Arrays.asList(
				new Dish("pork", false, 800, Dish.Type.MEAT),
				new Dish("beef", false, 700, Dish.Type.MEAT),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("pizza", true, 550, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("salmon", false, 450, Dish.Type.FISH)
		);
	List<String> lowCaloricDishesName = menu.stream()
			.filter(d -> d.getCalories() < 400)
			.sorted(Comparator.comparing(Dish::getCalories))
			.map(Dish::getName)
			.collect(Collectors.toList());
}

 

선언형으로 좀 더 readable 한 코드가 되었습니다.

또한, filter, sorted와 같이 여러 연산을 연결하여 처리 파이프라인을 만들었습니다.

 

이것을 멀티코어 아키텍쳐로 처리하고 싶다면 아래와 같이 stream()을 parallelStream()으로 변경하면 됩니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
	private final String name;
	private final boolean vegetarian;
	private final int calories;
	private final Type type;
    
	enum Type {
		MEAT, FISH, OTHER
	}
}

public static void main(String[] args) {
	List<Dish> menu = Arrays.asList(
				new Dish("pork", false, 800, Dish.Type.MEAT),
				new Dish("beef", false, 700, Dish.Type.MEAT),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("pizza", true, 550, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("salmon", false, 450, Dish.Type.FISH)
		);
	List<String> lowCaloricDishesName = menu.parallelStream()
			.filter(d -> d.getCalories() < 400)
			.sorted(Comparator.comparing(Dish::getCalories))
			.map(Dish::getName)
			.collect(Collectors.toList());
}

 

스트림 API의 특징은 아래와 같이 요약할 수 있습니다.

  • 선언형 : 더 간결하고 가독성이 좋습니다.
  • 조립할 수 있음 : 유연성이 좋습니다.
  • 병렬화 : 성능이 좋습니다.

 

 

 

 

반응형

 

 

 

 

 

3. 스트림 시작하기

스트림을 다시 재정의하자면 '데이터 처리 연산을 지원하도록소스에서 추출된 연속된 요소' 로 정의할 수 있습니다.

 

이 정의에서 나온 단어에 대해 하나씩 살펴보겠습니다.

 

1. 연속된 요소

 

컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공합니다.

여기서 스트림은 filter, sorted와 같이 데이터의 계산에 더 중점을 두고 있습니다.

 

2. 소스

 

스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비합니다.

정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지됩니다.

즉, 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지합니다.

 

3. 데이터 처리 연산

 

스트림은 함수형 연산과 데이터베이스와 비슷한 연산을 지원합니다.

예로 filter, map, reduce, find, match 등이 있습니다.

 

이런 스트림에는 2가지 특징이 있습니다.

 

1. 파이프라이닝

 

대부분의 스트림연산은 스트림 연산끼리연결해서 파이프라이 구성이 가능합니다.

그로 인해 laziness, short-circuiting과 같은 최적화도 얻을 수 있습니다.

 

2. 내부 반복

 

스트림은 컬렉션과 달리 내부 반복을 사용합니다.

 

 

 

아래는 스트림을 사용한 하나의 예제입니다.

 

List<String> threeHighCaloricDishNames =
        menu.stream()
                .filter(d -> d.getCalories() > 300)
                .map(Dish::getName)
                .limit(3)
                .collect(Collectors.toList());

 

이 예제에서는 데이터 소스로 요리 리스트를 사용하고 있습니다.

또한 filter, map, limit, collect로 일련의 파이프 라인을 구성하여 데이터 처리를 하고 있습니다.

 

여기서 스트림 함수는 collect가 호출되기 전까지는 메서드 호출을 저장만 하고 실제로 수행하진 않습니다.

 

4. 스트림과 컬렉션

스트림과 컬렉션은 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공합니다.

 

하지만, 스트림과 컬렉션도 차이점은 있습니다.

 

컬렉션의 경우 적극적 생성으로 모든 요소를 메모리에 올려서 사용하는 반면 스트림은 요청할때만 요소를 사용한다는 차이점이 있습니다.

 

1. 딱 한번만 탐색 가능

 

스트림도 반복자와 마찬가지로 딱 한번만 요소를 탐색하고 소비한다.

 

만약, 한번 더 탐색하고 싶다면 새로운 스트림을 열어야 합니다.

 

아래는 하나의 스트림에서 2번을 탐색하려고 하는 케이스로 오류가 나게 됩니다.

 

List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);

 

2. 외부 반복과 내부 반복

 

컬렉션 인터페이스는 사용자가 직접 요소를 반복해야합니다. 이를 외부 반복이라고 부릅니다.

반면, 스트림의 경우에는 내부 반복을 사용합니다.

 

외부 반복과 내부 반복의 차이점으로는 2가지가 있습니다.

 

첫째, 관리측면입니다.

내부 반복의 경우 병렬성을 안전하게 제공하고 있습니다.

하지만, 외부 반복의 경우에는 병렬성을 스스로 관리해야합니다.

 

둘째, 최적화입니다.

외부 반복의 경우에는 컬렉션에서 데이터 요소를 일일히 하나씩 가져와 처리를하게 되지만,

내부반복의 경우에는 내부적으로 함수들의 최적화 순서로 실행하게 됩니다.

 

아래는 외부 반복과 내부 반복의 예시 입니다.

 

// 외부 반복
List<Dish> menu = Arrays.asList(
        new Dish("pork", false, 800, Dish.Type.MEAT),
        new Dish("beef", false, 700, Dish.Type.MEAT),
        new Dish("chicken", false, 400, Dish.Type.MEAT),
        new Dish("french fries", true, 530, Dish.Type.OTHER),
        new Dish("rice", true, 350, Dish.Type.OTHER),
        new Dish("season fruit", true, 120, Dish.Type.OTHER),
        new Dish("pizza", true, 550, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("salmon", false, 450, Dish.Type.FISH)
);
List<String> names = new ArrayList<>();

for(Dish dish : menu) {
    names.add(dish.getName());
}

Iterator<Dish> iterator = menu.iterator();
while(iterator.hasNext()) {
    Dish dish = iterator.next();
    names.add(dish.getName());
}

// 내부 반복
List<String> names2 = menu.stream()
                .map(Dish::getName)
                .collect(Collectors.toList());

 

5. 스트림 연산

스트림 연산은 크게 2개로 나눌수 있습니다.

 

  • 중간 연산 - 스트림과 스트림을 연결할 수 있는 연산
  • 최종 연산 - 스트림을 닫는 연산

 

1. 중간 연산

 

중간 연산은 filter, sorted와 같이 다른 스트림을 반환합니다.

 

특징으로는 스트림 파이프라인을 실행하기 전까지는 아무 연산도 수행되지 않는다는 점입니다.

 

이것은 lazy연산으로 최적화로 이끌게 해주는 기법입니다.

 

2. 최종연산

 

최종연산은 반환값이 스트림이 아닌것을 의미하며 스트림 파이프라인을 수행시키는 트리거 역할을 합니다.

대표적으로 foreach, collect등이 있습니다.

 

3. 스트림 이용하기

 

스트림 이용과정은 아래의 3가지로 요약할 수 있습니다.

 

  • 질의를 수행할 데이터 소스
  • 스트림 파이프라인을 구성할 중간 연산 연결
  • 스트림 파이프라인을 실행하고 결과를 만들 최종 연산

6. 마무리

이번 포스팅에서는 Chapter 4인 스트림 소개에 대해 진행하였습니다.

다음에는 Chapter 5인 스트림 활용에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 Chapter3의 람다 표현식 에 대해 진행하도록 하겠습니다.

 

2. 람다란 무엇인가?

람다 표현식은 메서드로 전달할 수 있는 익명함수를 단순화한 것입니다.

 

아래는 람다의 특징입니다.

 

  1. 익명 : 보통 메서드와 달리 이름이 없어 익명함수입니다.
  2. 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않아 함수라고 부릅니다.
  3. 전달 : 람다는 메서드 인수로 전달하거나 변수로 저장이 가능합니다.
  4. 간결성 : 익명 클래스처럼 자질구레한 코드가 필요 없습니다.

람다는 자바 8 이전에 할 수 없었던 것을 제공하는 것이 아니라 간결한 코드를 만들어 준다고 이해하면 되겠습니다.

 

아래는 Comparator의 compare 메서드를 람다로 표현한 예제 입니다.

  • 파라미터 리스트 :  compare 메서드 파라미터입니다.
  • 화살표 : 람다의 파라미터 리스트와 바디를 구분하는 역할입니다.
  • 람다 바디 : 람다의 반환값에 해당하는 표현식입니다.

메서드는 인자 파라미터가 없을수도 있고 반환값이 없거나 반환값을 위해서 다양한 일을 수행해야 할수도 있습니다.

람다는 이를 모두 지원하기 위해 아래와 같이 총 5가지의 형태가 모두 가능합니다.

 

// 1 - String 파라미터 하나에 int 반환을 하는 람다
(String s) -> s.length()

// 2 - Apple 파라미터 하나에 boolean 반환을 하는 람다
(Apple a) -> a.getWeight() > 150

// 3 - int 파라미터 두개에 반환값이 없는 람다
(int x, int y) -> {
	System.out.println("Result: ");
    System.out.println(x+y);
}

// 4 - 파라미터는 없고 int를 반환하는 람다
() -> 42

// 5 - Apple 파라미터 두개에 int를 반환하는 람다
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

 

3. 어디에, 어떻게 람다를 사용할까?

람다는 함수형 인터페이스라는 문맥에서 사용할 수 있습니다.

 

1. 함수형 인터페이스

 

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스를 의미합니다.

 

인터페이스에는 많은 디폴트 메서드가 있을 수 있습니다.

하지만 추상 메서드가 오직 하나라면 이도 함수형 인터페이스라고 부를 수 있습니다.

 

이 함수형 인터페이스의 추상 메서드를 람다 표현식을 통해 간결하게 만들 수 있는 것입니다.

 

2. 함수 디스크립터

 

함수 디스크립터란 람다 표현식의 시그니처를 서술하는 메서드입니다.

 

여기서 시그니처란 함수의 설명이라고 이해하시면 됩니다.

 

예를 들어, Runnable 인터페이스는 인수와 반환값이 없는 시그니처 라고 말할 수 있습니다.

 

3. @FunctionallInterface

 

이 어노테이션은 함수형 인터페이스를 가리키는 어노테이션입니다.

만약 함수형 인터페이스가 아니라면 컴파일단에서 에러를 내뱉게 되어있습니다.

 

4. 람다 활용 : 실행 어라운드 패턴

람다를 활용하는 예제를 간단하게 진행해 보겠습니다.

 

아래와 같이 파일을 한줄씩 읽는 작업을 하는 메소드가 있습니다.

 

public String processFile() throws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return br.readLine();
	}
}

 

 

현재는 파일에서 한번에 한줄만 읽을 수 있습니다.

만약, 파일에서 두줄씩 읽어야하는 요구사항이 생긴다면?

파일을 open해서 읽는 작업은 동일하되, processFile의 읽는 코드만 수정하면 좋을 것입니다.

 

이를 위해 동작 파라미터화를 적용하겠습니다.

 

1. 동작 파라미터화

 

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

2. 함수형 인터페이스를 이용해서 동작 전달

 

위 정의한 동작을 함수형 인터페이스를 통해 전달하도록 수정해보겠습니다.

 

코드는 아래와 같습니다.

 

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}

public String processFile(BufferedReaderProcessor p) throws IOException {
	
}

 

3. 동작 실행

 

위 처럼 구조를 잡고 이젠 processFile의 메서드를 구현해 보겠습니다.

 

public String processFile(BufferedReaderProcessor p) throws IOException {
	try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return p.process(br);
	}
}

 

4. 람다 전달

 

이제 processFile을 사용하는 쪽에서는 람다를 통해 실행을 전달할 수 있습니다.

 

String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

 

위와 같은 작업을 적용한 패턴을 실행 어라운드 패턴이라고 부릅니다.

 

 

 

 

 

반응형

 

 

 

 

 

5. 함수형 인터페이스 사용

자바 8 에서는 기본적인 함수형 인터페이스를 java.util.function 패키지에 제공하고 있습니다.

 

대표적인 Predicate, Consumer, Function 에 대해 소개하도록 하겠습니다.

 

1. Predicate

 

test 라는 추상메서드가 있고, 시그니처는 아래와 같습니다.

제네릭 타입의 인자 한개를 받아 boolean 값을 반환.

 

아래는 Predicate를 사용하는 예제 코드입니다.

 

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

public <T> List<T> filter(List<T> list, Predicate<T> p) {
	List<T> results = new ArrayList<>();
	for (T t: list) {
		if(p.test(t)) {
			results.add(t);
		}
	}
	return results;
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

 

2. Consumer

 

accept라는 추상메서드가 있고, 시그니처는 아래와 같습니다.

제네릭 타입의 인자 한개를 받고 반환 값은 없는 void.

 

아래는 Consumer를 사용하는 예제 코드입니다.

 

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

public <T> void forEach(List<T> list, Consumer<T> c) {
	for (T t: list) {
		c.accept(t);
	}
}

forEach(
    Arrays.asList(1,2,3,4,5,),
    (Integer i) -> System.out.println(i)
);

 

3. Function

 

apply라는 추상메서드가 있고, 시그니처는 아래와 같습니다.

제네릭 타입의 인자 한개를 받고 제네릭 타입을 반환.

 

아래는 Function을 사용하는 예제 코드입니다.

 

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

public <T, R> List<R> map(List<T> list, Function<T, R> f) {
	List<R> result = new ArrayList<>();
	for (T t: list) {
		result.add(f.apply(t));
	}
	return result;
}

List<Integer> l = map(
		Arrays.asList("lambdas", "in", "action"),
		(String s) -> s.length()
);

 

대표적인 3개의 함수형 인터페이스를 알아봤습니다.

 

java.util.function에는 이 3개 말고도 아래와 같이 더 있습니다.

 

  • Supplier<T>
  • UnaryOperator<T>
  • BinaryOperator<T>
  • BiPredicate<L, R>
  • BiConsumer<T, U>
  • BiFunction<T, U, R>

6. 형식 검사, 형식 추론, 제약

이번에는 자바 컴파일러가 람다를 어떻게 처리하는지 알아보도록 하겠습니다.

 

1. 형식 검사

 

람다가 사용되는 콘텍스트를 통해 람다의 형식을 추론할 수 있습니다.

 

이전에 예제로 했던 filter 를 통해 설명하겠습니다.

 

List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

 

 

 

위 예제의 람다는 아래와 같은 순으로 형식검사가 이루어 집니다.

 

  1. 람다가 사용된 콘텍스트인 filter 메서드를 확인
  2. 확인 결과 Predicate<Apple>로 대상 형식 확인
  3. Predicate<Apple>의 추상메서드 확인
  4. 람다의 디스크립터와 Predicate<Apple>의 추상메서드가 동일한지 확인

 

 

2. 같은 람다, 다른 함수형 인터페이스

 

위 형식검사에서 본듯이 하나의 람다는 여러개의 함수형 인터페이스와 호환이 가능합니다.

 

아래는 그 예제입니다.

 

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

 

3. 형식 추론

 

자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해 람다 표현식과 관련된 함수형 인터페이스를 추론 할 수 있습니다.

 

그로인해, 람다 사용시 이 추론을 이용해 더욱 간결한 코드작성이 가능합니다.

 

아래는 그 예제입니다.

 

Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

 

컴파일러는 파라미터인 Apple을 추론하기 때문에 위와 같이 생략이 가능합니다.

 

4. 지역변수 사용

 

람다에서는 외부 변수를 조작하는 경우도 있습니다.

이 경우 가능은 하지만 외부 변수는 final로 선언되거나 final처럼 취급되는 것에 한해서 가능합니다.

 

이유로는 인스턴수 변수는 힙에, 지역 변수는 스택에 저장되기 때문입니다.

람다가 실행되는 스레드에서 지역 변수를 참조할 때는 지역변수의 복사본(읽기 전용)을 생성하고 참조하게 됩니다.

하지만, 람다가 직접 지역변수가 위치한 스택영역에 접근하게 되면 지역변수를 할당한 스레드가 끝나면 변수 할당이 해제되는 시점과 겹칠 수 있습니다.

그래서, 람다는 복사본(읽기 전용)을 참조하기 때문에 해당 변수의 값은 변경되어서는 안되는 제약사항이 생기게 된것입니다.

 

7. 메서드 참조

메서드 참조는 특정 메서드만을 호출하는 람다의 축약형입니다.

 

메서드 참조는 코드의 가독성을 높인다는 장점을 가지고 있습니다.

 

또한, 람다의 축약형으로 동일하게 콘텍스트의 형식과 일치해야 하며, 생성자 참조도 지원하고 있습니다.

 

아래는 생성자 참조의 예제 입니다.

 

Supplier<Apple> c1 = Apple::new

 

8. 람다, 메서드 참조 활용하기

이제 배운 람다와 메서드 참조를 통해 사과를 무게순으로 정렬하는 코드를 단계별로 적용하는 예제를 진행하겠습니다.

 

1. 코드 전달

 

@Getter
class Apple {
	private Integer weight;
}

public class AppleComparator implements Comparator<Apple> {
	@Override
	public int compare(Apple o1, Apple o2) {
		return o1.getWeight().compareTo(o2.getWeight());
	}
} 
inventory.sort(new AppleComparator());

 

2. 익명 클래스 사용

 

inventory.sort(new Comparator<Apple>() {
	@Override
	public int compare(Apple o1, Apple o2) {
		return o1.getWeight().compareTo(o2.getWeight());
	}
});

 

3. 람다 표현식 사용

 

inventory.sort((Comparator<Apple>) (o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));

 

4. 메서드 참조 사용

 

Arrays.asList().sort(Comparator.comparing(Apple::getWeight));

 

9. 람다 표현식을 조합할 수 있는 유용한 메서드

람다 표현식을 조합할 수 있는 유용한 메서드는 바로 디폴트 메서드입니다.

 

함수형 인터페이스는 디폴트 메서드가 있더라도 추상 메서드만 한개를 가지고 있으면 되기 때문에, 이 규칙에도 어긋나지 않습니다.

 

대표적인 함수형 인터페이스들의 디폴트 메서드를 소개하겠습니다.

 

1. Comparator

 

  • comparing - 비교에 사용할 키를 추출하는 Function
  • reversed - 오름차순이 아니라 내림차순으로 정렬.
  • thenComparing - 정렬을 위한 비교자 추가

2. Predicate

 

  • negate - 기존 Predicate 객체 결과를 반전시킨 객체 생성
  • and - 두 Predicate를 and로 연결하여 새로운 Predicate 객체 생성
  • or - 두 Predicate를 or로 연결하여 새로운 Predicate 객체 생성

3. Function

 

  • andThan - 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수
  • compose - 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공

 

10. 마무리

이번 포스팅에서는 Chapter 3인 람다 표현식에 대해 진행하였습니다.

다음에는 Chapter 4인 스트림 소개에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 Chapter2의 동작 파라미터화 코드 전달하기 에 대해 진행하도록 하겠습니다.

 

진행하기에 앞서 동작 파라미터화의 의미를 설명하자면 아래와 같습니다.

 

아직은 어떻게 실행할것인지 결정하지 않은 코드 블록

 

2. 변화하는 요구사항에 대응하기

책에서는 사과 농부의 요구사항을 예로 들고 있습니다.

 

요구사항으로는 녹색인 사과 혹은 특정 무게 이상인 사과와 같이 filter 류의 요구사항들입니다.

 

1) 첫 번째 시도 : 녹색 사과 필터링

 

private enum Color {
    GREEN,
    RED
}

@Getter
@RequiredArgsConstructor
private static class Apple {
    private final Color color;
    private final int weight;
}

private static List<Apple> filterGreenApples(List<Apple> apples) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: apples) {
        if(Color.GREEN.equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return result;
}

 

아직, 색이 다양해짐에 따라 메소드들이 늘어나야 하는 불편함이 존재합니다.

아래는 이를 해결하기 위해 값을 파라미터화한 케이스입니다.

 

 

2) 두 번째 시도 : 색을 파라미터화

 

private enum Color {
    GREEN,
    RED
}

@Getter
@RequiredArgsConstructor
private static class Apple {
    private final Color color;
    private final int weight;
}

private static List<Apple> filterApplesByColor(List<Apple> apples, Color color) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: apples) {
        if(color.equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return result;
}

private static List<Apple> filterApplesByWeight(List<Apple> apples, int weight) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: apples) {
        if(apple.getWeight() > weight) {
            result.add(apple);
        }
    }
    return result;
}

 

 

값을 파라미터화한 이 코드가 나쁘진 않습니다. 다만, 중복되는 코드가 존재하는것을 볼 수 있습니다.

아래는 그것을 해결하기 위해 flag 값을 두어 필터링하는 케이스입니다.

 

 

3) 세 번째 시도 : 가능한 모든 속성으로 필터링

 

private static List<Apple> filterApples(List<Apple> apples, Color color, int weight, boolean flag) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: apples) {
        if((flag && apple.getColor().equals(color)) 
        || (!flag && apple.getWeight() > weight)) {
            result.add(apple);   
        }
    }
    return result;
}

public static void main(String[] args) {
    List<Apple> appleList = Arrays.asList(new Apple(Color.RED, 200), new Apple(Color.GREEN, 30), new Apple(Color.GREEN, 300));
    final List<Apple> greenAppleList = filterApples(appleList, Color.GREEN, 0, true);
    final List<Apple> heavyAppleList = filterApples(appleList, null, 150, false);
}

 

한개의 메서드로 줄어지긴 했지만 코드가 너무 지저분하며 이해하기가 어렵습니다.

이를 해결하기 위해, 이제 동작 파라미터화를 진행해보겠습니다.

 

 

 

 

 

반응형

 

 

 

 

3. 동작 파라미터화

private interface ApplePredicate {
    boolean test (Apple apple);
} 

private static class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

private static class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return Color.GREEN.equals(apple.getColor());
    }
}

 

위와 같이 인터페이스를 하나 두어 각 필터링을 담당하는 클래스들을 선언합니다.

 

1) 네 번째 시도 : 추상적 조건으로 필터링

 

private static List<Apple> filterApples(List<Apple> apples, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();

    for(Apple apple: apples) {
        if(p.test(apple)) {
            result.add(apple);
        }
    }
    return result;
}

 

동작 파라미터화로 지저분한 코드를 깔끔하게 만들었으며 이해하기도 쉬워졌습니다.

다만, 요구사항이 늘어남에 따라 클래스를 생성해야 하니 코드수가 많이 늘게 됩니다.

또한, 한번만 사용하는 소스도 굳이 클래스를 생성하여 써야하는 불편함이 생겼습니다.

이를 해결하기 위해 아래와 같이 간소화 작업을 진행하겠습니다.

 

 

4. 복잡한 과정 간소화

1) 다섯 번째 시도 : 익명 클래스 사용

 

private static List<Apple> filterApples(List<Apple> apples, ApplePredicate p) {
    List<Apple> result = new ArrayList<>();

    for(Apple apple: apples) {
        if(p.test(apple)) {
            result.add(apple);
        }
    }
	return result;
}

final List<Apple> redAppleList = filterApples(appleList, new ApplePredicate() {
    public boolean test(Apple apple) {
        return Color.RED.equals(apple.getColor());
    }
});

 

익명클래스를 통해 클래스 선언을 제거했습니다.

하지만, 위 두번째 시도인 색을 파라미터화와 같이 중복되는 코드들이 많게됩니다.

 

2) 여섯 번째 시도 : 람다 표현식 사용

 

final List<Apple> redAppleList = filterApples(appleList, apple -> Color.RED.equals(apple.getColor()));

 

자바 8의 기능인 람다를 사용하여 중복되는 코드를 줄였으며 이해하기도 편하게 되었습니다.

 

5. 마무리

이번 포스팅에서는 동작 파라미터화 코드 전달하기에 대해 간단한 예제를 진행해보았습니다.

다음에는 람다 표현식에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(6) 스트림으로 데이터 수집  (0) 2020.04.04
(5) 스트림 활용  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
(3) 람다 표현식  (0) 2020.03.28
(1) 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?  (0) 2020.03.21
반응형

1. 서론

 

이번 포스팅에서는 Modern Java In Action 의 1장인 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가? 에 대해 알아보도록 하겠습니다.

 

2. 왜 아직도 자바는 변화하는가?

프로그래밍 언어 생태계는 끊임없이 변화하고 있습니다.

이러한 생태계에서 살아남기위해 프로그래밍 언어들은 계속 발전을 해왔고, 자바도 그 중 하나입니다.

 

자바는 살아남기 위해 무엇을 발전했는지 아래 설명하도록 하겠습니다.

 

 

1) 스트림 처리

 

자바8에서 java.util.stream 패키지가 추가되었습니다.

 

이 패키지에 있는 기능들은 리눅스의 파이프라인과 동일하다고 생각하시면 편합니다.

 

아래는 리눅스의 파일을 읽어 소문자로 변경하고 정렬하여 마지막 3줄만을 출력하는 command입니다.

 

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

 

각 파일읽기, 소문자 치환, 정렬 같은 일련의 작업을 ' | ' 사용하여 연결하는 것을 볼 수 있습니다.

 

이와 같이 자바 8에서도 Stream 패키지를 통해 각각의 작업을 하나의 일련의 작업으로 만들 수 있게 제공하고 있습니다.

 

 

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

 

자바 8에서는 메서드를 다른 메서드로 전달할 방법이 없었습니다.

 

하지만 자바 8에서 이를 지원하도록 개선되었고 이를 통해 복잡한 코드구조가 사라지게 되었습니다.

 

아래는 대표적인 메서드에 코드를 전달할 수 있는 함수형 인터페이스입니다.

패키지는 java.util.function 입니다.

 

  • Predicate
  • Consumer
  • Supplier

 

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

 

자바 8에서는 스트림API를 지원하면서 간단히 병렬성을 가져갈수 있도록 제공하고 있습니다.

하지만, 이를 위해서는 멀티 쓰레드 환경에서도 서로 코드를 동시에 수행하더라도 안전하게 바꿔야하는 번거로움이 존재합니다.

 

스트림 함수 사용 시 외부접근 데이터를 Atomic한 객체로 만들어 사용해야 합니다.
이는, 위의 안전한 코드여야 하기 때문입니다.

 

 

 

 

 

반응형

 

 

 

 

 

3. 자바 함수

 

프로그래밍 언어에서 함수라는 용어는 메서드와 같은 의미로 사용됩니다.

자바 8에서는 이러한 메서드를 일급시민으로 올리도록 제공하고 있습니다.

 

일급시민이란, 각 구조체의 값을 전달할 수 있는 것을 의미합니다.

그러므로, 위의 메서드를 일급시민으로 올린다는 의미는 메서드라는 구조체의 값을 다른 구조체에게 전달할 수 있다는 의미입니다.

 

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

 

1 - 메서드 참조

 

자바8 에서는 메서드참조를 제공합니다.

 

아래는 숨겨진 파일들만을 filter하여 얻어오는 코드입니다.

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

 

위의 코드는 자바 8이 나오기전의 코드입니다.

 

자바 8에서는 람다와 메서드 참조를 제공하였고 그 결과 아래와 같이 더욱 readable 있는 코드로 리팩토링이 가능해졌습니다.

 

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

 

람다를 통해 익명클래스를 간단하게 바꾸었으며, ::isHidden 인 메서드 참조를 통해 한결 간단해진 코드가 되었습니다.

 

 

2- 코드 넘겨주기

 

 

아래는 사과 리스트에서 초록색이며, 무게가 200이 넘는 사과만 분류하는 코드입니다. - 자바 8 이전

 

private enum Color {
    GREEN,
    RED
}

public static void main(String[] args) {
    List<Apple> appleList = Arrays.asList(new Apple(Color.RED, 200), new Apple(Color.GREEN, 30), new Apple(Color.GREEN, 300));
    final List<Apple> filterdGreenAppleList = filterGreenApples(appleList);
    final List<Apple> filterdGreenWithHeavyAppleList = filterHeavyApples(filterdGreenAppleList, 200);
}

@Getter
@RequiredArgsConstructor
private static class Apple {
    private final Color color;
    private final int weight;
}

private static List<Apple> filterGreenApples(List<Apple> apples) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: apples) {
        if(Color.GREEN.equals(apple.getColor())) {
            result.add(apple);
        }
    }
    return result;
}

private static List<Apple> filterHeavyApples(List<Apple> apples, int weight) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple: apples) {
        if(apple.getWeight() > weight) {
            result.add(apple);
        }
    }
    return result;
}

 

하지만, 자바 8의 스트림 함수와 람다를 사용하면 아래와 같이 간단하게 바뀔 수 있습니다.

 

public static void main(String[] args) {
    List<Apple> appleList = Arrays.asList(new Apple(Color.RED, 200), new Apple(Color.GREEN, 30), new Apple(Color.GREEN, 300));
    final List<Apple> filterdGreenWithHeavyAppleList = appleList
            .stream()
            .filter(apple -> Color.GREEN.equals(apple.getColor()))
            .filter(apple -> apple.getWeight() > 200)
            .collect(Collectors.toList());
}

 

단, filter 에 있는 조건들이 일회성이 아니라면 별도 함수로 추출해서 사용해야 합니다.

 

4. 스트림

이전 옛날 자바에서는 한개의 CPU만을 사용하는 단점이 있었습니다.

하지만 자바 8에서는 한개가 아닌 멀티 CPU를 점유해서 사용하도록 변경되었고 이는 성능의 극대화를 가져다 주었습니다

 

대표적으로 parallelStream 을 들 수 있습니다.

parallelStream은 멀티 CPU를 통해 분할로 처리할때 사용합니다.

 

단, Parallel Stream 작업이 독립적이면서 CPU사용이 높은 작업에 사용해야합니다.

 

5. 디폴트 메서드와 자바 모듈

자바 8에서는 인터페이스에 디폴트 메서드를 제공합니다.

 

디폴트 메서드는 기존의 코드를 수정하지 않고 확장하기 위해서 만들어진 것이며,

간단하게 인터페이스에도 기본적으로 정의된 구현 메서드가 있을 수 있다고 생각하면 됩니다.

 

아래는 List의 default sort 함수입니다.

 

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

 

6. 함수형 프로그래밍에서 가져온 다른 유용한 아이디어

 

자바 8에서 제공되는 기능들은 대체로 함수형 프로그래밍 특성을 가져다 주는 것입니다.

이런 함수형 프로그래밍의 패러다임을 가져오면서 많은 프로그램에 도움이 되는 아이디어가 나왔습니다.

 

한가지 예로, null을 회피하는 방법이며 자바에서는 Optional<T> 라는 컨테이너 클래스를 통해 제공하고 있습니다.

 

7. 마무리

이번 포스팅에서는 자바 8부터의 변화에 대해 간단한 소개와 설명을 했습니다.

다음에는 동작 파라미터화 코드 전달에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(6) 스트림으로 데이터 수집  (0) 2020.04.04
(5) 스트림 활용  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
(3) 람다 표현식  (0) 2020.03.28
(2) 동작 파라미터화 코드 전달하기  (0) 2020.03.21

+ Recent posts