본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.


강의 요약

오늘 강의에서는 리액티브 프로그래밍에서 non-blocking 코드의 품질을 검증하는 방법을 다루었다. 리액티브 시스템에서 의도치 않은 blocking 호출이 발생하면 성능 저하와 스레드 고갈 문제로 이어질 수 있다. 강의에서 소개된 BlockHound에 대해 알아보자.


BlockHound 개요

BlockHound는 Project Reactor 팀에서 개발한 Java Agent로, non-blocking 스레드에서 발생하는 blocking 호출을 런타임에 감지한다. WebFlux나 Reactor 기반 애플리케이션에서 개발자가 인지하지 못한 blocking 코드가 섞여 있으면 리액티브 시스템의 이점을 상쇄시킬 수 있는데, BlockHound는 이러한 문제를 조기에 발견할 수 있게 도와준다.

 

동작 원리

BlockHound는 JVM 클래스를 투명하게 계측하고, "논블로킹 작업 전용"으로 표시된 스레드에서 수행되는 블로킹 호출을 가로챈다. 이러한 상황이 발생할 경우 BlockingOperationError 에러를 발생시킨다.

// 설치 및 기본 사용
BlockHound.install();

Mono.delay(Duration.ofSeconds(1))
    .doOnNext(it -> {
        Thread.sleep(10); // BlockingOperationError 발생!
    })
    .block();

 

reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
    at java.base/java.lang.Thread.sleep(Native Method)
    at com.example.Example.lambda$exampleTest$0(Example.java:16)

 

주요 특징

항목 설명
감지 대상 Thread.sleep, Socket I/O, synchronized 블록 등 JVM의 blocking 메서드
감지 방식 바이트코드 인스트루먼테이션 (native 메서드 포함)
지원 프레임워크 Project Reactor 3.2+, RxJava 2 (SPI로 확장 가능)
오버헤드 낮음 (테스트/스테이징 환경 권장, 프로덕션도 가능)

 

JDK 버전별 설정

JDK 13 이상에서는 native 메서드 재정의가 제한되어 별도의 JVM 인자가 필요하다.

-XX:+AllowRedefinitionToAddDeleteMethods

 

gradle

tasks.withType<Test>().all {
    if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) {
        jvmArgs("-XX:+AllowRedefinitionToAddDeleteMethods")
    }
}

 

 

BlockHound 커스터마이징

실제 애플리케이션에서는 라이브러리나 프레임워크 내부에서 불가피하게 발생하는 blocking 호출이 있을 수 있다. BlockHound는 이러한 경우를 위한 화이트리스트 API를 제공한다.

 

allowBlockingCallsInside

특정 메서드 내부에서의 blocking 호출을 허용한다. 해당 메서드 및 콜스택 하위에서 발생하는 blocking은 감지되지 않는다.

BlockHound.install(builder -> builder
    .allowBlockingCallsInside(
        "ch.qos.logback.classic.Logger",
        "callAppenders"
    )
);

 

disallowBlockingCallsInside

반대로, 특정 메서드 내부에서 blocking을 명시적으로 금지한다. allow와 조합하여 세밀한 제어가 가능하다.

BlockHound.install(builder -> builder
    .allowBlockingCallsInside(NonBlockingClass.class.getName(), "outer")
    .disallowBlockingCallsInside(NonBlockingClass.class.getName(), "inner")
);

  • 위 설정에서 outer() 메서드 내부는 blocking이 허용되지만, outer()가 호출하는 inner() 내부에서는 다시 금지된다.

 

blockingMethodCallback

기본 동작인 예외 발생 대신 커스텀 콜백을 설정할 수 있다. 디버깅 단계에서 예외 없이 로그만 남기고 싶을 때 유용하다.

BlockHound.install(builder -> builder
    .blockingMethodCallback(method -> {
        new Exception(method.toString()).printStackTrace();
    })
);

 

 

화이트리스트 전략

화이트리스트 설정 시 가장 중요한 원칙은 least common denominator를 선택하는 것이다. 범용적인 메서드를 화이트리스트에 추가하면 실제 문제가 되는 blocking 호출을 놓칠 수 있다.

 

잘못된 예시

NonBlockingThread#run → OperationRunnable#run → TaskRunner#run → TaskExecutor#takeTask → blocking 발생

  • 위 콜스택에서 Thread#run이나 Runnable#run을 화이트리스트하면 해당 스레드의 모든 blocking이 무시된다.
  • LockSupport#parkNanos 같은 저수준 메서드도 다른 곳에 영향을 줄 수 있다.

 

권장 접근법

콜스택을 분석하여 blocking이 발생하는 가장 구체적인 메서드를 찾는다. 위 예시에서는 TaskExecutor#takeTask가 적절한 화이트리스트 대상이다. 단, 해당 메서드가 public API라면 다른 곳에서도 호출될 수 있으므로 주의가 필요하다.

화이트리스트 대상 범위 적합성
Thread#run 너무 광범위 부적절
LockSupport#parkNanos 저수준, 다른 곳에 영향 부적절
TaskExecutor#takeTask 구체적, 영향 범위 제한적 권장

 

도입 시 고려사항

BlockHound는 리액티브 코드의 품질 보증에 효과적인 도구이지만, 모든 상황에 적합한 것은 아니다. 설치 후에는 반드시 의도적인 blocking 테스트를 작성하여 BlockHound가 정상적으로 동작하는지 검증해야 한다. Agent가 제대로 설치되지 않으면 false negative가 발생할 수 있다.

장점

  • 런타임 감지로 복잡한 코드 경로의 blocking도 발견 가능
  • 테스트 단계에서 문제를 조기 발견
  • CI/CD 파이프라인에 통합하여 blocking 코드의 병합 방지

한계

  • 정적 분석이 아닌 런타임 검사이므로 실행되지 않은 경로는 감지 불가
  • JVM 버전에 따른 설정 필요
  • reactive 스레드에서 호출을 차단하는 플래그만 표시한다.
    • 전용 스레드(예: Schedulers.boundedElastic()) 내의 차단 코드는 감지되지 않을 수 있다.
  • 개발 및 디버깅 용도로 설계된 것이므로 프로덕션 코드에 사용 하지 말아야 한다.

 

참고 출처

 

 

시작 시간
종료 시간
학습 인증 - 디지털 필기
수강 인증

 

https://fastcampus.info/4oKQD6b

+ Recent posts