JAVA Stream API

Uicheon·2022년 11월 4일
0

자바

목록 보기
1/1

JAVA Stream API

프로젝트때 사용한 기본적인 Java Stream API을 설명합니다.

면접에서 면접관님이 Java에 대해 어느 정도 실력을 갖고 있냐는 질문과 함께 Stream API를 사용해봤냐고 여쭤보셨습니다.
저는 긴장한 나머지 질문을 못 이해하여 사용해본 적이 없다 했습니다. (있습니다!!!)
이에 이번 프로젝트 진행에 욕심을 담아 사용해봤습니다. 😉


1. What?

Stream은 컬렉션(맵, 배열, I/O, 자바 자료구조)에 저장된 요소들을 하나씩 참조하여 람다식을 통해 반복적으로 어떤 처리를 할 수 있도록 해주는 기능입니다.
어떻게 보면 sql문법과 비슷합니다.
(sql이 from → where → select을 거쳐 필터링 되듯이)

1-1. Stream의 특징

  1. Immutable, 원본 데이터 변경 X
  2. Lazy, 필요할 때만 연산하므로 효율적인 처리.
  3. 재사용 불가능
  4. 가독성과 실수 여지 감소

1번과 2번은 그렇다고 해도, 4번에 갸우뚱 할 수 있습니다.

예시를 볼까요?

모든 사람(리스트)의 형제들 중에서 이름이 null이 아니고, 나이가 20세 이상인 모든 여자 형제의 이름을 가져와라

@Data
public class Person{
    public String name;
    public int age;
    public String gender; //"M", "W", "Nb", "N"
    public List<Person> siblings;  
}

일단 for문으로 작성해보겠습니다.

public List<String> findSiblingNameWithComplexConditionUsingForLoop() {
    List<Person> personList = List.of();
    List<String> result = new ArrayList<>();
    for (Person person : personList) {
        for (Person sibling : person.getSiblings()) {
            if (sibling.getName() != null && sibling.getAge() >= 20 && sibling.getGender().equals("W")) {
                result.add(sibling.getName());
            }
        }
    }
    return result;
}

여기서도 조건이 1,2개만 추가되어도 depth수가 점점 늘어나고 가독성은 점점 떨어집니다. (남자이며, 이름이 'B'로 시작하는 사람의 형제+조건을 찾는다고 생각해보자)

그렇다면 Stream을 사용하면 어떨까요?

public List<String> findSiblingNameWithComplexConditionUisngStream() {
    List<Person> personList = List.of();
    List<String> result = new ArrayList<>();
    personList
        .stream()
        .flatMap(x -> x.getSiblings().stream())
            .filter(p -> p.getName() != null)
            .filter(p -> p.getAge() >= 20)
            .filter(p -> p.getGender().equals("W"))
        .collect(Collectors.toList());
    return result;
}

filter 덕분에 가로로 길어지지 않아도(&&조건), depth가 더 깊어지지 않아도 됩니다.(if문안의 if문)

즉, 조건을 유추하는 가독성이 높아지고, 실수를 덜 발생시키게 됩니다.

2. Why?

Stream은 분명 어렵고 러닝커브가 있고, 함수형이니 람다식이나 등 익숙하지 않은 개념이 등장합니다.

코드도 꼬불꼬불한게 언뜻보면 멋있어보이려고 사용한 게 아닐까 싶기도 합니다. (저는 멋있어보이려고 씁니다.)

그럼에도 불구하고 왜 Stream을 사용할까요?

결국은 분리입니다.

우리가 Repository 인터페이스에 하나의 RepositoryImpl 클래스를 만들어 사용하듯이,

혹여 수정될 반복문의 관심사를 분리하여 가독성을 높이고, 지속 가능하게 함에 의미가 있다고 생각합니다.

특히, 중첩되는(depth에 depth) 조건문은 눈살을 찌푸리게 할 수 밖에 없습니다.

이를 람다식이 해결해 줄 수 있기에 Stream을 사용한다고 생각합니다.

3. But

그러나 그럼에도 불구하고 Stream이 만능 해결사는 아닙니다.

앞서 언급한 러닝커브와, parallel을 사용하지 않으면 단순 for-loop보다 느릴 수도 있다는 점이 있습니다.

(그렇다고 parallel을 무턱대고 사용하면 thread pool을 사용해 심각한 성능 장애를 일으킬 수도 있습니다.)

그러므로 주어진 환경에 우리에게 맞는 방법을 항상 고민해야 겠습니다.

4. How

우리 프로젝트에서 Stream은 이렇게 사용되었습니다.

우리 프로젝트에 중요한 기능 중 하나인 해쉬태그 기능이 있습니다.

상품마다 지정된 해쉬태그는 DB에 쉼표로 구분된 문자열 형식으로 저장됩니다. (ex "1,2,3,9,20")

Stream 함수를 이용하여 가져온 방식은 다음과 같습니다.

  1. 먼저, 숫자 1~3은 기업의 규모(entSize)를 나타냅니다.

사용자 입력에 맞게 filter를 사용하여 해당하는 규모의 Product만 가져옵니다.

  1. 숫자 4~18은 업종을 나타냅니다.

이에 맞게 업종만을 tagList에 저장합니다.

  1. 숫자에 따른 업종명(해쉬에 존재)를 매핑합니다.

  2. 이를 문자열을 갖는 리스트로 Collect합니다. (한 Product는 N개의 업종명을 갖습니다.)

  3. 이를 문자열을 갖는 리스트의 리스트로 Collect합니다. (즉, Product의 업종명을 갖는 리스트의 리스트를 가져옵니다.)

List<List<String>> filteredHashTagStringList = productList
                .stream()
                .sorted(Comparator.comparing(Product::getId))
                .filter(e -> (Arrays.stream(e.getTaglist().split(",")).anyMatch(q -> q.equals(entSize)))) //기업 규모와 맞는 Product들 중에서
                .map(e -> (Arrays.stream(e.getTaglist().split(",")) 
                        .filter(z -> Integer.parseInt(z) >= 4 && Integer.parseInt(z) <= 18) // 업종 항목만 갖는
                        .map(k -> hashTagMap.get(Integer.parseInt(k) - 1)) // 숫자를 해쉬태그 이름에 매핑
                        .collect(Collectors.toList()))) //문자열 리스트로
                .collect(Collectors.toList()); //문자열 리스트의 리스트로 가져온다.

이로써 "1,2,4"와 같은 태그 번호가 "대기업, 통신, 보안"으로 변화합니다.

참고한 블로그

profile
컨셉입니다~

0개의 댓글