본문 바로가기
Java

리스트 객체를 특정 key 기준으로 grouping 그리고 merge

by ybs 2021. 9. 20.
반응형

아래와 같이 Product 리스트가 있다고 해보자. Product 객체에는 id(고유 식별자), productId, itemNo(리스트) 필드가 있다. 여기서 id와 productId의 차이에 대한 설명은 비지니스 정책 이슈라 생략하려 한다. 중요한건 productId는 중복이 가능하다. 그래서 prodocutId를 기준으로 grouping 을 하고 itemNo 리스트값을 중복없이 merge 하려고 한다.

List<Product> products = Arrays.asList(
	new Product("1", "a", Arrays.asList("11", "12", "13")),
	new Product("2", "a", Arrays.asList("12", "13", "14")),
	new Product("3", "b", Arrays.asList("31", "32", "33")),
	new Product("4", "b", Arrays.asList("32", "33", "34"))
);
원하는 결과(Map 으로 변환) : 
Map.key : productId=a,
Map.value : ProductApiRequest(productId=a, itemNo=[11, 12, 13, 14]),

Map.key : productId=b,
Map.value : ProductApiRequest(productId=b, itemNo=[31, 32, 33, 34]),

 

그동안은 이런 문제(또는 비슷한 문제)가 주어졌을 때 항상 for문과 Map.containsKey 를 활용했었다.

그러다 최근에 같이 일하는 팀원분이 리팩토링한 코드를 보고, 새로운 방식을 알게 되어 동작 원리를 정리했다.

 

먼저 기존에 사용했던 방식이다. Product 리스트를 순회하면서 productId 가 Map에 없으면 새롭게 추가, 있으면 기존것을 수정했다.

@Test
void method1() {
	List<Product> products = Arrays.asList(
		new Product("1", "a", Arrays.asList("11", "12", "13")),
		new Product("2", "a", Arrays.asList("12", "13", "14")),
		new Product("3", "b", Arrays.asList("31", "32", "33")),
		new Product("4", "b", Arrays.asList("32", "33", "34"))
	);

	Map<String, ProductApiRequest> requestsByProductId = new LinkedHashMap<>();
	for (Product product : products) {
		if (!requestsByProductId.containsKey(product.getProductId())) {
			ProductApiRequest productApiRequest = new ProductApiRequest(
				product.getProductId(),
				new HashSet<>(product.getItemNo())
			);

			requestsByProductId.put(product.getProductId(), productApiRequest);
		} else {
			ProductApiRequest exist = requestsByProductId.get(product.getProductId());
			exist.getItemNo().addAll(product.getItemNo());
			requestsByProductId.replace(product.getProductId(), exist);
		}
	}

	List<ProductApiRequest> collect = requestsByProductId.values()
		.stream()
		.collect(toUnmodifiableList());
	System.out.println(collect);
}
public class Product {
	private String id;
	private String productId;
	private List<String> itemNo;

	public Product(String id, String productId, List<String> itemNo) {
		this.id = id;
		this.productId = productId;
		this.itemNo = itemNo;
	}

	public String getProductId() {
		return productId;
	}

	public List<String> getItemNo() {
		return itemNo;
	}
}

public class ProductApiRequest {
	private String productId;
	private Set<String> itemNo;

	public ProductApiRequest(String productId, Set<String> itemNo) {
		this.productId = productId;
		this.itemNo = itemNo;
	}

	public String getProductId() {
		return productId;
	}

	public Set<String> getItemNo() {
		return itemNo;
	}
}

 

다음은 같이 일하는 팀원분이 리팩토링한 방식이다. 먼저 Product 리스트인 products 를 stream으로 돌려서 map을 수행하는데, from 메서드를 통해 ProductApiRequest 객체로 만든다. 기존 방식에서는 Map.containsKey() 가 false인 상황에 해당한다. 

@Test
void method2() {
	List<Product> products = Arrays.asList(
		new Product("1", "a", Arrays.asList("11", "12", "13")),
		new Product("2", "a", Arrays.asList("12", "13", "14")),
		new Product("3", "b", Arrays.asList("31", "32", "33")),
		new Product("4", "b", Arrays.asList("32", "33", "34"))
	);

	Map<String, ProductApiRequest> requestsByProductId = products.stream()
		.map(product -> ProductApiRequest.from(product))
		.collect(toUnmodifiableMap(
			productApiRequest -> productApiRequest.getProductId(),
			identity(),
			(thisProduct, otherProduct) -> thisProduct.merge(otherProduct)
		));

	List<ProductApiRequest> collect = requestsByProductId.values()
		.stream()
		.collect(toUnmodifiableList());
	System.out.println(collect);
}
// ProductApiRequest 에 추가
public static ProductApiRequest from(Product product) {
	return new ProductApiRequest(
		product.getProductId(),
		new HashSet<>(product.getItemNo())
	);
}

 

그 다음 unmodifiableMap으로 만든다. 만드는데 필요한 3개 인자는 아래와 같다.

public static <T, K, U>
Collector<T, ?, Map<K,U>> toUnmodifiableMap(Function<? super T, ? extends K> keyMapper,
                                            Function<? super T, ? extends U> valueMapper,
                                            BinaryOperator<U> mergeFunction) {
    ...
    // toMap 메서드의 주요 코드
    (map, element) -> map.merge(
		keyMapper.apply(element), // productApiRequest -> productApiRequest.getProductId()
		valueMapper.apply(element), // identity()
		mergeFunction // (thisProduct, otherProduct) -> thisProduct.merge(otherProduct)
    );
}

 

그리고 내부적으로 Map의 merge (default 메서드)를 사용한다.

default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if (newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

 

마지막으로 merge를 어떻게 할건지를 작성해야 한다. 다시말해 remappingFunction.apply(oldValue, value) 에 해당하는 람다 바디를 만들어야 한다. 해당 코드는 (thisProduct, otherProduct) -> thisProduct.merge(otherProduct) 이고 ProductApiRequest 의 merge 메서드는 아래와 같다. 

// ProductApiRequest 에 추가
public ProductApiRequest merge(ProductApiRequest otherProduct) {
	return new ProductApiRequest(
		this.productId,
		Stream
			.concat(this.itemNo.stream(), otherProduct.itemNo.stream())
			.collect(toUnmodifiableSet())
	);
}

itemNo 를 합치기 위해서 Stream.concat 을 사용한다. 그리고 합친 후에 toUnmodifiableSet 으로 만들어 중복을 제거해준다.

 

반응형

'Java' 카테고리의 다른 글

Mono.defer  (0) 2021.11.11
The dark side of distributed systems  (0) 2021.11.04
ip 범위 구하고 범위안에 포함되는지(with subnetmask)  (0) 2021.08.24
재귀 호출 최적화(Tail-Call)  (2) 2021.05.11
Singleton(면접단골질문)  (0) 2021.05.09