본문 바로가기
Java

java stream 으로 Map 다루는 방법

by ybs 2023. 4. 6.
반응형

이전글 에서 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
	));

 

반응형