Reactive GraphQL 마스터 노트 시리즈 5편. Reactive GraphQL이 WebFlux 환경에 자연스러운 이유, Mono·Flux 반환 자동 처리, R2DBC·Reactive MongoDB 통합, Reactor 합성·flatMap 패턴, BatchMapping의 Reactive 버전, Subscription에서 Backpressure까지.
이 글은 Reactive GraphQL 마스터 노트 시리즈의 다섯 번째 편입니다. 4편(Spring)에서 어노테이션 기반을 봤다면, 이번엔 Reactive 깊은 통합 — Mono/Flux·WebFlux·R2DBC.
Spring GraphQL = Reactive 우선 설계. WebFlux + R2DBC + GraphQL 자연스러운 조합. 처리량·확장성에 강력.
처음 Reactive GraphQL이 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, Mono·Flux 반환 시 어떻게 처리되는지 막연합니다. 둘째, R2DBC와 결합이 막연합니다.
해결법은 한 가지예요. "Reactive = 그냥 Mono/Flux 반환" 한 줄. Spring GraphQL이 자동 처리. 합성·subscribe 신경 X.
Mono/Flux 반환
@QueryMapping
public Mono<User> user(@Argument String id) {
return userService.findByIdReactive(id);
}
@QueryMapping
public Flux<User> users() {
return userService.findAllReactive();
}
@QueryMapping
public Mono<List<User>> usersList() {
return userService.findAllReactive().collectList();
}
여기서 정말 중요한 시험 함정 — Spring GraphQL이 Mono/Flux 자동 처리. block() 호출 X. WebFlux 친화·논블로킹.
R2DBC 통합
public interface UserRepository extends ReactiveCrudRepository<User, String> {
Mono<User> findByEmail(String email);
Flux<User> findByActiveTrue();
}
@Controller
public class UserController {
@Autowired
private UserRepository repo;
@QueryMapping
public Mono<User> user(@Argument String id) {
return repo.findById(id);
}
@QueryMapping
public Flux<User> activeUsers() {
return repo.findByActiveTrue();
}
}
자연스럽게 통합. DB 쿼리 = Reactive 그대로 GraphQL 응답.
flatMap 합성
@QueryMapping
public Mono<UserWithStats> userWithStats(@Argument String id) {
return userRepo.findById(id)
.flatMap(user ->
statsService.getStats(id)
.map(stats -> new UserWithStats(user, stats))
);
}
여러 비동기 호출 합성. Mono·Flux 표준.
SchemaMapping + Reactive
type User {
id: ID!
name: String!
posts: [Post!]!
}
@SchemaMapping(typeName = "User", field = "posts")
public Flux<Post> posts(User user) {
return postRepo.findByAuthorId(user.getId());
}
자연스럽게 Flux 반환.
BatchMapping + Reactive
@BatchMapping(typeName = "User", field = "posts")
public Mono<Map<User, List<Post>>> userPosts(List<User> users) {
List<String> userIds = users.stream().map(User::getId).toList();
return postRepo.findByAuthorIdIn(userIds)
.collectList()
.map(posts -> {
Map<String, List<Post>> byUser = posts.stream()
.collect(Collectors.groupingBy(Post::getAuthorId));
return users.stream()
.collect(Collectors.toMap(
user -> user,
user -> byUser.getOrDefault(user.getId(), List.of())
));
});
}
여기서 시험 함정이 하나 있어요. BatchMapping도 Mono 반환 가능. Reactive 환경에서 자연스러움. 동기 Map 반환과 둘 다 OK.
Subscription Backpressure
@SubscriptionMapping
public Flux<Post> postCreated() {
return sink.asFlux()
.onBackpressureBuffer(1000)
.delayElements(Duration.ofMillis(10)); # 옵션 — 처리량 제어
}
구독자 처리 못 따라가면? Backpressure 자동·버퍼·drop·error 정책.
Mono.zip — 병렬 호출
@QueryMapping
public Mono<DashboardData> dashboard(@AuthenticationPrincipal UserDetails user) {
String userId = user.getUsername();
return Mono.zip(
userRepo.findById(userId),
statsService.getStats(userId),
notificationRepo.findUnread(userId).collectList()
).map(tuple -> new DashboardData(
tuple.getT1(), # User
tuple.getT2(), # Stats
tuple.getT3() # Notifications
));
}
3 비동기 호출 동시. 가장 느린 시간만큼.
Reactive Mongo
public interface PostRepository extends ReactiveMongoRepository<Post, String> {
Flux<Post> findByCategory(String category);
Mono<Post> findBySlug(String slug);
}
R2DBC와 같은 패턴. MongoDB 비동기.
트랜잭션
@MutationMapping
@Transactional
public Mono<Order> placeOrder(@Argument PlaceOrderInput input) {
return orderService.validate(input)
.flatMap(orderRepo::save)
.flatMap(order ->
inventoryService.reserve(order)
.thenReturn(order)
);
}
R2DBC @Transactional = TransactionalOperator 자동. Reactive 트랜잭션.
여기서 정말 중요한 시험 함정 — Reactive 트랜잭션 한계 — JPA처럼 풍부 X. R2DBC·MongoDB 별도 동작. 분산 트랜잭션은 Saga·Outbox.
에러 처리 — Reactive 패턴
@QueryMapping
public Mono<User> user(@Argument String id) {
return userRepo.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException(id)))
.onErrorMap(DataAccessException.class,
e -> new ServiceException("DB error", e));
}
Reactor 표준 에러 처리. @GraphQlExceptionHandler와 결합.
캐싱 — Reactor Cache
@QueryMapping
public Mono<User> user(@Argument String id) {
return cacheManager.get("user-" + id)
.switchIfEmpty(
userRepo.findById(id)
.doOnSuccess(u -> cacheManager.put("user-" + id, u, Duration.ofMinutes(5)))
);
}
또는 CacheMono.lookup:
@QueryMapping
public Mono<User> user(@Argument String id) {
return CacheMono.lookup(this::lookupCache, id)
.onCacheMissResume(() -> userRepo.findById(id))
.andWriteWith(this::writeCache);
}
Reactor Cache Add-ons. 4편 (Reactive Redis 시리즈) 참조.
Context — Reactor Context
@QueryMapping
public Mono<User> me() {
return Mono.deferContextual(ctx -> {
String userId = ctx.get("userId");
return userRepo.findById(userId);
});
}
@Bean
public WebFilter contextFilter() {
return (exchange, chain) -> {
String userId = extractUserId(exchange);
return chain.filter(exchange)
.contextWrite(Context.of("userId", userId));
};
}
Reactor Context로 사용자 정보·trace ID 등 비동기 전파.
부분 데이터 + 에러
{
"data": {
"user": {
"name": "Alice",
"posts": null # 부분 실패
}
},
"errors": [
{
"message": "Failed to load posts",
"path": ["user", "posts"]
}
]
}
GraphQL = 부분 응답 가능. 한 필드 실패 = 다른 필드는 응답.
여기서 시험 함정이 하나 있어요. 부분 실패 처리 — Reactive Resolver에서 onErrorReturn·onErrorResume으로 빈 응답 가능.
WebFlux 환경 + GraphQL
spring:
main:
web-application-type: reactive # WebFlux 강제
graphql:
path: /graphql
websocket:
path: /graphql
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Spring Boot 3.x + WebFlux + GraphQL = 자동 통합.
성능 — Reactive 효과
시나리오: 100 동시 요청, 각 1초 DB 대기
WebFlux + R2DBC + GraphQL:
처리 시간: ~1초 (모두 동시)
메모리: 적음
Spring MVC + JPA + GraphQL:
처리 시간: ~5초 (Pool 200)
메모리: 많음
WebFlux 효과 = Virtual Thread + Spring MVC와 비슷.
운영 권장 패턴
✓ WebFlux + R2DBC + GraphQL (Reactive 일관)
✓ BatchMapping (N+1 자동 해결)
✓ Mono.zip (병렬 호출)
✓ Backpressure (Subscription)
✓ @Transactional (Reactive)
✓ Caching (Reactor Cache)
✓ Reactor Context (사용자·trace)
✓ 부분 응답 + 에러 (onErrorReturn)
✓ Virtual Thread 또는 WebFlux
Reactive vs Virtual Thread
Reactive (WebFlux):
- 명시적 Mono/Flux
- 백프레셔 표준
- 학습 곡선 ↑
- 합성 강력
Virtual Thread (Spring MVC + Java 21):
- 동기 코드 그대로
- 학습 곡선 ↓
- 디버깅 쉬움
- 일부 백프레셔 X
여기서 정말 중요한 시험 함정 — GraphQL은 둘 다 OK. Reactive 권장 (Subscription 자연스러움) but Virtual Thread도 가능. 팀 친화도로 결정.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 5편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- Mono/Flux 반환만 — Spring GraphQL 자동 처리
- block() 호출 X
- R2DBC = Reactive DB 통합 자연스러움
- ReactiveCrudRepository → Mono·Flux
- flatMap 합성 = 여러 비동기 호출 연결
- Mono.zip = 병렬 호출 (가장 느린 시간만큼)
- BatchMapping + Mono = N+1 자동 해결 (Reactive)
- Subscription Backpressure —
onBackpressureBuffer·delayElements - Reactive 트랜잭션 한계 — Saga·Outbox 필요
- 에러 처리 —
switchIfEmpty·onErrorMap·onErrorResume - 캐싱 — Reactor Cache Add-ons (
CacheMono.lookup) - Reactor Context = 사용자 정보·trace ID 전파
- WebFilter로 컨텍스트 주입
- 부분 응답 + 에러 = 한 필드 실패 시 다른 응답
- WebFlux + R2DBC + GraphQL = 자연 통합
- Reactive 효과 — I/O Bound 처리량·메모리
- Reactive vs Virtual Thread — 둘 다 OK
- Reactive = Subscription 자연스러움
- Virtual Thread = 동기 코드·학습 곡선 ↓
시리즈 다른 편
- 1편 — 기본 개념·Schema
- 2편 — Query·Mutation·Variables
- 3편 — Subscription·실시간 구독
- 4편 — Spring for GraphQL
- 5편 — Reactive GraphQL (현재 글)
- 6편 — Security·Testing
- 7편 — 고급 (DataLoader·Federation·운영)
공식 문서: Reactor / R2DBC 에서 더 깊이.
다음 글(6편)에서는 Security·Testing — 인증·인가·필드 보안·통합 테스트·@GraphQlTest까지 풀어 갑니다.