본문 바로가기
개발/Java

[다시 공부하는 Java] Stream API

by 방구쟁이 2023. 4. 18.
728x90

개요


 '아는 만큼 보인다' 라는 말을 Java와 Spring을 다시 공부하면서 매우 공감했다. 처음 습득하는 지식은 이해도 쉽지 않고 기억에 남기 어렵다. 하지만 여러번 반복해서 접하다보면 '이게 그때 그 말이었구나' 하고 깨닫게 된다. 그래서 내가 놓치고 있고 부족한 부분을 채우기 위해 다시 Java와 Spring을 공부하기 시작했다.

 오늘 주인공은 Stream이다. Java 8부터 람다식, 함수형 인터페이스 등을 지원하면서 데이터를 처리하는데 자주 사용되는 함수들을 추상화한 Stream부터 복습해보자.

Java 로고

 

Stream 개념

Stream이란, Collection의 담겨진 요소들을 하나씩 람다식을 수행하는 일회성 반복자이다.

소개한 그대로 Stream의 내부 메서드를 통해 반복적인 일처리가 가능하며 일회성이기 때문에 연산이 끝나게 되면 다시 사용할 수 없는 특징을 가진다.

 

Stream API 사용하기

stream의 원리는 아래와 같다.

  1. stream 객체 생성
  2. 중간 연산(제공되는 API를 사용하여 가공)
  3. 최종 연산(원하는 결과값 반환)

 

1. Stream 객체 생성

stream 객체를 얻는 방법은 다양하다. 대표적으로 Collection 타입을 통해 stream 객체를 얻는 방법부터 살펴보자.

private void createStream(){
    List<String> tagList = Arrays.asList("#태그1", "#태그2");

    tagList.stream()
}

위의 소스를 보면 Collection을 상속받은 List<T> 타입인 tagList에서 stream()을 호출할 수 있다. stream()을 호출하게 되면 컬렉션 요소에 대한 순차적 Stream을 반환해준다고 한다.

stream의 Documented

그러면 stream() 내부를 살펴보자.

// Collection의 stream 메서드

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
// StreamSupport의 stream 메서드

public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
    Objects.requireNonNull(spliterator);
    return new ReferencePipeline.Head<>(spliterator,
                                        StreamOpFlag.fromCharacteristics(spliterator),
                                        parallel);
}

메서드 내부를 따라가면 ReferencePipeline를 반환시켜주는데 Stream을 implements한 클래스로 stream의 메서드를 오버라이딩하여 Stream API 메서드들을 사용할 수 있게 된 것이다.

 

2. 중간 연산

 중간 연산은 생성한 Stream을 가공하는 단계이다. 중간 처리 메서드들은 중간 처리된 Stream을 반환하고 파이프라인을 형성하여 최종 연산 전까지 처리를 지연시킨다. 중간 연산 메서드들의 인자값으로 함수형 인터페이스가 사용된다.

중간 연산 예시

certificatesRepository.findAllByUserId(id) 				// 리스트
            .stream()							// 스트림
            .map(certificates -> certificates.toResponse())		// 스트림

위 메서드는 다음과 같다.

  1. List를 stream으로 변환
  2. 중간 처리 메서드인 stream.map()을 이용하여 중간처리
  3. 최종 연산 메서드인 collect()를 이용하여 요소들로 새로운 컬렉션을 생성

중간 연산을 하는 메서드 종류는 다음과 같다.

  • 필터 : filter(), distinct()
  • 변환 : map(), flatMap(), asDoubleStream(), asLongStream(), boxed()
  • 정렬 : sorted()
  • 루핑 : peek(), forEach()

 

3. 최종 연산

 최종 연산은 Stream 타입을 기본 타입으로 반환시킨다. 최종 연산이 시작되면 지연되었던 중간 연산들을 처리하고 최종 연산을 수행 후 원하는 타입으로 반환된다.

최종 연산 예시

public List<CertificatesResponse> findAllByUserId(Long id){
        return certificatesRepository.findAllByUserId(id)
                .stream()
                .map(certificates -> certificates.toResponse())
                .collect(Collectors.toList());			// 최종 연산 수집
    }

 

최종 연산 메서드 종류는 다음과 같다.

  • 매칭 : allMath(), anyMatch(), noneMatch()
  • 집계 : sum(), count(), average(), max(), min()
  • 집계 : reduce()
  • 수집 : collect()

 

반복문과 Stream 비교 코드

반복문을 사용한 코드

private List<Hashtag> getChangedTags(CertificatesDto certificatesDto, List<Hashtag> existTags){
    List<Hashtag> resultTags = new ArrayList<>();
    List<String> addTags = certificatesDto.getHashtags();

    for (int i = 0; i < addTags.size(); i++){
        Hashtag existTag = null;
        for(int j = 0; j < existTags.size(); j++){
            if(existTags.get(j).getTagName().equals(addTags.get(i))){
                existTag = existTags.get(j);
            }
        }

        Optional<Hashtag> resultTag = Optional.ofNullable(existTag);
        if(resultTag.isEmpty()){
            resultTags.add(
                    Hashtag.builder().tagName(addTags.get(i)).build()
            );
        }
    }

    return resultTags;
}

 

Stream을 사용 코드

private List<Hashtag> getChangedTags(CertificatesDto certificatesDto, List<Hashtag> existTags){
    List<String> addTags = certificatesDto.getHashtags();

    List<Hashtag> resultTags = addTags.stream()
            .filter(tagName -> existTags.stream().noneMatch(tag -> tag.getTagName().equals(tagName)))
            .map(tagName -> Hashtag.builder().tagName(tagName).build())
            .collect(Collectors.toList());

    return resultTags;
}

 

 해당 메서드는 certificatesDto에 tag 중 존재하지 않는 tag만 반환해준다. 결과적으로 코드 라인 수가 줄어들고 stream의 메서드들을 알면 가독성도 높아졌다고 볼 수 있다.

 

Stream API 병렬 처리

Collection 인터페이스에 보면 다음과 같이 stream()뿐 아니라 parallelStream()도 존재한다.

Collection의 parallelStream()

 

parallelStream

 Parallel Stream도 역시 Java8부터 제공하며 멀티 스레드 환경에서 스레드를 병렬적으로 실행한다. 요소들을 각각 서브 요소들로 나누고 여러 스레드에서 병렬 처리 시킨 뒤 ForkJoin 프레임워크로 결과를 합쳐 최종 결과를 반환한다.

더 자세한 내용은 다음 링크를 접속하여 확인 가능하다.

 

 

 

참고 문서

감사합니다.

728x90

'개발 > Java' 카테고리의 다른 글

java.util.Optional<T> 이란?  (0) 2021.12.22
JAVA 성능 향상 시키기  (2) 2021.08.11
[Java] StringTokenizer란?  (0) 2021.08.02
.class와 .java 확장자  (0) 2021.07.21
JVM(Java Virtual Machine) 이란?  (2) 2021.06.24

댓글