WebFlux를 쓰는데도 응답이 갑자기 느려질 때가 있다.
대부분 원인은 비슷하다. 논블로킹 경로 안에 블로킹 코드가 섞여 들어간 경우다.

BlockHound는 이 지점을 꽤 직설적으로 잡아준다.

  • 지금 이 스레드에서 이 호출은 블로킹이다
  • 그래서 여기서 실패시킨다

이 글은 BlockHound를 왜 쓰는지, 어떻게 붙이는지, 실패가 나면 어떻게 고치는지를
실전 흐름으로 정리한 가이드다.

1) 왜 BlockHound가 필요한가

Why BlockHound

WebFlux 장점은 이벤트 루프 스레드를 오래 붙잡지 않는 데 있다.
그런데 Thread.sleep, 블로킹 JDBC, 파일 I/O가 끼어들면 장점이 바로 사라진다.

겉으로 CPU가 여유 있어 보여도 p95/p99가 튀는 케이스가 여기서 자주 나온다.

2) BlockHound가 실제로 하는 일

BlockHound detection flow

BlockHound는 런타임에서 블로킹 메서드 호출을 감지한다.
그리고 그 호출이 논블로킹 스레드에서 일어나면 BlockingOperationError를 터뜨린다.

포인트는 성능 최적화 도구라기보다 위반 탐지 도구라는 점이다.

3) 적용: 테스트에서 먼저 켜기

Gradle 의존성

dependencies {
    testImplementation("io.projectreactor.tools:blockhound")
}

JUnit에서 설치

import reactor.blockhound.BlockHound;

class ReactiveTestSupport {
    static {
        BlockHound.install();
    }
}

실무에서는 보통 테스트 공통 베이스에서 한 번만 설치한다.

4) 실패를 재현해보면 바로 이해된다

BlockHound test story

아래 코드는 의도적으로 블로킹을 넣은 예시다.

import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier;

class BlockHoundDemoTest extends ReactiveTestSupport {

    @Test
    void detectsBlockingCallOnNonBlockingThread() {
        Mono<String> pipeline = Mono.fromRunnable(() -> {
                    try {
                        Thread.sleep(30); // blocking
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                })
                .publishOn(Schedulers.parallel())
                .thenReturn("ok");

        StepVerifier.create(pipeline)
                .expectErrorMatches(t -> t.getClass().getName().contains("BlockingOperationError"))
                .verify();
    }
}

이 실패는 오히려 반갑다.
운영에서 늦게 터질 문제를 CI에서 먼저 막아주기 때문이다.

5) 실패가 나면 어떻게 고칠까

Fix patterns

실무에서는 보통 아래 셋 중 하나다.

A. 논블로킹 대체재로 교체 (가장 좋음)

  • RestTemplate -> WebClient
  • JDBC -> R2DBC (가능한 경우)

B. 블로킹 구간 격리

Mono<User> loadUser(String id) {
    return Mono.fromCallable(() -> blockingRepository.findById(id))
            .subscribeOn(Schedulers.boundedElastic());
}

마이그레이션 단계에서는 이 방식이 현실적으로 가장 많이 쓰인다.

C. 불가피한 레거시만 allow-list

BlockHound.install(builder ->
    builder.allowBlockingCallsInside("com.example.LegacyBridge", "readOnce")
);

중요한 점: 이건 문제 해결이 아니라 예외 관리다.
이유, 담당자, 제거 시점을 같이 기록해두는 게 좋다.

6) 롤아웃은 이렇게 가면 안전하다

Rollout strategy

추천 순서:

  1. 로컬 테스트에 먼저 적용
  2. CI 테스트에서 강제
  3. allow-list 최소화 + 정리 일정 관리
  4. 지표(p95/p99)로 실제 효과 확인

이 순서로 가면 팀 충격을 줄이면서 품질 가드를 만들 수 있다.

7) 도입 시 자주 나오는 질문

Q1. BlockHound를 프로덕션에 항상 켜야 하나요?

필수는 아니다. 많은 팀이 테스트/스테이징에서 강하게 사용한다.
목표가 “위반 조기 발견”이기 때문이다.

Q2. 모든 블로킹을 100% 잡아주나요?

아니다. 다만 실무에서 문제를 만드는 주요 패턴을 빠르게 드러내는 데 꽤 유용하다.

Q3. WebFlux면 무조건 BlockHound를 써야 하나요?

필수는 아니지만, 팀 규모가 커지거나 코드가 많아질수록 효과가 커진다.
특히 리뷰에서 놓치기 쉬운 블로킹 호출을 자동으로 잡아준다.


정리하면 BlockHound는 성능 튜닝 도구보다,
반응형 규율을 지키게 해주는 안전장치에 가깝다.

  • 이벤트 루프를 막는 코드 조기 발견
  • 실패를 테스트 단계로 당김
  • 운영 지연 사고 가능성 낮춤

WebFlux를 이미 쓰고 있다면, BlockHound는 투자 대비 효과가 큰 품질 가드다.