[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 |
스트림 요소의 순서가 반드시 유지되지는 않는다 |