Java

자바로 같은 상품 Grouping

ybs 2023. 2. 5. 17:59
반응형

아래 장바구니 페이지를 보면 손목 보호대는 같은 상품 2개가 묶여서 담겨있다. 하지만 팔꿈치 보호대는 같은 상품임에도 1개씩 개별적으로 담겨있다. 그래서 같은 상품끼리 Grouping 해주는 작업이 추가적으로 필요하다.

 

먼저 Product 와 Item 의 관계를 이해해야한다. 위에서 Product 는 '슈퍼비스트 손목보호대'  '슈퍼비스트 팔꿈치, 무릎 관절보호대' 이고 Item 이 블랙, 팔꿈치(무릎은 선택안함) 이다. 쉽게 생각하면 Item 은 옵션정보다. 그래서 Product 는 여러개의 Item 들을 가질 수 있다.

import java.util.List;

import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class Product {
	String productId;

	List<Item> items;

	@Value
	@Builder
	public static class Item {
		String itemId;

		long itemQuantity;
	}
}

 

샘플로 만든 products 에는 총 3개의 Product 가 있고 그중 두개는 같은 productId 다. Item 레벨에서도 itemId 1 과 2 가 중복으로 같이 있다. 최종적으론 같은 productId, 같은 itemId 별로 Grouping 하는게 목적이다.

public class Main {
  public static void main(String[] args) {
    /**
     * [
     *   Product(
     *     productId=1,
     *     items=[
     *       Product.Item(itemId=1, itemQuantity=1),
     *       Product.Item(itemId=1, itemQuantity=1),
     *       Product.Item(itemId=2, itemQuantity=2)
     *     ]
     *   ),
     *   Product(
     *     productId=1,
     *     items=[
     *       Product.Item(itemId=3, itemQuantity=3),
     *       Product.Item(itemId=2, itemQuantity=2)
     *     ]
     *   ),
     *   Product(
     *     productId=2,
     *     items=[
     *       Product.Item(itemId=5, itemQuantity=5)
     *     ]
     *   )
     * ]
     */
     List<Product> products = getProducts();
  }

  private static List<Product> getProducts() {
    return List.of(
      Product.builder()
        .productId("1")
        .items(List.of(
          Item.builder()
            .itemId("1")
            .itemQuantity(1)
            .build(),
          Item.builder()
            .itemId("1")
            .itemQuantity(1)
            .build(),
          Item.builder()
            .itemId("2")
            .itemQuantity(2)
            .build()
        )
      )
      .build(),
      Product.builder()
        .productId("1")
        .items(List.of(
          Item.builder()
            .itemId("3")
            .itemQuantity(3)
            .build(),
          Item.builder()
            .itemId("2")
            .itemQuantity(2)
            .build()
        )
      )
      .build(),
      Product.builder()
        .productId("2")
        .items(List.of(
          Item.builder()
            .itemId("5")
            .itemQuantity(5)
            .build()
          )
        )
      .build()
    );
  }
}

 

같은 productId, 같은 itemId 별로 Grouping 시킨 결과는 ProductGroup 으로 만들었다. 중복없는 productId 별로 여러개의 ItemGroup 을 가질 수 있고, ItemGroup 안의 itemId 도 중복되지 않는다. 마지막으로 같은 itemId 의 itemQuantity 는 모두 합쳐진다.

@Value
@Builder(access = AccessLevel.PRIVATE)
public class ProductGroup {
	String productId;

	List<ItemGroup> itemGroups;

	@Value
	@Builder(access = AccessLevel.PRIVATE)
	public static class ItemGroup {
		String itemId;

		long totalQuantity;
	}
}

 

이제 Grouping 시키는 로직을 살펴보자. 제일 먼저 해야할것은 productId 별로 Product 들을 Grouping 해야한다.

public static List<ProductGroup> grouping(List<Product> products) {
  return products.stream()
    // 1차 작업 Map<String, List<Product>>
    .collect(Collectors.groupingBy(Product::getProductId, toList()))
    
    
    .entrySet() // Set<Map<K,V>.Entry<String, List<Product>>>
    .stream() // Stream<Map<K,V>.Entry<String, List<Product>>>
    
    // 2차 작업
    .map(ProductGroup::makeEachProductGroups) // Stream<ProductGroup>
    .toList();
}

 

 

1차 작업을 수행하면 아래 주석과 같이 Map으로 담겨진다. 이 Map 을 갖고 entrySet().stream() 을 활용해서 ProductGroup 을 만든다.

