Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20240719 Stream()을 쓴 이유 & 주의사항 본문

개발Article

[TIL]20240719 Stream()을 쓴 이유 & 주의사항

최밤빵 2024. 7. 19. 23:58

Stream()을 사용하는 이유? 그냥 강사님이 쓰셨고, 그래서 써보니 간략하고 좋아서 였는데 이렇게 대답하면 안되는거였다.

사실 이게 어떻게 돌아가는건지 자바에서 듣고오지않아서 정말 하나도 모르는 상태라 그냥 반복문인가? 정도로만 알고있었어서 알아두기위해 오늘 개발일지의 주제로 정했다. (사실 스트림은 비슷한 작업을 할 수 있는거지 완전히 같은 개념도 아니였다.)

이번 개발일지에서는 Java 8에서 도입된 Stream을 사용하는 이유와 그 장점, 단점에 정리해봤다. BookService 클래스의 getAllBooks() 메서드에서는 Stream을 사용하여 책 목록을 정렬하고 변환하는 작업을 수행한다. Stream을 사용하는 이유와 그 유용성, 주의해야 할 점에 대해 이해해보려고한다!

 

▶ Stream이란 ?

Stream은 Java 8에서 도입된 기능으로, 컬렉션 데이터를 선언적 방식으로 처리할 수 있게 해주는 도구이다. Stream은 데이터의 흐름을 나타내며, 데이터를 필터링, 매핑, 정렬, 축소(reduce) 등의 연산을 통해 쉽게 처리할 수 있도록 한다. Stream은 **내부 반복(internal iteration)**을 사용하여, 코드가 간결하고 읽기 쉽게 만든다.

Stream의 기본적인 동작 방식은 다음과 같다:

→ 생성:

컬렉션이나 배열 등의 데이터 소스에서 스트림을 생성한다.

중간 연산:

filter(), map(), sorted()와 같은 메서드를 통해 데이터를 변환하거나 필터링한다.

종료 연산:

collect(), forEach(), reduce()와 같은 메서드를 통해 최종 결과를 반환하거나 처리한다.

 

▶ Stream의 사용 이유

BookService 클래스에서 Stream을 사용하여 책 목록을 처리하는 이유는 다음과 같다.

코드의 간결성과 가독성:

bookRepository.findAll().stream().map(this::toResponseDto).sorted(...);

예시와 같은 Stream 코드는 직관적이고, 코드의 의도를 명확히 드러낸다.

Stream을 사용하면 데이터 처리 로직을 선언적으로 표현할 수 있다. 예를 들어, 책 목록을 정렬하고 변환하는 작업을 한 줄의 코드로 명확히 표현할 수 있다. 이는 반복문을 사용한 코드보다 훨씬 간결하고 이해하기 쉽고, 유지보수성을 높여준다.

함수형 프로그래밍 스타일의 장점 활용:

Stream은 함수형 프로그래밍의 장점을 Java에 도입하여, 데이터 처리 로직을 함수형 인터페이스로 표현할 수 있다. 이로 인해 코드의 재사용성과 확장성이 높아진다. map(), filter(), sorted() 등의 메서드를 활용하면 데이터 변환과 필터링을 쉽게 수행할 수 있다.

병렬 처리의 용이성:

Stream은 쉽게 병렬 처리를 수행할 수 있는 API를 제공한다. parallelStream()을 사용하면 데이터 처리 작업을 여러 CPU 코어에서 병렬로 수행할 수 있어, 대규모 데이터 처리 성능을 크게 향상시킬 수 있다.

변경 불가능성(Immutable)과 안전한 멀티스레드 환경:

Stream은 변경 불가능한(immutable) 객체로 동작하므로, 멀티스레드 환경에서 안전하게 사용할 수 있다. 원본 데이터를 변경하지 않으며, 각 연산 결과는 새로운 Stream을 반환하기 때문에 동시성 문제를 피할 수 있다.

 

▶ Stream의 단점과 주의사항

Stream을 사용할 때는 몇 가지 주의할 점이 있다. Stream의 장점이 많은 만큼, 잘못 사용하면 성능과 코드 유지보수에 악영향을 미칠 수 있다.

성능 문제:

Stream의 사용이 항상 성능을 보장하는 것은 아니다. 특히, Stream의 연산이 많아질수록, 내부적으로 많은 객체를 생성하게 되어 메모리 사용량이 증가할 수 있다. 큰 컬렉션에서는 성능에 악영향을 줄 수 있다.

디버깅이 어려움:

Stream은 선언형 스타일로 작성되므로, 디버깅이 어렵다. forEach()나 peek()를 사용하여 디버깅할 수는 있지만, 기존의 명령형 프로그래밍 방식보다 디버깅이 어려울 수 있다.

오용의 위험:

Stream은 변경 불가능한 데이터를 다루도록 설계되었지만, 잘못 사용하면 데이터의 변경이 발생할 수 있다. 예를 들어, forEach()에서 외부의 상태를 변경하는 로직을 작성하는 것은 함수형 프로그래밍 원칙을 위배하는 것이다.

병렬 스트림의 과도한 사용:

