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 실습 (0) | 2024.09.26 |