[ODOP:08]Java 함수형 프로그래밍 - 07. 스트림의 활용(Collector)

벤 바이디히 의 'A Functional Approach to Java' 를 읽고 정리 한 내용입니다 Java 의 Stream 에서 지원하는 Collector 를 다루고 있습니다

Collector

스트림을 컬렉션으로 만들고자 할 때 일반적인 List, Set 컬렉션 구성은 몹시 Easy 합니다
하지만 Map 같은 형태를 만들기 위해서는 복잡한 데이터 조작이 필요할 수 있습니다
이럴 때는 다운스트림 컬렉터를 이용할 수 있습니다

Down stream collector

java.util.stream.Collectors 를 살펴보면 여러 기능들을 제공 한다

  • transforming (변환)
  • reducing (축소)
  • flattening (평탄화)
  • filtering (필터링)
  • composite collector operation (복합 컬렉터 연산)
    테스트를 위해서는 데이터 모델이 필요 합니다
record User(
	UUID id, 
	String group, 
	LocalDateTime lastLogin,
	List logEntries
) { 
	public static User createIn(String group, int logCount) {  
	    List logEntries = new ArrayList(logCount);  
	    for (int i = 0; i < logCount; i++) {  
	       logEntries.add("log" + i);  
	    }  
	    return new User(
		    UUID.randomUUID(), 
		    group, 
		    LocalDateTime.now(), 
		    logEntries
		);  
	}
}

List<User> users = List.of(  
    User.createIn("A", 1),  
    User.createIn("A", 3),  
    User.createIn("B", 2),  
    User.createIn("B", 6),  
    User.createIn("C", 4),  
    User.createIn("C", 4),  
    User.createIn("C", 3)  
);

grouping by

code: Group 이름을 기준으로 User 정보 추출 -> Map<String, List<User>>

User 의 group 을 기준으로 Map 으로 만든다면 아래와 같은 형태로 코드를 만들 수 있다

Map<String, List<User>> collect = users.stream()  
    .collect(Collectors.groupingBy(User::group));  
  
System.out.println("collect = " + collect);
result:
{
	A=[
		User[
			id=5615df99-cac1-4d93-9284-886fbd922ac0, 
			group=A, 
			lastLogin=2024-08-11T13:52:19.753292, 
			logEntries=[]
		], 
		User[
			id=22584fa7-2a57-440f-9bf2-4890788369ee, 
			group=A, 
			lastLogin=2024-08-11T13:52:19.753400, 
			logEntries=[]
		]
	], 
	B=[
		User[
			id=0bdcbff2-6e06-4fe2-ad0f-4a6f725d5a0b, 
			group=B, 
			lastLogin=2024-08-11T13:52:19.753437, 
			logEntries=[]
		], 
		User[
			id=4d3627af-a2be-4e23-b70f-edeac7d95035, 
			group=B, 
			lastLogin=2024-08-11T13:52:19.753471, 
			logEntries=[]
		]
	], 
	C=[
		User[
			id=b67b981b-995c-4265-a4e4-13bd0dbd8054, 
			group=C, 
			lastLogin=2024-08-11T13:52:19.753477, 
			logEntries=[]
		], 
		User[
			id=28fb19aa-d6d9-4a86-8c84-a0c96144c30a, 
			group=C, 
			lastLogin=2024-08-11T13:52:19.753515, 
			logEntries=[]
		], 
		User[
			id=31b6c5ee-fee9-4657-98f0-a83559a1260a, 
			group=C, 
			lastLogin=2024-08-11T13:52:19.753544, 
			logEntries=[]
		]
	]
}
code: Group 이름 기준으로 User ID 추출 -> Map<String, List<UUID>>
Map<String, List<UUID>> collect = users.stream()  
    .collect(  
       Collectors.groupingBy(  
          User::group,  
          Collectors.mapping(  
             User::id,  
             Collectors.toList()  
          )  
       )  
    );  
  
System.out.println("collect = " + collect);
result
collect = {
	A=[
		f43db1af-d57a-48d6-b707-d32db7ea1c92, 
		9adf9235-7e9b-437c-9183-07743f083989
	], 
	B=[
		9c5ff2de-1000-44a4-b958-8f8206196f16, 
		f60dfb1f-9878-4c9d-8002-e5acddaf231e
	], 
	C=[
		feca21f7-c1d5-481f-9f8c-b8b8a5ef0020, 
		3964357a-e9be-4661-a893-1ff89eedec2a, 
		c26a90f6-c68f-4044-9506-6b207209a6f4
	]
}

