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:
- fastcampus 시그니처 백엔드 path 초격차 패키지 course 7
- https://github.com/reactor/BlockHound
반응형
'Lecture' 카테고리의 다른 글
ChatGpt를 이용해 유튜브 요약하기 - 프롬프트 엔지니어링이란? 생성형 AI에게 최적의 답을 얻는 노하우 대방출! [안될과학X삼성SDS 2탄] (1) | 2024.01.21 |
---|---|
대용량 트래픽을 대비한 Spring Webflux와 Reactive Redis를 이용한 접속자 대기열 시스템 (0) | 2023.10.22 |
Jmeter를 이용한 Spring MVC Vs. Webflux 성능 비교 (0) | 2023.10.21 |
Spring webflux - R2DBC, Redis (0) | 2023.10.21 |
객체 필드 중복을 해결하는 다양한 방법들과 객체지향 패러다임을 이용한 순수 코드 방식의 해결법(feat. DDD의 Aggregate와의 연관성) (0) | 2023.07.22 |