parallelStream()은 성능을 향상시킬 수 있지만, 모든 상황에서 적합하지는 않다. 특히, 작은 데이터 셋에서의 병렬 처리나, 공유 자원을 많이 사용하는 경우 오히려 성능 저하를 초래할 수 있다.

 

▶ 어떤 상황에서 Stream을 사용하는 것이 좋을까?

Stream은 다음과 같은 상황에서 사용하면 좋다.

데이터 변환이 필요한 경우:

여러 개의 변환이나 필터링이 필요한 경우, Stream을 사용하면 코드가 간결해지고 가독성이 좋아진다.

데이터 필터링 및 정렬:

필터링 조건에 맞는 데이터를 선택하거나, 정렬이 필요한 경우 Stream의 filter(), sorted() 메서드를 활용할 수 있다.

데이터 집계 및 계산:

Stream의 reduce(), collect() 메서드를 사용하여 데이터 집계나 통계 계산을 수행할 때 유용하다.

병렬 처리가 유용한 경우:

대규모 데이터를 처리하거나 계산량이 많은 작업에서 성능 최적화를 위해 병렬 Stream을 사용할 수 있다.

 

▶ BookService 클래스에서 Stream 사용 예시 설명

BookService 클래스의 getAllBooks() 메서드는 Stream을 활용하여 데이터베이스에서 조회한 책 목록을 BookResponseDto로 변환하고, 등록 날짜에 따라 정렬하는 작업을 수행한다.

public List<BookResponseDto> getAllBooks() {
    return bookRepository.findAll().stream() .map(this::toResponseDto) .sorted((b1, b2)
            -> b1.getRegistrationDate().compareTo(b2.getRegistrationDate())) .collect(Collectors.toList()); }

stream()을 사용하여 데이터베이스에서 조회한 책 리스트를 스트림으로 변환한다.

map(this::toResponseDto)를 통해 Book 엔티티를 BookResponseDto로 변환한다.

sorted() 메서드를 사용하여 등록 날짜 기준으로 책 목록을 정렬한다.

collect(Collectors.toList())를 통해 스트림을 리스트로 변환하여 최종 결과를 반환한다.

 

▶ 스트림은 반복문일까? 

Stream은 반복문과 비슷한 작업을 할 수 있지만, 완전히 같은 개념은 아니다. 스트림은 컬렉션 데이터를 처리하는 데 사용되는 고급API로 데이터를 반복하여 처리하는 방식에서 더 나아가 선언형 프로그래밍 스타일을 제공한다. 

 

▷ 스트림(Stream)과 반복문(Loop)의 차이점

1. 동작방식 

→ 반복문(Loop):

for, while과 같은 반복문은 명령형 프로그래밍 방식을 사용하여, 명시적으로 반복을 수행하면서 각 요소에 대한 처리를 정의한다. 예를 들어, for 반복문에서 컬렉션의 각 요소를 하나씩 가져와서 처리한다.

→ 스트림(Stream):

스트림은 선언형 프로그래밍 방식을 사용하여, "어떻게 반복할지"가 아니라 "무엇을 할지"에 초점을 맞춘다. 내부적으로 반복을 처리하고, 데이터의 필터링, 매핑, 수집 등의 작업을 더 간결하게 표현할 수 있다.

2. 코드 간결성

→ 반복문은 종종 코드가 장황해질 수 있다. 예를들어, 인덱스를 관리하거나 조건을 명시적으로 처리해야 한다. 

→ 스트림을 사용하면 코드를 더 간결하고 직관적으로 작성할 수 있다. 스트림은 함수형 스타일의 메서드 체이닝을 통해 데이터를 처리한다. 

3. 병렬 처리 

→ 반복문은 기본적으로 순차적으로 실행되며, 개발자가 직접 병렬 처리를 구현해야 한다. 

→ 스트림에서는 parallelStream()을 사용하여 손쉽게 병렬 처리를 구현할 수 있다. 

4. 불변성(Immutability)

→ 반복문을 사용할 때는 컬렉션의 요소를 수정할 수 있다. 

→ 스트림은 불변성을 보장하여 원본 데이터를 수정하지 않고, 새로운 데이터로 변환해 처리한다. 이는 안전한 멀티스레드 프로그래밍을 가능하게 한다. 

 

▽ 반복문 예시

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> filteredNames = new ArrayList<>();

for (String name : names) {
        if (name.startsWith("A")) {
        filteredNames.add(name);
    }
            }

            System.out.println(filteredNames);
 

 ▽ 스트림(Stream) 예시 

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

List<String> filteredNames = names.stream()
        .filter(name -> name.startsWith("A"))
        .collect(Collectors.toList());

System.out.println(filteredNames); 

→ 두 코드는 똑같이 "Alice"를 출력하고, 스트림을 사용한 예시는 필터링 조건을 명시하는 방식으로 반복문보다는 코드가 간결하다.

 

▶ 정리 

이번 개발일지를 통해, Java Stream을 사용하는 이유와 장점, 그리고 주의사항에 대해 조금은 이해할 수 있었다. Stream은 간결하고 가독성 높은 코드를 작성하는 데 유용하지만, 잘못 사용하면 성능 문제나 디버깅의 어려움 등을 초래할 수 있다고한다. Stream의 장점과 단점을 잘 이해하고, 적절한 상황에서 효과적으로 사용하는 것이 중요한것같다.