요소 축소

code: User 별 log 수 카운트 -> Map<String, Integer>
Collector<Integer, ?, Integer> summingUp = 
					Collectors.reducing(0, Integer::sum);
  
var downstream = Collectors.mapping(  
    (User user) -> user.logEntries().size(),  
    summingUp  
);
  
Map<UUID, Integer> collect = users  
    .stream()  
    .collect(Collectors.groupingBy(  
       User::id,  
       downstream  
    ));
  
System.out.println("collect = " + collect);
result:
collect = {
	5f10754d-d083-4e5c-a1d8-f16053ed8294=1, 
	055408f9-f488-4849-931e-48e0fa98a42c=6, 
	51a3e4af-e3af-4df8-8836-0bfca2987911=3, 
	3d095706-031c-4670-bbe0-684ac7d1a830=4, 
	9d2bc95a-580c-4964-b1b7-2b4f2ed7ed5c=3, 
	44927a72-1f57-44b5-a271-89c9e447dfa1=2, 
	62d658ea-041f-4747-9881-cdd3f8d87903=4
}

컬렉션 평탄화

Collector<User, ?, List<String>> downStream = Collectors.flatMapping(  
    (User user) -> user.logEntries().stream(),  
    Collectors.toList()  
);  
  
Map<UUID, List<String>> collect = users  
    .stream()  
    .collect(Collectors.groupingBy(  
       User::id,  
       downStream  
    ));  
  
System.out.println("collect = " + collect);
result
collect = {
	c1c63ce8-a436-4cdb-82b0-dcb2ee9f082f=[log0], 
	51b872ee-fb2b-484b-9ab4-e24bd0bf6710=[log0, log1, log2], 
	4f301d67-964d-48df-90d0-253d7e47a1a9=[log0, log1, log2, log3], 
	e53382c9-a33e-4c19-9fde-f5fa7d05c7b5=[log0, log1, log2], 
	0874bcf1-c8f6-4454-a219-31a72d3f611b=[log0, log1, log2, log3, log4, log5], 
	7e3b1d1d-9321-40bf-bf16-fe71e5de55ac=[log0, log1], 
	f94db876-3660-4d41-8065-ffe0b9ba219a=[log0, log1, log2, log3]
}

요소 필터링 (예제 없음)

기존 filter 와 다른점이 딱히 없으므로 예제를 추가하지 않았다

합성 컬렉터

이 컬렉터는 한 번에 두 개의 다운 스트림 컬렉터를 동시에 처리하고 그 결과를 하나로 통합 합니다

code: 2023, 2024 접속자의 로그만 카운팅

먼저 데이터 코드를 변경 한다

List<User> users = List.of(  
    User.createIn("A", 1, LocalDateTime.of(2022, 1, 1, 0, 0)),  
    User.createIn("A", 3, LocalDateTime.of(2023, 1, 1, 0, 0)),  
    User.createIn("B", 2, LocalDateTime.now()),  
    User.createIn("B", 6, LocalDateTime.of(2023, 1, 1, 0, 0)),  
    User.createIn("C", 4, LocalDateTime.of(2022, 1, 1, 0, 0)),  
    User.createIn("C", 4, LocalDateTime.now()),  
    User.createIn("C", 3, LocalDateTime.of(2022, 1, 1, 0, 0))  
);
public record User(  
    UUID id,  
    String group,  
    LocalDateTime lastLogin,  
    List<String> logEntries  
) {  
    public static User createIn(String group, int logCount) {  
       return User.createIn(group, logCount, LocalDateTime.now());  
    }  
  
    public static User createIn(String group, int logCount, LocalDateTime lastLogin) {  
       List logEntries = new ArrayList(logCount);  
       for (int i = 0; i < logCount; i++) {  
          logEntries.add("log" + i);  
       }  
       return new User(UUID.randomUUID(), group, lastLogin, logEntries);  
    }  
}

2022 년을 제외 한 2023, 2024 년도에 로그인 한 사람들의 로그를 카운팅 한다

