BlockHound로 비동기 코드에서 블로킹 호출 감지하기
Java에서 비동기 프로그래밍은 높은 성능과 확장성을 제공하는 강력한 방법입니다. 특히, Spring WebFlux와 Project Reactor 같은 비동기 프레임워크들은 적은 리소스로도 수많은 동시 요청을 처리할 수 있습니다. 하지만 비동기 코드에서 실수로 블로킹 호출을 사용하면, 이러한 비동기 프로그래밍의 장점을 잃어버릴 수 있으며 심각한 성능 저하가 발생할 수 있습니다.
이 문제를 예방하고, 비동기 코드에서 블로킹 호출을 감지하는 데 도움을 주는 도구가 바로 BlockHound입니다.
BlockHound란?
BlockHound는 Java 애플리케이션에서 블로킹 호출을 탐지하는 Java Agent입니다. 특히 비동기 프로그래밍 환경에서 실수로 블로킹 메서드(예: Thread.sleep(), 파일 읽기/쓰기, 블로킹 I/O)를 사용하는 경우를 찾아내고, 이를 실시간으로 감지하여 예외를 던지거나 로그로 남깁니다. 이를 통해 개발자는 문제를 빠르게 인지하고 수정할 수 있습니다.
비동기 환경에서 블로킹 호출이 실행되면, 스레드가 대기 상태에 들어가며 이는 성능 저하를 일으킵니다. BlockHound는 이러한 상황을 방지하여 비동기 애플리케이션이 고성능을 유지할 수 있도록 도와줍니다.
BlockHound 주요 기능
- 비동기 코드에서 블로킹 호출 탐지: BlockHound는 비동기 논블로킹 코드에서 실수로 사용된 블로킹 호출을 감지합니다. 예를 들어, I/O 작업이나
Thread.sleep()같은 메서드 호출을 감지하고, 예외를 발생시킵니다. - 커스터마이징 가능: BlockHound는 특정 메서드나 라이브러리에서 발생하는 블로킹 호출을 허용하거나 무시하도록 설정할 수 있습니다. 이를 통해 개발자는 애플리케이션에 맞는 최적의 설정을 적용할 수 있습니다.
- Reactor, RxJava와의 통합: BlockHound는 Project Reactor, RxJava 같은 비동기 프레임워크와 쉽게 통합되어 사용할 수 있습니다. 비동기 논블로킹 환경을 지향하는 대부분의 비동기 프레임워크와 호환됩니다.
- 실시간 블로킹 호출 감지: 애플리케이션 실행 중 블로킹 호출이 발생하면 BlockHound는 실시간으로 이를 감지하고 로그를 남기거나 예외를 발생시켜 개발자가 즉각적인 조치를 취할 수 있게 합니다.
BlockHound 설치 방법
BlockHound는 간단한 설정으로 애플리케이션에 적용할 수 있습니다. Gradle 프로젝트를 기준으로 BlockHound를 설정하는 방법을 살펴보겠습니다.
1. Gradle 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.projectreactor.tools:blockhound:1.0.9.RELEASE'
testImplementation 'io.projectreactor.tools:blockhound:1.0.9.RELEASE'
}
2. BlockHound 설치
애플리케이션 실행 시 BlockHound를 설치하는 코드를 추가해야 합니다. 일반적으로 애플리케이션 시작 지점인 main 메서드에서 설치합니다.
import reactor.blockhound.BlockHound;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebfluxApplication {
public static void main(String[] args) {
// BlockHound 설치
BlockHound.install();
SpringApplication.run(WebfluxApplication.class, args);
}
}
BlockHound.install()을 호출하면 애플리케이션이 실행되는 동안 비동기 환경에서 블로킹 호출이 감지됩니다.
3. JDK 13+에서의 추가 설정
JDK 13 이상을 사용하는 경우 -XX:+AllowRedefinitionToAddDeleteMethods JVM 옵션을 추가해야 합니다. 예외가 발생하면 이 플래그를 추가해 문제를 해결할 수 있습니다.
Exception in thread "main" java.lang.IllegalStateException: The instrumentation have failed.
It looks like you're running on JDK 13+.
You need to add '-XX:+AllowRedefinitionToAddDeleteMethods' JVM flag.
IntelliJ 또는 다른 IDE에서 해당 JVM 플래그를 추가해 문제를 해결할 수 있습니다.
간단한 BlockHound 테스트
이제 BlockHound를 사용해 실제로 블로킹 호출을 탐지하는 코드를 작성해보겠습니다. Spring Boot 애플리케이션의 ApplicationRunner 인터페이스를 구현하여 테스트해볼 수 있습니다.
테스트 코드
package com.example.blockhound;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import reactor.blockhound.BlockHound;
import reactor.core.publisher.Mono;
import java.time.Duration;
@SpringBootApplication
public class BlockhoundApplication implements ApplicationRunner {
public static void main(String[] args) {
// BlockHound 설치
BlockHound.install();
SpringApplication.run(BlockhoundApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
Mono.delay(Duration.ofSeconds(1))
.doOnNext(it -> {
try {
// 블로킹 호출: Thread.sleep() 사용
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})
.subscribe();
}
}
실행 결과
서버를 실행하면 BlockHound가 Thread.sleep() 호출을 감지하고 다음과 같은 예외 로그를 출력합니다.
reactor.core.Exceptions$ErrorCallbackNotImplemented: reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
Caused by: reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
at java.base/java.lang.Thread.sleep(Thread.java) ~[na:na]
at com.example.blockhound.BlockhoundApplication.lambda$run$0(BlockhoundApplication.java:25) ~[main/:na]
이 로그는 비동기 코드에서 Thread.sleep() 같은 블로킹 호출이 사용되었음을 알리고, 이를 수정할 필요가 있음을 보여줍니다.
BlockHound의 커스터마이징
BlockHound는 모든 블로킹 호출을 무조건 감지하지만, 특정 블로킹 호출을 허용해야 하는 상황이 있을 수 있습니다. 이럴 때는 BlockHound의 allowBlockingCallsInside() 메서드를 사용해 특정 메서드나 클래스에서의 블로킹 호출을 허용할 수 있습니다.
특정 메서드에서 블로킹 호출 허용
BlockHound.builder()
.allowBlockingCallsInside("com.example.MyClass", "myMethod")
.install();
위 코드는 com.example.MyClass의 myMethod()에서 발생하는 블로킹 호출을 허용하도록 설정합니다.
특정 라이브러리에서 블로킹 호출 허용
BlockHound.builder()
.allowBlockingCallsInside("org.springframework.util.ReflectionUtils", "doWithFields")
.install();
이 설정은 Spring Framework의 ReflectionUtils.doWithFields 메서드에서 발생하는 블로킹 호출을 허용합니다.
BlockHound와 Reactor 통합
BlockHound는 Project Reactor와 같은 비동기 프레임워크와 자연스럽게 통합되어 동작합니다. 예를 들어, Reactor에서 Mono 또는 Flux를 사용하는 코드에서 블로킹 호출이 발생하면 BlockHound는 이를 감지하고 예외를 던집니다.
Mono.just("Hello")
.map(value -> {
Thread.sleep(1000); // 블로킹 호출
return value;
})
.block();
이 코드는 Thread.sleep() 블로킹 호출을 감지하여 예외를 발생시킵니다.
BlockHound 테스트 코드 작성
BlockHound는 테스트 환경에서 블로킹 호출이 발생하지 않도록 보장할 수 있는 유용한 도구입니다. JUnit을 사용한 간단한 테스트 코드를 작성하여 BlockHound의 동작을 확인해볼 수 있습니다.
package com.example.blockhound;
import org.junit.jupiter.api.Test;
import reactor.blockhound.BlockHound;
import reactor.blockhound.BlockingOperationError;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
public class BlockHoundTest {
static{
BlockHound.install(); // Activate BlockHound
}
@Test
void blockHoundTest(){
StepVerifier.create(Mono.delay(Duration.ofSeconds(1))
.doOnNext( it -> {
try{
Thread.sleep(100); // This should trigger BlockHound
}catch(InterruptedException e){
throw new RuntimeException(e);
}
})
).expectErrorMatches(throwable -> throwable instanceof BlockingOperationError)
.verify();
}
@Test
void testBlockHoundDetectsBlockingCall() {
// 블로킹 호출을 비동기적으로 실행
Mono<String> mono = Mono.delay(Duration.ofSeconds(1))
.map(it -> {
try {
Thread.sleep(10); // 블로킹 호출
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "Hello, World!";
});
// 블로킹 호출을 감지하고 예외를 던지는지 테스트
StepVerifier.create(mono)
.expectErrorMatches(throwable -> throwable instanceof reactor.blockhound.BlockingOperationError)
.verify();
}
}
위 테스트 코드는 Thread.sleep()과 같은 블로킹 호출
을 포함한 Mono를 실행하고, BlockHound가 이를 감지해 예외를 던지는지 확인합니다. StepVerifier는 Reactor에서 비동기
코드 테스트를 지원하는 도구로, 비동기 흐름에서 발생하는 예외를 검증하는 데 유용합니다.

결론
BlockHound는 비동기 프로그래밍에서 흔히 발생하는 블로킹 호출 문제를 실시간으로 탐지하고 예외를 발생시켜 개발자가 빠르게 인지하고 수정할 수 있도록 도와주는 도구입니다. 이를 통해 비동기 애플리케이션의 성능을 최적화하고, 블로킹 호출로 인한 성능 저하를 방지할 수 있습니다.
비동기 프로그래밍을 할 때, 특히 Spring WebFlux, Project Reactor 같은 프레임워크를 사용할 때, BlockHound를 적극 활용하여 코드 내에서 불필요한 블로킹 호출을 탐지하고 최적의 성능을 유지하도록 하세요.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
| 접속자 대기열 시스템 #3- 셋업 (2) | 2024.10.10 |
|---|---|
| 접속자 대기열 시스템 #4- 대기열 등록 API 개발 (6) | 2024.10.09 |
| Spring MVC와 Spring Webflux 성능비교 (0) | 2024.10.08 |
| Reactive Redis (1) | 2024.10.07 |
| webflux - R2DBC 실습 (2) | 2024.09.26 |