본문 바로가기
Lecture

webflux 리액티브 프록그래밍에서 블록킹을 디버깅하는 도구 blockhound 간단 사용법

by Renechoi 2023. 10. 21.

blockhound ?

webflux 등의 리액티브 프로그래밍을 쓰는 이유가 non-blocking 환경에서 비동기적 처리를 하기 위함인데 blocking이 발생하면 하나의 발생으로도 전체 성능에 영향을 끼칠 수 있다.

비동기적 프로그래밍의 어려운 점은 테스트하기가 동기에 비해 어렵다는 점이다.

blockhound는 운영 환경에서 사용하긴 힘들지만 개발 환경에서 blocking 을 검출할 수 있게 도와주는 도구다.

간단한 의존성 추가와 코드 몇 줄만으롣 검출이 가능하기 때문에 쉽게 사용할 수 있다.

의존성

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.8'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

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

dependencies {
    implementation 'io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64'

    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
    implementation "io.asyncer:r2dbc-mysql:1.0.2"

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    testImplementation 'io.projectreactor.tools:blockhound:1.0.8.RELEASE'
}

tasks.named('test') {
    useJUnitPlatform()
}

13 버전 이후에는 호환성을 위해 AllowRedefinitionToAddDeleteMethods 를 vm 옵션에 추가해주어야 한다.

사용하기

다음과 같이 어플리케이션 시작 부분에 install 코드를 추가한다.

@SpringBootApplication
public class Webflux1Application implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(Webflux1Application.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
    }
}

자동으로 검출해준다.

테스트 환경에서 사용하려면 어떻게 할까


@WebFluxTest(UserController.class)
@AutoConfigureWebTestClient
class UserControllerTest {
    static {
        BlockHound.install();
    }
    @Autowired
    private WebTestClient webTestClient;

    @MockBean
    private UserService userService;

    @MockBean
    private PostServiceV2 postServiceV2;

    @Test
    void blockHoundTest() {
        StepVerifier.create(Mono.delay(Duration.ofSeconds(1))
                .doOnNext(it -> {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }))
                .verifyComplete();
    }
    @Test
    void createUser() {
        when(userService.create("greg", "greg@fastcampus.co.kr")).thenReturn(
                Mono.just(new User(1L, "greg", "greg@fastcampus.co.kr", LocalDateTime.now(), LocalDateTime.now()))
        );

        webTestClient.post().uri("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(new UserCreateRequest("greg", "greg@fastcampus.co.kr"))
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(UserResponse.class)
                .value(res -> {
                    assertEquals("greg", res.getName());
                    assertEquals("greg@fastcampus.co.kr", res.getEmail());
                });
    }

    @Test
    void findAllUsers() {
        when(userService.findAll()).thenReturn(
                Flux.just(
                        new User(1L, "greg", "greg@fastcampus.co.kr", LocalDateTime.now(), LocalDateTime.now()),
                        new User(2L, "greg", "greg@fastcampus.co.kr", LocalDateTime.now(), LocalDateTime.now()),
                        new User(3L, "greg", "greg@fastcampus.co.kr", LocalDateTime.now(), LocalDateTime.now())
                ));

        webTestClient.get().uri("/users")
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBodyList(UserResponse.class)
                .hasSize(3);
    }

    @Test
    void findUser() {
        when(userService.findById(1L)).thenReturn(
                Mono.just(new User(1L, "greg", "greg@fastcampus.co.kr", LocalDateTime.now(), LocalDateTime.now())
                ));

        webTestClient.get().uri("/users/1")
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(UserResponse.class)
                .value(res -> {
                    assertEquals("greg", res.getName());
                    assertEquals("greg@fastcampus.co.kr", res.getEmail());
                });
    }

    @Test
    void notFoundUser() {
        when(userService.findById(1L)).thenReturn(Mono.empty());

        webTestClient.get().uri("/users/1")
                .exchange()
                .expectStatus().is4xxClientError();
    }

    @Test
    void deleteUser() {
        when(userService.deleteById(1L)).thenReturn(Mono.empty());

        webTestClient.delete().uri("/users/1")
                .exchange()
                .expectStatus().is2xxSuccessful();
    }

    @Test
    void updateUser() {
        when(userService.update(1L, "greg1", "greg1@fastcampus.co.kr")).thenReturn(
                Mono.just(new User(1L, "greg1", "greg1@fastcampus.co.kr", LocalDateTime.now(), LocalDateTime.now()))
        );

        webTestClient.put().uri("/users/1")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(new UserCreateRequest("greg1", "greg1@fastcampus.co.kr"))
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(UserResponse.class)
                .value(res -> {
                    assertEquals("greg1", res.getName());
                    assertEquals("greg1@fastcampus.co.kr", res.getEmail());
                });
    }
}

test 코드에서 사용하는 StepVerifier에 대해서 create 하고 doOneNext 함수 내부를 작성하면 된다.

테스트를 돌렸을 때 블락킹이 발견되면 검출해준다.



reference:

반응형