아래 장바구니 페이지를 보면 손목 보호대는 같은 상품 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();
}
'Java' 카테고리의 다른 글
java stream 으로 Map 다루는 방법 (0) | 2023.04.06 |
---|---|
Enum 타입 에 따른 테스트(DynamicTest) (0) | 2023.02.18 |
index 가 필요해도 자바 고전 for 문 안쓰는 방법 (0) | 2023.01.14 |
자바 Function.andThen 과 apply 작동 순서 (0) | 2022.08.30 |
함수형 파라미터를 두번 전달하는 이슈 정리 (0) | 2022.01.05 |