Kuma's Curious Paradise
Tuple, zip(), flatMap() 이해하기 본문
메인 페이지를 개발하다 보면, 여러 서버를 찔러서 한꺼번에 데이터를 가져와 합쳐야 하는 경우가 있다.
이들은 속도가 중요하기 때문에 Reactor 형식으로 구성되어 있다.
예를 들어, 메인 페이지에 필요한 메뉴가 4가지라면 다음과 같은 비동기 호출이 있을 수 있다.
Mono<MenuDto> fetchFirstMenu();
Mono<MenuDto> fetchSecondMenu();
Mono<MenuDto> fetchThirdMenu();
Mono<MenuDto> fetchFourthMenu();
이때 결과를 합쳐서 TotalMenuResponseDto에 담아야 한다면 어떻게 해야 할까?
고민이 된다면 tupel과 zip(), flatMap() 등의 개념을 이해해야 할 때다.
1. Tuple이란 무엇인가?
Tuple(튜플)은 수학에서 “순서가 있는 값들의 모음”이라는 뜻이다.
Tupe은 다음과 같은 특징을 지닌다.
- 순서가 있다. (Tuple2<T1, T2>라면 첫 번째 값은 항상 T1, 두 번째는 항상 T2)
- 위치(index) 기반 접근이 가능하다. (tuple.getT1(), tuple.getT2())
- 불변(immutable)이다. 한 번 만들어지면 수정할 수 없다.
Tuple과 List의 차이
| 길이 | 고정 (Tuple2, Tuple3, …) | 가변 (길이가 줄어들거나 늘어날 수 있다.) |
| 접근 | getT1(), getT2() 등 위치별 메서드 | get(0), get(1) 등 index로 접근 |
| 불변성 | 불변 | 가변 |
| 쓰임새 | 서로 다른 타입의 값들을 묶을 때 | 동일 타입의 여러 값을 담을 때 |
정리하면, Tuple은 “서로 다른 타입의 값들을 묶어 한 덩어리로 다루고 싶을 때” 사용한다.
예를 들어 (User, Order) 같은 묶음은 List보다는 Tuple2<User, Order>을 사용해야 한다.
2. zip() — 여러 비동기 흐름을 하나로 묶기
zip()은 Reactor에서 여러 Mono/Flux를 동시에 실행하고, 그 결과를 Tuple에 담아 반환하는 연산자이다.
두 가지의 메뉴를 만들기 위해 여러 API를 찌르고 결과를 반환하는 메서드인 fetchFirstMenu, fetchSecondMenu 가 있다고 가정하자.
두 가지의 비동기 흐름을 하나로 묶어서 결과를 반환하기 위해서 zip()을 사용할 수 있다.
Mono.zip(fetchFirstMenu(), fetchSecondMenu())
.subscribe(tuple -> {
MenuDto menu1 = tuple.getT1();
MenuDto menu2 = tuple.getT2();
});
- 반환 타입은 Tuple2<MenuDto, MenuDto>
- Tupe2<User, Menu> 같은 식이어도 OK.
- 호출 순서와 상관없이 모든 Mono가 완료되었을 때 결과가 묶여 반환된다.
- 비동기 호출을 순차적으로 기다리지 않고, 병렬로 실행 후 모으는 효과가 있음
여러 API나 DB 쿼리 결과를 합쳐서 하나의 응답 객체로 만들어야 할 때 아주 자주 쓰인다.
3. flatMap() — 중첩된 비동기 흐름 평탄화
mashup을 구현하다 보면 flatMap()도 자주 만납니다.
map()과 flatMap()의 차이는 아래 코드를 보면서 이해해 보자.
Mono.just(1)
.map(id -> findUserById(id))
// 결과: Mono<Mono<User>>
Mono.just(1)
.flatMap(id -> findUserById(id))
// 결과: Mono<User>
- map: Mono 안의 값을 변환 → 중첩될 수 있음
- flatMap: Mono 안의 Mono를 펼쳐서 단일 Mono로 반환
이처럼 flatMap은 비동기 값을 '평탄화'한다고 하는데, 쉽게 말하면 Mono로 한번 덧씌우지 않고 값을 반환하는 것을 말한다.
4. Mashup 예시 — 여러 메뉴를 합쳐 DTO 만들기
Mono<TotalMenuResponseDto> totalMenu =
Mono.zip(fetchFirstMenu(), fetchSecondMenu(), fetchThirdMenu(), fetchFourthMenu())
.map(tuple -> new TotalMenuResponseDto(
tuple.getT1(),
tuple.getT2(),
tuple.getT3(),
tuple.getT4()
));
- zip()을 통해 네 개의 비동기 호출을 병렬로 실행한다.
- zip은 내부적으로 모든 호출의 Publisher가 완료될 때까지 기다리므로, 응답이 늦는 API가 하나 있으면 응답 지연의 원인이 된다.
- 반환된 결과는 Tuple4<User, MenuDto, MenuDto, Order>와 같은 식이다.
- 이를 통해 여러 API를 병렬로 실행 후 하나의 ResponseDto로 만들 수 있다.
Mono.zip(fetchFirstMenu(), fetchSecondMenu())
.flatMap(tuple -> createUserOrderDto(tuple.getT1(), tuple.getT2()));
- zip()으로 두 개의 비동기 호출을 병렬로 실행한다.
- 반한된 Tuple2를 가지고 다른 비동기 호출이 필요하다면 flatMap()을 사용한다.
- 순서를 지켜야 한다면 flatMap() 대신 concatMap()을 사용할 수 있다.
'스프링' 카테고리의 다른 글
| CircuitBreaker 2 : Reactive와 함께 (0) | 2025.10.05 |
|---|---|
| DB에서 자주 보이는 utf8mb4_unicode_ci란 무엇일까? (0) | 2025.09.14 |
| CircuitBreaker 1 : 내부 구조 살펴보기 (3) | 2025.08.02 |
| S3에서 파일을 지웠지만 새 파일이 다운로드되지 않는 문제 + 해결 (1) | 2025.07.28 |
| 외부 API 호출, 어떤 비동기 방식을 선택해야 할까? - WebClient, CompletableFuture, @Async (4) | 2025.07.20 |