public static void teeing(List<User> users) {  
    Collector<User, ?, Integer> beforeYearsFilter = 
		    Collectors.filtering(  
		        (User user) -> {
			        return user.lastLogin().getYear() == 
							(LocalDateTime.now().getYear() - 1)
			    },  
		        Collectors.mapping(  
				    (User user) -> user.logEntries().size(),  
				    Collectors.reducing(0, Integer::sum)  
		        )  
		    );
		
		
    Collector<User, ?, Integer> lastYearsFilter = 
		    Collectors.filtering(  
			    (User user) -> {
				    return user.lastLogin().getYear() == 
						    LocalDateTime.now().getYear()
				},
		        Collectors.mapping(  
			        (User user) -> user.logEntries().size(),  
			        Collectors.reducing(0, Integer::sum)
				)  
		    );  
  
    Map<UUID, Integer> collect = users  
		.stream()  
		.collect(
			Collectors.groupingBy(  
				User::id,  
				Collectors.teeing(  
					beforeYearsFilter,  
					lastYearsFilter,  
					(before, last) -> before + last  
				)  
			)
		);  
  
    System.out.println("users = " + users);  
    System.out.println("collect = " + collect);  
}
result:
users = [
	User[
		id=021e6af7-b9f8-401f-9ae0-57f1ec9579e9, 
		group=A, 
		lastLogin=2022-01-01T00:00, 
		logEntries=[log0]
	], 
	User[
		id=224a9442-a50e-455e-83aa-320c10335b55, 
		group=A, 
		lastLogin=2023-01-01T00:00, 
		logEntries=[log0, log1, log2]
	], 
	User[
		id=121be3a1-b7aa-4f68-af8f-282113221e13, 
		group=B, 
		lastLogin=2024-08-11T16:29:39.034203, 
		logEntries=[log0, log1]
	], 
	User[
		id=f0208259-b8b3-4618-926c-a710750b6ed3, 
		group=B, 
		lastLogin=2023-01-01T00:00, 
		logEntries=[log0, log1, log2, log3, log4, log5]
	], 
	User[
		id=d0b848ef-9307-467f-971e-f0b272bff31e, 
		group=C, 
		lastLogin=2022-01-01T00:00, 
		logEntries=[log0, log1, log2, log3]
	], 
	User[
		id=679cacaa-6d44-4e4b-b194-cf438d60ad62, 
		group=C, 
		lastLogin=2024-08-11T16:29:39.034346, 
		logEntries=[log0, log1, log2, log3]
	], 
	User[
		id=cc8a8ee4-2296-49f4-89e0-81ee1fe5bcb9, 
		group=C, 
		lastLogin=2022-01-01T00:00, 
		logEntries=[log0, log1, log2]
	]
]

collect = {
	d0b848ef-9307-467f-971e-f0b272bff31e=0, // 2022
	021e6af7-b9f8-401f-9ae0-57f1ec9579e9=0, // 2022
	f0208259-b8b3-4618-926c-a710750b6ed3=6, // 2023
	121be3a1-b7aa-4f68-af8f-282113221e13=2, // 2024
	224a9442-a50e-455e-83aa-320c10335b55=3, // 2023
	679cacaa-6d44-4e4b-b194-cf438d60ad62=4, // 2024
	cc8a8ee4-2296-49f4-89e0-81ee1fe5bcb9=0  // 2022
}

나만의 컬렉터

java.util.stream.Collections 에서 컬렉터(컬렉션 아님)를 정의하고 커스텀 하기 위해서는 아래의 4개 메서드를 이해하면 사용자 정의 컬렉터를 만들 수 있다

  • Supplier<A> supplier()
  • BiConsumer<A, T> accumulator()
  • BinaryOperator<A> combiner()
  • Function<A, R> finisher()

또한, java.util.Collector.Characteristics 의 특성을 부여하여 이미 구현되어 있는 여러 최적화 기능을 사용 할 수 있다

특성 설명
CONCURRENT 병렬 처리를 지원 한다
IDENTITY_FINISH finisher 는 항등 함수로, 누적기 자체를 반환한다.
finisher 호출 대신 형 변환이 필요하다
UNORDERED 스트림 요소의 순서가 반드시 유지되지는 않는다