Post

동기·비동기·Reactive 설계 가이드

동기/비동기와 블로킹/논블로킹을 구분하고 WebFlux·WebClient로 대기 시간과 백프레셔를 다루는 방법

동기·비동기·Reactive 설계 가이드

요약 (5–7줄)

Reactive는 “빠른 처리”가 아니라 대기 시간을 다루는 방식의 전환이다.
동기/비동기, 블로킹/논블로킹은 서로 다른 축이며 혼동되는 순간 설계가 망가진다.
WebClient의 핵심 가치는 스레드 절약이 아니라 대기 중 스레드 점유 제거와 백프레셔다.
Reactive는 트래픽 폭주·외부 I/O 지연에서 유리하지만, 복잡도와 디버깅 비용을 동반한다.
Reactive 시스템은 “전부 Reactive”가 아니라 경계면만 Reactive여도 충분하다.
실무 실패 원인은 성능이 아니라 컨텍스트 전파·로깅·트레이싱 붕괴다.


1. 정의

  • 동기(Synchronous): 호출자가 응답을 기다림
  • 비동기(Asynchronous): 호출자가 응답을 기다리지 않음
  • 블로킹(Blocking): 스레드가 대기 동안 점유됨
  • 논블로킹(Non-blocking): 대기 동안 스레드 반환

이 네 개는 서로 다른 개념이며, 하나로 묶어 이해하면 설계 판단이 왜곡된다.


2. 직관

문제는 “연산 속도”가 아니다.
문제는 기다리는 동안 무엇을 낭비하느냐다.

  • 블로킹: 스레드를 낭비
  • 논블로킹: 상태 관리 복잡도를 소비

Reactive는 CPU를 빠르게 만드는 기술이 아니라
스레드 낭비를 없애는 선택지다.


3. 2×2 매트릭스 정리

 블로킹논블로킹
동기전통적 MVC (RestTemplate)거의 없음
비동기@Async + blocking I/OReactive (WebFlux)

핵심 포인트:

  • 비동기 ≠ 논블로킹
  • WebFlux는 “비동기 + 논블로킹” 조합

4. 작동원리: Reactive의 실제 동작

1) 요청 수신
2) I/O 발생 시 스레드 반환
3) 이벤트 완료 시 콜백/시그널
4) 다시 스레드 할당 후 처리 재개

이 구조에서 중요한 것은:

  • 스레드 수 < 동시 요청 수 가능
  • 대신 흐름 제어(backpressure)가 필수

5. WebClient가 중요한 이유

5.1 스레드 점유 제거

  • RestTemplate: 외부 API 응답 대기 동안 스레드 고정
  • WebClient: 대기 중 스레드 반환

5.2 대기 시간 분리

  • 느린 외부 API가 내부 처리량을 잠식하지 않음

5.3 백프레셔

  • 처리 가능한 만큼만 소비
  • 폭주 트래픽 시 시스템 붕괴 방지

WebClient의 가치는 “비동기 호출”이 아니라
대기 시간에 대한 제어권이다.


6. Reactive가 유리한 경우 4가지

1) 외부 API 호출 비중이 높음
2) 트래픽 스파이크가 잦음
3) I/O 지연 시간이 길고 불안정
4) 동일 리소스를 많은 요청이 공유

7. Reactive가 불리한 경우 4가지

1) 디버깅 난이도 증가
2) 팀의 Reactive 이해도 부족
3) 블로킹 레거시 라이브러리 다수
4) 단순 CRUD 중심 서비스

Reactive는 “성능 만능 해법”이 아니라
특정 부하 패턴에만 맞는 도구다.


8. “Reactive면 전체가 Reactive여야 하는가?”

정답: 아니다.

경계면 전략

  • 외부 I/O 경계: Reactive
  • 내부 도메인 로직: 동기
  • DB: 상황에 따라 분리

중요한 것은 블로킹이 시스템 내부로 전파되지 않게 막는 것이지
모든 코드를 Reactive로 만드는 게 아니다.


9. 실무 함정: 컨텍스트 전파 문제

9.1 MDC/로깅

  • 스레드 로컬 기반 로깅 붕괴
  • 해결: Reactor Context 사용

9.2 트레이싱

  • 요청 흐름 단절
  • 해결: 명시적 컨텍스트 전달

9.3 보안 컨텍스트

  • 인증 정보 손실
  • 해결: 경계면에서 캡처

Reactive 실패의 70%는
성능이 아니라 관측성 붕괴다.


10. 트레이드오프

  • 장점: 높은 동시성, 스레드 효율, 폭주 내성
  • 비용: 복잡성, 디버깅 난이도, 학습 비용

Reactive는 “싸게 빠르게”가 아니라
비용을 바꿔 지불하는 선택이다.


11. 최소 예시

11.1 Toy 예시: 외부 API 3개 호출

  • 동기:
    • API A → 대기
    • API B → 대기
    • API C → 대기
    • 총 대기 시간 = 합
  • 비동기/Reactive:
    • A/B/C 동시 요청
    • 가장 느린 응답 기준
    • 스레드 점유 최소화

11.2 실무 예시: 검색 쿼리 서비스

  • 상황: 이벤트성 트래픽 폭주
  • 전략:
    • WebClient 기반 외부 검색 호출
    • 타임아웃 + 백프레셔
    • 내부 스레드 고갈 방지

결과:

  • 일부 요청 지연은 허용
  • 전체 장애는 방지

12. 실무 함정 → 해결 패턴

함정결과해결 패턴
Reactive + Blocking 혼합스레드 고갈경계 차단
MDC 의존 로깅로그 단절Reactor Context
무제한 병렬폭주백프레셔

13. 오해/실수 3개 + 교정

1) “Reactive는 무조건 빠르다” → 대기 처리에 유리
2) “전체를 Reactive로 바꿔야 한다” → 경계면만으로 충분
3) “WebClient = 비동기 RestTemplate” → 스레드 모델이 다름


14. 판단 기준

사용해야 할 때

  • 외부 I/O 병목이 명확
  • 트래픽 변동성이 큼
  • 장애 전파를 막아야 함

쓰지 말아야 할 때

  • 단순 내부 CRUD
  • 낮은 동시성
  • 팀 학습 여력 부족

15. 재학습 체크리스트 (10–14)

  • 동기/비동기/블로킹 차이를 구분하는가?
  • 외부 I/O가 병목인가?
  • 스레드 고갈 시나리오가 있는가?
  • WebClient 사용 이유가 명확한가?
  • 블로킹 코드가 경계를 넘는가?
  • 백프레셔 전략이 있는가?
  • 타임아웃이 설정돼 있는가?
  • MDC/트레이싱 대책이 있는가?
  • 디버깅 전략이 있는가?
  • Reactive 도입 목적이 합의됐는가?
  • 성능 테스트 시나리오가 있는가?
  • 실패 시 롤백 전략이 있는가?

This post is licensed under CC BY 4.0 by the author.