java stream 으로 Map 다루는 방법
이전글 에서 java stream 으로 Grouping 하는 방법을 설명하면 중간에 map 을 이용하는 방법을 공유했다. 이 글에서는 다른 예제로 설명을 해보려고 한다.
먼저 product 와 item 구조는 아래와 같다. product 안에 item 들을 리스트로 가질 수 있다.
@Value
@Builder
public class Product {
long productId;
List<Item> items;
@Value
@Builder
public static class Item {
long itemId;
}
}
외부 api 를 호출해서 여러개의 product 들을 가져온다고 해보자. 이때 item 을 다루기 위해선 이중 for 문이 필요하다. product 도 여러개인데 각 product 마다 item 들도 여러개이기 때문이다. itemId 별로 Item 객체를 담는 Map 을 만들려면 아래와 같이 이중 for 문 안에서 put 을 해주면 된다.
public class Main {
public static void main(String[] args) {
// 외부 api 호출 해서 product 리스트 결과 얻어옴
ApiClient apiClient = new ApiClient();
List<Product> products = apiClient.getProducts().collectList().block();
Map<Long, Item> itemsByItemId = new HashMap<>();
for (Product product : products) {
for (Item item : product.getItems()) {
itemsByItemId.put(item.getItemId(), item);
}
}
System.out.println(itemsByItemId);
}
}
하지만 이중 for 문 대신 stream 을 사용하는 것으로 바꾸고 싶었다. 그래서 1차로 작업한 코드는 아래와 같다. 딱봐도 뭔가 복잡하다. products.stream 을 시작으로 그안에서 item.stream 을 다시 시작한다. item 별로 Map.of 를 통해 Map 을 만들어 합치고, product 레벨에서 다시 또 합쳐야 하니 flatMap(map -> map.entrySet().stream()) 과 collect(Collectors.toMap) 이 두번씩 들어갔다.
public class Main {
public static void main(String[] args) {
// 외부 api 호출 해서 product 리스트 결과 얻어옴
ApiClient apiClient = new ApiClient();
List<Product> products = apiClient.getProducts().collectList().block();
Map<Long, Item> itemsByItemId = products.stream() // Stream<Product>
.map(product ->
product.getItems().stream() // Stream<Item>
.map(item ->
Map.of(item.getItemId(), item)) // Stream<Map<Long, Item>>
.flatMap(map -> map.entrySet().stream()) // Stream<Entry<Long, Item>>
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue,
(item1, item2) -> item2
)
)
) // Stream<Map<Long, Item>>
.flatMap(map -> map.entrySet().stream()) // Stream<Entry<Long, Item>>
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue,
(item1, item2) -> item2
)
);
System.out.println(itemsByItemId);
}
}
그런데 생각해보면 굳이 매번 Map.of 로 만들필요가 없었다. product 마다 존재하는 Item 리스트들을 flatMap 으로 먼저 정리한다음, 한번만 Map.of 로 만들면 되니까 코드가 좀 더 심플해졌다.
public class Main {
public static void main(String[] args) {
// 외부 api 호출 해서 product 리스트 결과 얻어옴
ApiClient apiClient = new ApiClient();
List<Product> products = apiClient.getProducts().collectList().block();
Map<Long, Item> itemsByItemId = products.stream() // Stream<Product>
.flatMap(product -> product.getItems().stream()) // Stream<Item>
.map(item -> Map.of(item.getItemId(), item)) // Stream<Map<Long, Item>>
.flatMap(map -> map.entrySet().stream()) // Stream<Entry<Long, Item>>
.collect(Collectors.toMap(
Entry::getKey,
Entry::getValue,
(item1, item2) -> item2
)
);
System.out.println(itemsByItemId);
}
}
그런데 사실 collect(Collectors.toMap) 으로 Map 을 만들어주기 때문에 굳이 내가 직접 Map.of 를 사용해서 작업할 필요가 없다. 최종적으로는 아래와 같이 훨씬 심플한 코드가 됐다.
public class Main {
public static void main(String[] args) {
// 외부 api 호출 해서 product 리스트 결과 얻어옴
ApiClient apiClient = new ApiClient();
List<Product> products = apiClient.getProducts().collectList().block();
Map<Long, Item> itemsByItemId = products.stream() // Stream<Product>
.flatMap(product -> product.getItems().stream()) // Stream<Item>
.collect(Collectors.toMap(Item::getItemId, Function.identity()));
System.out.println(itemsByItemId);
}
}
마지막으로 Map type 으로 collect 되는 3가지 경우를 간단히 정리했다.
Map<String, List<Product>> productsById = products.stream()
.collect(Collectors.groupingBy(Product::getProductId, toList()));
// 중복된 productId 가 나오면 duplicateKeyException 예외 발생
Map<String, Product> productById = products.stream()
.collect(toUnmodifiableMap(Product::getProductId, Function.identity()));
// 중복된 productId 가 나오면 mergeFunction 에 의해 합쳐져서 예외 발생 안함
Map<String, Product> collect = products.stream()
.collect(toUnmodifiableMap(
Product::getProductId,
Function.identity(),
(product1, product2) -> product2
));