public static List<ProductGroup> grouping(List<Product> products) {
  return products.stream()

    /**
     * 1 차 작업 : Map<String, List<Product>> productListsById
     * {
     * 1=[
     *     Product(
     *       productId=1,
     *       items=[
     *         Product.Item(itemId=1, itemQuantity=1),
     *         Product.Item(itemId=1, itemQuantity=1),
     *         Product.Item(itemId=2, itemQuantity=2)
     *       ]
     *     ),
     *     Product(
     *       productId=1,
     *       items=[
     *         Product.Item(itemId=3, itemQuantity=3),
     *         Product.Item(itemId=2, itemQuantity=2)
     *       ]
     *     )
     *   ],
     * 2=[
     *     Product(
     *       productId=2,
     *       items=[Product.Item(itemId=5, itemQuantity=5)]
     *     )
     *   ]
     * }
     */
    .collect(Collectors.groupingBy(Product::getProductId, toList()))
    
    .entrySet() // Set<Map<K,V>.Entry<String, List<Product>>>
    .stream() // Stream<Map<K,V>.Entry<String, List<Product>>>
    
    // 2차 작업
    .map(ProductGroup::makeEachProductGroups) // Stream<ProductGroup>
    .toList();

 

cf) 아래와 같이 toUnmodifiableMap 을 쓰면, 중복된 productId 가 있을경우 duplicateKeyException 예외가 발생한다.

Map<String, Product> productsById = products.stream()
	.collect(Collectors.toUnmodifiableMap(Product::getProductId, Function.identity()));

 

이어지는 2차 작업작업에서 ProductGroup 을 만든다. 그리고 ItemGroup 단계(makeItemGroups)로 가서 먼저 flatMap(product -> product.getItems().stream()) 으로 Product 안 Item 들을 Stream으로 만든 후 itemId 별로 다시 Grouping 한다.

 

그 결과를 갖고 ItemGroup 을 만들고 totalQuantity 에는 Grouping 된 Item 들의 quantity 를 다 더한다.

private static ProductGroup makeEachProductGroups(Entry<String, List<Product>> productListById) {
  return ProductGroup.builder()
    .productId(productListById.getKey())
    .itemGroups(makeItemGroups(productListById.getValue()))
    .build();
  }


private static List<ItemGroup> makeItemGroups(List<Product> products) {
  return products.stream() // Stream<Product>
    .flatMap(product -> product.getItems().stream()) // Stream<Product.Item>
    .collect(groupingBy(Item::getItemId, toList())) // Map<String, List<Product.Item>>
    .entrySet().stream() // Stream<Map<K,V>.Entry<String, List<Product.Item>>>
    .map(it ->
      ItemGroup.builder()
        .itemId(it.getKey())
        .totalQuantity(it.getValue().stream().mapToLong(Item::getItemQuantity).sum())
        .build()
    ) // Stream<ProductGroup.ItemGroup>
    .toList();
  }

 

Grouping 해서 얻은 최종 결과는 아래와 같다.

	/**
	 * 2 차 작업 : grouping
	 * [
	 *   ProductGroup(
	 *     productId=1,
	 *     itemGroups=[
	 *       ProductGroup.ItemGroup(itemId=1, totalQuantity=2),
	 *       ProductGroup.ItemGroup(itemId=2, totalQuantity=4),
	 *       ProductGroup.ItemGroup(itemId=3, totalQuantity=3)
	 *     ]
	 *   ),
	 *   ProductGroup(
	 *     productId=2,
	 *     itemGroups=[
	 *       ProductGroup.ItemGroup(itemId=5, totalQuantity=5)
	 *     ]
	 *   )
	 * ]
	 */

 

makeItemGroups 로직에서 주의해야할 게 하나있다. flatMap(product -> product.getItems().stream()) 으로 Product 안 Item 들을 Stream으로 만든 후 itemId 별로 다시 Grouping 해야한다.

 

아래와 같이 각 product 별 item 을 이용해 Grouping 하면 안된다.

private static List<ItemGroup> makeItemGroupsWrongWay(List<Product> products) {
  return products.stream()
    .flatMap(product -> //  proudct 마다 하면 안됌
      product.getItems().stream()
        .collect(groupingBy(Item::getItemId, toList()))
        .entrySet()
        .stream()
        .map(it ->
          ItemGroup.builder()
            .itemId(it.getKey())
            .totalQuantity(it.getValue().stream().mapToLong(Item::getItemQuantity).sum())
            .build()
        ))
        .toList();
}

 

반응형