Java8
Java에서는 java8 이전과 이후는 정말 많은 차이가 있을 정도로 바뀐점이 많은 업데이트였다
이러한 기법이 나오게 된 근본적인 배경은
사이드 Effect가 없는 병렬 처리에 대한 요구가 증가했기 때문이에요!
일반적으로 Collection 데이터를 바탕으로 데이터 연산이 진행되는데,
멀티 쓰레드 환경에서 데이터를 조작하게 된다면 java에서 비싼 연산 중 하나인 synchronized 를 사용할 수 밖에 없죠 ㅠㅠ
그래서 개발자들은 고민하게 됩니다
우리는 synchronized를 사용하지 않고, 병렬처리가 가능한 데이터 stream을 만들 수 없을까?
이러한 고민에 대한 해결책이 바로 Stream 이에요
Functional Programming을 기반으로 해서, 상태를 저장하지 않고 일련의 stream으로 데이터를 조작하는 방식입니다
내부 변수를 따로 저장하지 않기 때문에 side effect가 없고, 멀티쓰레드 환경에서 병렬처리가 가능하다는 것이죠!
그렇다면 어떻게 조작할 수 있을까요?
한번 차근차근 알아가봅시다
Lambda 표현식
먼저 우리는 labmda표현식(함수형 인터페이스)을 알아봐야 하는데요
크게 4가지 종류가 있습니다
함수형 인터페이스 | 함수 디스크립터 | 예시 |
Predicate<T> | T -> boolean | (string) -> string.startswith("k") |
Consumer<T> | T -> void | (string) -> System.out.println(string) |
Supplier<T> | () -> T | () -> new String("2") |
Function<T,R> | (T) -> R | (string) -> new Order(string) |
크게 람다 함수로 매개변수를 받을 것인지(Predicate, Consumer, Function)
혹은 아닐 것인지(Supplier) 가 있습니다!
예시는 예시일 뿐이니 한번 직접 작성해보시면서 공부하는 것도 좋은 것 같습니다!
그러면 궁금한게 많이 있을 텐데요,,,
그렇다면 나는 매개변수로 2개를 받고 싶은데, 이러한 인터페이스는 없는데 어떻게 해야하죠?
정말 간단합니다
만들면 되요 ㅎㅎㅎㅎ................. ?????
한번 예시를 들어볼게요
package com.huisam.springstudy.stream;
@FunctionalInterface
public interface TwoParameterFunction<T, U, R> {
R apply(T t, U u);
}
FunctionalInterface라는 어노테이션을 활용하면 쉽게 만들 수 있습니다
다만 함수형 인터페이스는 오직 한가지의 메서드만 가질 수 있으니 참고하세요!
그럼 한번 확인해보러 가볼까요?
우선 Entity를 하나 만들고,
package com.huisam.springstudy.stream;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@AllArgsConstructor
@Builder
@Getter
@EqualsAndHashCode(of = "id")
class Order {
private Long id;
private String name;
private String product;
private Integer price;
private String address;
public String nameWithProduct(TwoParameterFunction<String, String, String> function) {
return function.apply(name, product);
}
}
간단하게 사용자 이름과 제품 이름을 합쳐보는 인터페이스를 만들어 보고 싶네요!
@Test
@DisplayName("Order에서 custom Function 테스트")
void order_function_test() {
/* given */
Order order = new Order(1L, "hi", "book", 123, "seoul");
/* when */
String nameWithProduct = order.nameWithProduct((name, book) -> name + " " + book);
/* then */
assertThat(nameWithProduct).isEqualTo("hi book");
}
요렇게 런타임에서 람다 인터페이스를 만들면 내 마음대로 원하는 함수를 만들 수 있습니다!
이제 내 마음대로 함수형 인터페이스를 만들 수 있게 됬네요 ㅎㅎ
그럼 진짜 본론으로 들어가볼게요
Stream
데이터 병렬처리를 위해서는 for each를 활용한 방식보다는
stream을 적극적으로 활용하는 것이 좋습니다!!
우선 대표적인 특징 2가지가 존재하는데요
- 파이프라이닝: 스트림 연산은 스트림 연산끼리 연결이 가능한데, 그 덕분에 Laziness, Short Circuiting과 같은 최적화를 얻을 수 있다
- 내부 반복: for each를 활용해 iterator를 할 필요가 없다
그래서 위 특징을 하나씩 파보기전에..!!
일단 가독성적인 측면에서 많이 달라요 ㅎㅎ
한번 봐볼까요?
List<Order> findOrders = Lists.newArrayList();
for (Order order : orders) {
if (order.getName().startsWith("g")) {
findOrders.add(order);
}
}
Order findOrder = null;
try {
findOrder = findOrders.get(0);
} catch (ArrayIndexOutOfBoundsException e) {
throw new IllegalArgumentException("");
}
간단하게 Order들에서 'g'로 시작하는 이름을 찾고, 제일 먼저 찾은 첫번째만 찾고 싶을 상황을 작성한 코드인데요,
Stream을 활용하게 되면?
Order findOrder = orders.stream()
.filter(o -> o.getName().startsWith("g"))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
정말 간단해지는 것을 볼 수 있습니다!
그래서 스트림에는 어떤 연산이 있나.?
연산 | 형식 | 반환 형식 | 인수 | 디스크립터 |
filter | 중간 연산 | Stream<T> | Predicate<T> | T -> boolean |
map | 중간 연산 | Stream<R> | Function<T, R> | T -> R |
flatMap | 중간 연산 | Stream<R> | Function<T, Stream<R>> | T -> Stream<R> |
limit | 중간 연산 | Stream<T> | ||
sorted | 중간 연산 | Stream<T> | Comparator<T> | (T, T) -> int |
distinct | 중간 연산 | Stream<T> |
연산 | 형식 | 반환 형식 | 목적 |
forEach | 최종 연산 | void | 스트림을 소비하면서 람다를 적용 |
count | 최종 연산 | long | 스트림의 요소 개수를 반환 |
collect | 최종 연산 | 스트림을 리듀스해서 List, Map, 정수 형식의 컬렉션을 반환 |
엄청 많죠.? ㅎㅎ
하지만 추가되는게 몇 개 더 있습니다
java9에 추가된 중간 연산들이 있는데요,
연산 | 형식 | 반환 형식 | 인수 | 목적 |
takeWhile | 중간 연산 | Stream<T> | Predicate<T> | predicate이 마지막 true인 지점까지만 가진다 |
dropWhile | 중간 연산 | Stream<T> | Predicate<T> | predicate이 처음 false인 지점부터 가진다 |
limit | 중간 연산 | Stream<T> | long | 최대 스트림을 n개로 제한한다 |
skip | 중간 연산 | Stream<T> | long | 처음 n개의 stream을 건너뛴다 |
정말 마음대로 할 수 있겠다는 생각이 드네요 ㅎㅎ
하지만 더 있어요
위에서 본 findFirst와 같은 검색과 매칭에 대한 연산도 있습니다!
연산 | 형식 | 반환 형식 | 인수 | 목적 |
anyMatch | 최종 연산 | boolean | Predicate<T> | stream에서 적어도 하나의 요소가 predicate를 만족하는지 |
allMatch | 최종 연산 | boolean | Predicate<T> | stream에서 모든 요소가 predicate를 만족하는지 |
noneMatch | 최종 연산 | boolean | Predicate<T> | stream에서 모든 요소가 predicate를 만족하지 않는지 |
findFirst | 최종 연산 | Optional<T> | stream에서 첫번째 요소를 반환 | |
findAny | 최종 연산 | Optional<T> | stream에서 아무 요소를 반환 |
이러한 연산들이 ShortCircuit을 만족하는 스트림입니다!
그렇다면 스트림끼리 연산을 하는 방법은 무엇이 있을까요?
바로 flatMap입니다
flatMap은 어려우니 바로 예제로 살펴볼게요 ㅎㅎ
간단하게 2개의 숫자리스트를 하나의 Pair 리스트로 합치는 작업을 해볼게요!
@Test
@DisplayName("2개의 숫자 리스트 pair로 합치기")
void number_pair_test() {
/* given */
List<Integer> numbers1 = List.of(1, 2, 3);
List<Integer> numbers2 = List.of(4, 5);
/* when */
List<List<Integer>> answer = numbers1.stream()
.flatMap(n1 -> numbers2.stream()
.map(n2 -> List.of(n1, n2))
)
.collect(Collectors.toUnmodifiableList());
/* then */
assertThat(answer).isEqualTo(List.of(
List.of(1, 4), List.of(1, 5),
List.of(2, 4), List.of(2, 5),
List.of(3, 4), List.of(3, 5))
);
}
flatMap을 통해서 본인 자신의 스트림(n1)과 새로운 스트림(numbers2.stream())을 열어서
하나로 합치는 방법입니다!
이렇게 해서 Stream을 다양하게 활용하고, 왜 사용하는지에 대해 알아보았는데요 ㅎㅎ
제가 작성하지 못한 부분이 굉장히 많습니다!
한번씩 실습해보면서 하면 좋을 것 같아요!!
'Developer > Kotlin & Java' 카테고리의 다른 글
JVM과 Garbage Collection - G1GC vs ZGC (6) | 2020.11.02 |
---|---|
Java8 자바 안정적인 비동기 처리 - CompletableFuture (0) | 2020.05.22 |
자바 직렬화 - Java Serialization (0) | 2020.03.20 |
SOLID - DIP(Dependency Inversion Principle)란 : 의존성 역전 원칙 (8) | 2019.11.27 |
SOLID - ISP(Interface Segregation Principle)란? : 인터페이스 분리 원칙 (6) | 2019.11.26 |