티스토리 뷰

Java Stream API 란?

Java8 API에 새로 추가된 기능입니다. 컬렉션, 배열 등을 함수형 프로그래밍을 활용하여 반복 처리하도록 도와주는 API입니다.

그럼 Stream API를 왜 쓸까?

1. 컬렉션 데이터를 선언형으로 코드를 구현할 수 있습니다.

With For-Loop

List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish dish : menu) {
  if(dish.getCalories < 400) {
      lowCarloricDishes.add(dish);
  }
}

With Stream API

List<String> lowCaloricDishesName = 
    menu.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());

2. 골치아프게 멀티스레드 코드를 별도로 구현하지 않고, 병렬로 처리할 수 있습니다.(parallelStream() 과 같은 걸 사용해서)

List<String> lowCaloricDishesName =
    menu.parallelStream()
        .filter(d -> d.getCalories() < 400)
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());

다만, Stream API는 매우 비싼 연산이기 때문에, 멋져보인다고 막 남용하면 안됩니다. 너무나도 좋은 글을 참고하였습니다.

for-loop 를 Stream.forEach() 로 바꾸지 말아야 할 3가지 이유

 

Stream API을 활용하여 Collection 데이터를 연산하는 방법을 알아보겠습니다.

Stream 생성

Stream API의 생성 과정은 크게 세가지로 나뉘어집니다.

  1. Collection과 같은 데이터 소스로 Stream 생성
  2. Stream 파이프라인을 구성할 중간연산
  3. 파이프라인을 수행하고 결과를 만들 최종연산
List<String> words = Arrays.asList("show", "me", "the", "money", "money");

List<String> distinctWords =
        words.stream()                        // 1. Stream 생성
        .filter(word -> word.length() > 3)    // 2. 중간연산
        .distinct()                          // 2. 중간연산
        .collect(Collectors.toList());        // 3. 최종연산

아래는 중간 연산과 최종 연산의 예입니다.

연산 종류 연산 Return Type Parameter Descriptor
중간 연산 filter Stream<T> Predicate<T> T -> boolean
map Stream<R> Function<T, R> T -> R
limit Stream<T> -  
sorted Stream<T> Comparator<T> (T, T) -> int
distinct Stream<T> -  

 

연산 종류 연산 Return Type 설명
최종 연산 forEach void 스트림의 각 요소를 소비하면서 람다를 적용함
count long 스트림의 요소 개수를 반환함
collect Collection 스트림을 Collection 형태로 반환함

 

함수형 인터페이스 사용

연산의 파라미터로 java.util.function 에 정의되어있는 함수형 인터페이스를 사용합니다. 대표적으로 Predicate, Consumer, Function 세개의 함수형 인터페이스를 알아봅니다. 

Predicate

filter의 파라미터로 사용됩니다. 제네릭의 T 타입 객체를 파라미터로 받아서, boolean 타입을 리턴하는 test 추상 메소드를 정의합니다.

filter와 사용할 때, boolean 타입으로 반환 할 filtering 조건식을 filter 메소드의 인자로 넘겨줍니다. 

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

 

@Test
void test_predicate(){
	List<Integer> integers = Arrays.asList(3, 4, 10, 11, 100, 101, 102, 500, 2000);

	Predicate<Integer> overHundredPredicate = i -> (i > 100);

	assertThat(integers.stream()
			.filter(overHundredPredicate)
			.count()).isEqualTo(4);
}

 

Consumer

인터페이스명처럼 소비만 하고, 별도의 리턴은 하지않습니다.

peek() 나 foreach() 메소드의 파라미터 값으로 활용 되고, 인자로 받아서 실행 후 별도 리턴없이 종료됩니다.

@FunctionalInterface
public interface Consumer<T> {
...
    void accept(T t);
...
}
@Test
void test_consumer(){
	List<Integer> integers = Arrays.asList(3, 4, 10);

	Consumer<Integer> integerConsumer = System.out::println;

	integers.stream()
    	.forEach(integerConsumer);
}

 

Function

인자를 받아서 특정 값으로 반환하는 apply 추상 메소드를 구현합니다.

주로 stream의 map()과 사용하여, 스트림 안의 값들을 하나씩 특정 값으로 변환합니다. 

@FunctionalInterface
public interface Function<T, R> {
...
    R apply(T t);
...
}
@Test
void test_function(){
	List<Integer> integers = Arrays.asList(3, 4, 10);

	Function<Integer, String> integerToString = i -> i.toString();

	List<String> stringList =
    	integers.stream()
        	.map(integerToString)
		.collect(Collectors.toList());
}

 

Stream 특징

일회성

Stream은 한번 소비되면 재활용이 불가능합니다. 아래와 같이 한번 사용 후 재사용 시 IllegalStateException 예외를 던집니다.

@Test
void should_ThrowIllegalStateException_When_CallStreamTwice() {
	List<String> words = Arrays.asList("show", "me", "the", "money", "money");

	Stream<String> stream =
		words.stream()
			.filter(word -> word.length() > 3)
			.distinct();

	assertThat(stream.findFirst().get()).isEqualTo("show");	// 1번째 호출
	assertThrows(IllegalStateException.class,
    			() -> stream.findFirst(),	// 2번째 호출
            	"stream has already been operated upon or closed");
}

 

실무에서 Stream API 사용해보기

Entity

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class LoginHistory {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idx;

    @Column(columnDefinition = "datetime DEFAULT CURRENT_TIMESTAMP")
    private LocalDateTime createdAt;

    ...

DTO

public class LoginHistoryFindResponseDto {
...
public static LoginHistoryFindResponseDto from(LoginHistory loginHistory){
    return LoginHistoryFindResponseDto
        .builder()
        .createdAt(loginHistory.getCreatedAt())
        .deviceType(loginHistory.getDevice())
        .loginTag(loginHistory.getTag())
        .build();
}

Service

아래와 같이 Service Layer에서 조회한 Entity List를 DTO List로 반환 하는 용도로 사용합니다.

@Service
@RequiredArgsConstructor
public class LogService {

    private final LoginHistoryRepository loginHistoryRepository;

...
@Transactional(readOnly = true)
public List<LoginHistoryFindResponseDto> findAllLoginHistory(User user){
    List<LoginHistoryFindResponseDto> loginHistories =
        loginHistoryRepository.findAllByUser(user)
            .stream()
            .map(LoginHistoryFindResponseDto::from)
            .collect(Collectors.toList());

    return loginHistories;
}
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
«   2024/04   »
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
글 보관함
Total
Today
Yesterday