요새 날이 넘 춥네요..

 

안그래도 추운 날씨에 밖에 나가기가 무서워진 요즘..

 

Handle이 고장한 8톤 트럭,,도 아닌데, throws를 선언해도 처리되지 않았다며 우기는 Controller와 화해한 이야기 풉니다,,

 

 

1. 문제의 코드

@GetMapping("/touch")
Mono<?> touch(@RequestParam(name = "queue", defaultValue = "default") String queue,
      @RequestParam(name = "user_id") Long userId,
      ServerWebExchange exchange) throws NoSuchAlgorithmException {
    return Mono.defer(() -> userQueueService.generateToken(queue, userId))
        .map(token -> {
          exchange.getResponse().addCookie(
              ResponseCookie.from("user-queue-%s-token".formatted(queue), token)
                  .maxAge(Duration.ofSeconds(300))
                  .path("/")
                  .build()
          );

          return token;
        });
  }
  
public Mono<String> generateToken(final String queue, final Long userId) throws NoSuchAlgorithmException{
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    String input = "user-queue-%s-%d".formatted(queue, userId);
    byte[] encodedHash = digest.digest(input.getBytes(StandardCharsets.UTF_8));

    StringBuilder hexString = new StringBuilder();
    for (byte b : encodedHash) {
      hexString.append(String.format("%02x", b));
    }
    
    return Mono.just(hexString.toString());
}

 

 

2. 문제의 상황

generateToken 메서드는 SHA-256 암호화 알고리즘을 이용해서 토큰값을 생성합니다.

MessageDigest 클래스의 getInstance를 사용하려면 Checked Exception인 NoSuchAlgorithmException에 대한 처리를 해줘야 합니다.

 

generateToken()를 호출하는 "/touch" API 또한 NoSuchAlgorithmException이 발생한다고 throws를 적으면 NoSuchAlgorithmException이 Unhandled exception이라며 오류가 발생합니다^^!

 

 

3. 문제의 원인

Spring WebFlux에서 Mono 또는 Flux 내부에서 발생하는 Checked 예외는 반드시 Mono.error() 또는 onErrorResume() 등을 통해 처리해야 합니다.

⭐ 즉, 비동기 코드(WebFlux)에서는 throws가 먹히지 않고 Mono.error(e)를 사용해야 합니다.

 

 

4. 해결 방법

Mono.defer() 내부에서 예외를 처리
@GetMapping("/touch")
Mono<?> touch(@RequestParam(name = "queue", defaultValue = "default") String queue,
              @RequestParam(name = "user_id") Long userId,
              ServerWebExchange exchange) {
    return Mono.defer(() -> {
        try {
            return userQueueService.generateToken(queue, userId);
        } catch (NoSuchAlgorithmException e) {
            return Mono.error(e);
        }
    }).map(token -> {
        exchange.getResponse().addCookie(
            ResponseCookie.from("user-queue-%s-token".formatted(queue), token)
                .maxAge(Duration.ofSeconds(300))
                .path("/")
                .build()
        );
        
        return token;
    });
}

 

Checked 예외를 Unchecked 예외로 변경
public Mono<String> generateToken(final String queue, final Long userId) {
    MessageDigest digest = null;
    try {
        digest = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }

    String input = "user-queue-%s-%d".formatted(queue, userId);
    byte[] encodedHash = digest.digest(input.getBytes(StandardCharsets.UTF_8));

    StringBuilder hexString = new StringBuilder();
    for (byte b : encodedHash) {
        hexString.append(String.format("%02x", b));
    }
    return Mono.just(hexString.toString());
}

 

 

🤗 정리

예외 유형 Mono에서 처리 방식
Checked Exception (NoSuchAlgorithmException) Mono.error(e)를 써야 Mono 체인에서 처리 가능
Unchecked Exception (RuntimeException)
따로 감싸지 않아도 Mono 체인에서 자동으로 예외 감지

 

 

🚀 결론

예외를 클라이언트에게 전달할 필요가 있을 경우 → Mono.error(e)로 감싸서 처리하는 것이 좋음
500 에러로 내보내면 충분할 경우 → try-catch 후 RuntimeException 던지는 것이 더 간결하고 좋음

 

 

 

📑

참고 자료

Chat GPT

https://fastcampus.co.kr/dev_online_traffic_data

 

9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 완벽 마스터하기 | 패스트캠퍼스

실무에서 자주 일어나는 대용량 트래픽 & 데이터 처리 업무를 한번에 마스터할 수 있도록 모든 것을 담았습니다. 대기업 & 빅테크 현업 강사진 8인과 함께 하는 고퀄리티 현업 대비형 강의! 타사

fastcampus.co.kr

 

 

Chill한 루피를 데려왔습니다.

이유는 없고, 요즘 유행인 것 같길래 우선 넣어봤습니다^^!

 

평소보다 이미지가 길어서 깔끔하지 않은 모양새로 글을 시작하게 됬지만, Chill한 루피와 함께라면 이 정도는 Chill하게 넘어갈 수 있지 않을까,, 생각합니다,,

 

오늘은 간단하게 테스트 코드 작성 시, Embedded Redis를 사용하는 방법을 정리해 보고자 합니다.

 

1. Spring Boot + Redis 환경에서 테스트 코드 작성 시 Embedded Redis를 사용하는 이유

실제 Redis 서버에 의존하지 않고 테스트를 실행할 수 있기 때문입니다. 즉, Redis를 설치하지 않아도 테스트 가능합니다.
Embedded Redis는 테스트가 끝나면 자동으로 종료되므로, 테스트 데이터 정리를 신경 쓸 필요 없습니다.
클라이언트-서버 통신이 필요없어서 네트워크 I/O 오버헤드가 발생하지 않으므로 실제 Redis 서버보다 좀 더 빠릅니다.

 

* 네트워크 I/O 오버헤드: 데이터를 전송할 때 발생하는 추가적인 시간/비용/리소스 소비

 

 

2. Dependancy 추가

build.gradle
dependencies {
	testImplementation 'com.github.codemonstur:embedded-redis:1.4.3' // test 전용 embedded redis 추가
}

 

 

3. Configuration 파일 작성

EmbeddedRedis.java
@TestConfiguration
public class EmbeddedRedis {
  private final RedisServer redisServer;

  public EmbeddedRedis() throws IOException {
    this.redisServer = new RedisServer(63790);
  }

  // 1. Spring이 EmbeddedRedis 객체를 생성
  @PostConstruct
  public void start() throws IOException {
    // 2️. EmbeddedRedis 생성 후 → start() 호출 (Redis 서버 시작)
    this.redisServer.start();
  }

  // 3. 테스트 실행

  @PreDestroy
  public void stop() throws IOException {
    // 4. EmbeddedRedis 소멸 전 → stop() 호출 (Redis 서버 종료)  
    this.redisServer.stop();
  }
}

🚨 Local에서 Redis를 기본 포트(6379)로 실행하고 있어서 Embedded Redis는 다른 포트를 지정해주었습니다.

 

 

4. 환경 설정 파일 추가

application.yml
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379

---
spring:
  config:
    activate:
      on-profile: test
  data:
    redis:
      host: 127.0.0.1
      port: 63790

test 환경에서는 redis의 port가 다르므로 환경 설정 파일에 해당 내용을 추가해줍니다.

 

이를 통해 알 수 있는 건 환경마다 환경 설정을 다르게 할 수 있다는 사실입니다🤗!

 

 

5. 테스트 코드 작성

Test.java
@SpringBootTest
@Import(EmbeddedRedis.class)
@ActiveProfiles("test")
class UserQueueServiceTest {}

작성한 Redis Configuration을 Import 해주고, Profile을 적용하기 위해 ActiveProfiles도 추가해줍니다.

 

 

 

📑

참고 자료

Chat GPT

https://fastcampus.co.kr/dev_online_traffic_data

 

9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 완벽 마스터하기 | 패스트캠퍼스

실무에서 자주 일어나는 대용량 트래픽 & 데이터 처리 업무를 한번에 마스터할 수 있도록 모든 것을 담았습니다. 대기업 & 빅테크 현업 강사진 8인과 함께 하는 고퀄리티 현업 대비형 강의! 타사

fastcampus.co.kr

 

헤헷,, 제가 한 건 아니고,, Spring Boot Scheduler가 한 것입니다,,

를 나타나내는 잔망루피입니다..

 

사용 방법이 간단해서 짧게 정리하면서 git bash에서 하염없이 백그라운드 요청을 했던 조그마한 실수도 공유합니다^^..

 

1. Spring boot Scheduler란

주로 백그라운드에서 반복적으로 수행해야 하는 작업을 위해 사용되며, 정해진 시간이나 주기에 따라 작업을 실행할 수 있게 해줍니다.

대기열에 계신 고객님들을 손수 대기열에서 제거해드릴 수 있지만, 저보단 Spring Boot Scheduler가 더 잘 할 것이라 믿습니다^^!

 

 

2. 설정

FlowApplication.java
@EnableScheduling
@SpringBootApplication
public class FlowApplication {

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

 

@EnableScheduling을 통해 Scheduler 사용을 활성화힙니다.

 

 

3. Scheduler를 실행할 메서드 지정

UserQueueService.java
public class UserQueueService {
  @Scheduled(initialDelay = 5000, fixedDelay = 10000) // 서버 시작 후 5초 뒤, 이후 10초마다 메서드 실행
  public void scheduleAllowUser(){
    log.info("called Scheduling...");
  }
}

 

 

4. local/test 환경에서 Scheduler 동작 설정

application.yml
server:
  port: 9010

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379

scheduler:
  enabled: true

---
spring:
  config:
    activate:
      on-profile: test
  data:
    redis:
      host: 127.0.0.1
      port: 63790

scheduler:
  enabled: false

테스트를 Scheduler를 실행하면 예상과는 다르게 동작할 수 있으므로 잠시 실행을 꺼둡니다.

 

public class UserQueueService {
  @Value("${scheduler.enabled}")
  private Boolean scheduling = false;
  
  @Scheduled(initialDelay = 5000, fixedDelay = 10000)
  public void scheduleAllowUser(){
    if(!scheduling){
      log.info("passed scheduling...");
      return;
    }

    log.info("called Scheduling...");
  }
}

환경 설정 value를 받아서 scheduling값에 할당해줍니다.

test 환경에서 enabled를 false로 설정해두었으니, passed scheduling을 호출하고 메서드는 종료될 것입니다.

 

🌹CMD를 이용해서 API 요청을 보낼 때, 주의할 점

$ curl -X POST localhost:9010/api/v1/queue?user_id=1001&queue=test1
[1]+  Done                    curl -X POST localhost:9010/api/v1/queue?user_id=1001

위에 처럼 요청하면, 의문의 [1]+ 함께 제 queue 파라미터를 무시합니다.

자판기에 동전을 넣고 음료수를 뽑아 먹으려했지만, 자판기가 동전을 먹은 경우랑 비슷한 것입니다..

하지만, 이 경우에는 제 탓도 있던^^...

 

Shell에서는 &가 명령어를 백그라운드에서 실행하도록 만들기 때문에, curl 명령어에서 &를 사용하면 URL의 쿼리 파라미터가 아니라 셸 명령어로 해석될 수 있습니다.

따라서 뒤에 붙은 queue=test1은 무시되거나 처리되지 않아 user_id 파라미터만 전달됩니다.

 

$ curl -X POST "localhost:9010/api/v1/queue?user_id=1001&queue=test1"

이런 경우에는 간단히, URL을 큰따옴표로 감싸서 셸이 URL을 제대로 인식하도록 하면 됩니다.

 

 

 

📑

참고 자료

Chat GPT

https://fastcampus.co.kr/dev_online_traffic_data

 

9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 완벽 마스터하기 | 패스트캠퍼스

실무에서 자주 일어나는 대용량 트래픽 & 데이터 처리 업무를 한번에 마스터할 수 있도록 모든 것을 담았습니다. 대기업 & 빅테크 현업 강사진 8인과 함께 하는 고퀄리티 현업 대비형 강의! 타사

fastcampus.co.kr

https://velog.io/@ktf1686/Spring-Spring-Scheduler-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0

 

[Spring] Spring Scheduler 적용해보기

스프링 스케줄러는 Java에서 제공하는 스프링 프레임워크의 일부로, 애플리케이션 내에서 정기적으로 실행되어야 하는 작업을 스케줄링하는데 사용됩니다.스프링 스케줄러는 주로 백그라운드

velog.io

https://spring.io/guides/gs/scheduling-tasks

 

Getting Started | Scheduling Tasks

Although scheduled tasks can be embedded in web applications, the simpler approach (shown in this guide) creates a standalone application. To do so, package everything in a single, executable JAR file, driven by a Java main() method. The following snippet

spring.io

 

대기열 시스템 강의가 마무리에 들어가고 있습니다.

 

기존에 마실가실 리팩토링을 진행하면서 Custom Exception을 정리했던 적이 있는데, 이번에 새로 알게된 Record 객체와 더불어 다시 한 번 Custom Exception을 정리해 보고자합니다.

 

(+ 이전에 쓴 글: [1년 후 마실가실] Custom Error 처리)

 

 

1. ApplicationException.java

RuntimeException을 상속하는 Custom Exception을 선언합니다.

Exception이 발생했을 때, 고갱님께 의도하는 ErrorCode와 메시지를 보여드리기 위함입니다.

@AllArgsConstructor
@Getter
public class ApplicationException extends RuntimeException {
  private HttpStatus status;
  private String code;
  private String reason;

}

 

왜냐면 고객님께 아래와 같은 순수한 에러를 보여드리기에는 사이가 멀어질 수 있기 때문입니다.

"status":500,"error":"Internal Server Error",
"requestId":"679d92c0-1","message":"already registered...",
"trace":"java.lang.Exception: already registered...\r\n
\tat com.traffic.flow.service.UserQueueService.registerWaitQueue(UserQueueService.java:22)\r\n\t
Suppressed: The stacktrace has been enhanced by Reactor, 
refer to additional information below: \r\n
Error has been observed at the following site(s):\r\n
\t*__checkpoint ⇢ Handler com.traffic.flow.controller.UserQueueController#registerUser(Long) [DispatcherHandler]
...

 

 

2. ErrorCode.java

여러 에러 코드를 Enum 형식으로 정리해놓으면 기분이 좋습니다.

농담이고, enum을 사용하면 한 곳(ErrorCode)에서 예외를 관리할 수 있어서 유지보수가 용이해지기 때문입니다.

또한, build() 메서드를 활용해서 status, code, reason을 계속해서 직접 입력했을 때, 실수할 가능성도 낮출 수 있습니다.

@AllArgsConstructor
public enum ErrorCode {
  QUEUE_ALREADY_REGISTERED_USER(HttpStatus.CONFLICT, "UQ-0001", "Already Registered in Queue"),

  private final HttpStatus status;
  private final String code;
  private final String reason;

  public ApplicationException build(){
    return new ApplicationException(status, code, reason);
    // ErrorCode.QUEUE_ALREADY_REGISTERED_USER.build()를 호출하면 ApplicationException이 생성
  }
}

 

⭐ build() 메서드 동작 과정

Java의 enum은 내부적으로 객체처럼 동작하며, 생성된 인스턴스를 통해 필드 값을 참조할 수 있습니다.
ErrorCode.QUEUE_ALREADY_REGISTERED_USER.build()를 호출하면 QUEUE_ALREADY_REGISTERED_USER의 필드 값이 자동으로 ApplicationException 생성자에 전달됩니다.
따라서, 각 예외 코드마다 status, code, reason을 직접 넘겨줄 필요 없이 enum을 활용해 깔끔하게 관리할 수 있습니다.

 

🍀 동적 메시지 포맷팅 지원

@AllArgsConstructor
public enum ErrorCode {
  QUEUE_ALREADY_REGISTERED_USER_IN_QUEUE(HttpStatus.CONFLICT, "UQ-0002", "Already Registered in Queue-%s");
  // ErrorCode.QUEUE_ALREADY_REGISTERED_USER_IN_QUEUE.build("A"); -> Already Registered in Queue-A
  // 인자를 초과해서 넘길 경우 -> 무시, 인자보다 부족하게 넘김 경우 -> MissingFormatArgumentException 발생

  private final HttpStatus status;
  private final String code;
  private final String reason;

  public ApplicationException build(Object ...args){ // 0개 이상의 인자를 받을 수 있음
    return new ApplicationException(status, code, reason.formatted(args));
  }
}

🚨 formatted 메서드는 Java15 이상부터 사용 가능

 

 

3. applicationExceptionHandler.java

애플리케이션 전역에서 발생하는 예외를 처리하는 @RestControllerAdvice를 사용하여 applicationExceptionHandler를 만듭니다.

@RestControllerAdvice
// Spring에서 전역 예외 처리를 담당하는 클래스
// 컨트롤러에서 발생하는 예외를 가로채고, 적절한 응답을 반환
public class ApplicationAdvice {

  @ExceptionHandler(ApplicationException.class)
  // ApplicationException이 발생했을 때 실행되는 핸들러
  Mono<ResponseEntity<ServerExceptionResponse>> applicationExceptionHandler(ApplicationException e) {
    return Mono.just(ResponseEntity // ResponseEntity: HTTP 응답을 생성하는 객체
               .status(e.getStatus())
               .body(new ServerExceptionResponse(e.getCode(), e.getReason()))); // JSON 응답으로 변환할 데이터 구조
  }

  public record ServerExceptionResponse(String code, String reason) {

  }

 

⭐ record ServerExceptionResponse를 사용하는 이유

단순 문자열로 처리할 경우,

  1. JSON이 아니기 때문에 프론트엔드에서 파싱할 때 문제가 발생할 수 있음

  2. 새로운 정보를 추가하기 어려워 확장성이 떨어짐

방식 JSON 응답 확장성 유지보수
e.getCode() + e.getReason() ❌ 단순 문자열 ❌ 새로운 필드 추가 어려움 ❌ 가독성 떨어짐
new ServerExceptionResponse
(e.getCode(), e.getReason())
✅ JSON 자동 변환 ✅ 필드 추가 쉬움 ✅ 직관적

* Jackson 라이브러리가 있으면 record는 기본적으로 자동으로 JSON 직렬화됨

  * webflux, mvc 모두 jackson 라이브러리를 포함하고 있음

 

⭐  Java record

불변(immutable) 데이터 객체를 간단하게 정의하기 위한 새로운 클래스 유형으로, Java 16부터 정식 기능으로 사용

  * getter, toString(), equals(), hashCode() 등을 자동으로 생성

  * 데이터 전달용 객체(DTO)나 불변 객체를 만들 때 사용

public record RegisterUserResponse(Long rank) {}

// record -> class
public final class RegisterUserResponse {
// final class로 다른 클래스 extends 불가, 인터페이스 implements만 가능
    private final Long rank;
    // 필드 값을 변경할 수 없음

    public RegisterUserResponse(Long rank) {
        this.rank = rank;
    }

    public Long rank() { // getter 역할, *필드명과 동일한 이름을 가짐
        return rank;
    }

    @Override
    public String toString() {
        return "RegisterUserResponse[rank=" + rank + "]";
    }

    @Override
    public boolean equals(Object o) { /* equals() 자동 생성 */ }

    @Override
    public int hashCode() { /* hashCode() 자동 생성 */ }
}

 

 

4. 결과

* Connected to localhost (127.0.0.1) port 9010 (#0)
> POST /api/v1/queue?user_id=85 HTTP/1.1
> Host: localhost:9010
> User-Agent: curl/7.87.0 #요청을 보낸 애플리케이션
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 409 Conflict
< Content-Type: application/json
< Content-Length: 57
<
{ [57 bytes data] 100    57  100    57    0     0   3879      0 --:--:-- --:--:-- --:--:--  4071
# 100: 총 데이터의 100%를 받았다는 의미
# 57: 총 데이터 크기 (57 바이트)
# 100: 총 데이터의 100%를 보냈다는 의미
# 57: 전송된 데이터 크기 (57 바이트)
# 0: 업로드 크기 (현재 요청이 POST지만, 업로드할 데이터가 없으면 0)
# 0: 현재 업로드 속도(body에 데이터가 있는 경우)
# 3879: 다운로드 속도 (3.879 KB/s)
# 0: 예상 남은 시간 (이미 완료되었으면 0)
{"code":"UQ-0001","reason":"Already Registered in Queue"}
* Connection #0 to host localhost left intact #서버가 연결을 유지한 상태

 

 

📑

참고 자료

Chat GPT

https://fastcampus.co.kr/dev_online_traffic_data

 

9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 완벽 마스터하기 | 패스트캠퍼스

실무에서 자주 일어나는 대용량 트래픽 & 데이터 처리 업무를 한번에 마스터할 수 있도록 모든 것을 담았습니다. 대기업 & 빅테크 현업 강사진 8인과 함께 하는 고퀄리티 현업 대비형 강의! 타사

fastcampus.co.kr

 

 

 

BlockHound 정리해야지,, 생각만 하다가 어느덧 일주일이 흘러버렸습니다,,

 

문제는 BlockHound,, 기억이 나지 않는다는 것^^,,,

 

이런걸 진정한 복습이라 생각하며, 기억과 코드를 찾아보며 정리해 보고자합니다,,

 

할 수 있다,, 기억해 낼 수 있다,, 아자아자,,🐦‍🔥

 

1. BlockHound 정의

개발자가 직접 작성한 코드를 포함하여 JDK, Thrid-party 라이브러리에 사용된 블로킹 메소드 호출을 모두 찾아내서 알려주는 도구

💡 Spring Webflux 기반의 애플리케이션은 모든 코드가 Reactive 방식으로 작동해야 최상의 성능이 나옵니다. 즉, blocking 코드가 존재한다면 충분한 성능이 나오지 않을 수도 있다는 것입니다. 이를 위해 BlockHound를 사용합니다.

 

❓Reactive 방식
비동기(Asynchronous)
논블로킹(Non-blocking)
데이터 스트림(Streams): 데이터를 필요한 만큼 조금씩 전달하며 처리하는 방식
Backpressure: 데이터 소비자가 처리할 수 있는 속도를 조절하는 기능

 

https://github.com/reactor/BlockHound

 

GitHub - reactor/BlockHound: Java agent to detect blocking calls from non-blocking threads.

Java agent to detect blocking calls from non-blocking threads. - reactor/BlockHound

github.com

 

 

2. BlockHound 설정

build.gradle
테스트 시, 사용 예정이므로 testImplementation을 선언
버전은 Github를 참고하여 최신 버전을 입력(25.02.09기준 최신 버전 - 1.0.10)
dependencies {
	testImplementation 'io.projectreactor.tools:blockhound:1.0.10.RELEASE'
}

 

 

3. 테스트 코드 작성

static void setUp() {
    BlockHound.install();
}

@Test
void shouldDetectBlockingCall() {
  Mono<Long> blockingMono = Mono.delay(Duration.ofSeconds(1))
                                .doOnNext(t -> {
                                  try {
                                    Thread.sleep(100);  // 블로킹 호출
                                  } catch (InterruptedException e) {
                                    throw new RuntimeException(e);
                                  }
                                });

  StepVerifier.create(blockingMono)
              .expectError(BlockingOperationError.class)
              .verify();
}

 

하지만, 살다보면 Webflux에서도 Blocking을 허용해야하는 순간이 올 것입니다..🍂

static {
    BlockHound.install(builder ->
        builder.allowBlockingCallsInside(UserControllerTest.class.getName(), "shouldAllowBlockingCallInsideAllowedMethod")
    );
}

@Test
void shouldAllowBlockingCallInsideAllowedMethod() {
  Mono<Long> allowedMono = Mono.delay(Duration.ofSeconds(1))
                               .doOnNext(t -> {
                                 try {
                                   Thread.sleep(100);
                                 } catch (InterruptedException e) {
                                   throw new RuntimeException(e);
                                 }
                               });

  StepVerifier.create(allowedMono)
              .expectNextCount(0)
              .verifyComplete();
}

 

🚨 분명 Blocking을 허용하겠다고 했는데, 아래와 같은 오류가 발생합니다.

expectation "expectComplete" failed (expected: onComplete(); 
actual: onError(reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep))

 

allowBlockingCallsInside는 특정 클래스의 특정 메서드 내부에서 발생하는 블로킹 호출만 허용하는데, shouldAllowBlockingCallInsideAllowedMethod안에 있는 Thread.sleep(100)은 doOnNext의 람다 내부에서 호출되고 있습니다.

이 때, BlockHound는 람다 표현식을 익명 클래스로 취급하기 때문에, allowBlockingCallsInside가 해당 호출을 허용하지 않게 되어 오류를 발생시킵니다.

 

따라서 블로킹이 발생하는 부분을 따로 메서드로 분리합니다.

static {
    BlockHound.install(builder ->
        builder.allowBlockingCallsInside(UserControllerTest.class.getName(), "allowedBlockingMethod")
);

// 블로킹 호출을 별도 메서드로 분리
void allowedBlockingMethod() {
  try {
    Thread.sleep(100);
  } catch (InterruptedException e) {
    throw new RuntimeException(e);
  }
}

@Test
void shouldAllowBlockingCallInsideAllowedMethod() {
  Mono<Long> allowedMono = Mono.delay(Duration.ofSeconds(1)) // 1초 후에 0L이라는 값을 방출
                                     .doOnNext(__ -> allowedBlockingMethod());

  StepVerifier.create(allowedMono)
              .expectNextCount(1)
              .verifyComplete();
}

 

 

 

📑

참고 자료

Chat GPT

https://velog.io/@be_have98/Spring-Webflux-BlockHound-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 

[Spring Webflux] BlockHound 적용하기

비동기 프로그램을 구현하기 위해 블로킹 메소드를 검출하는 BlockHound를 적용해보자

velog.io

https://velog.io/@be_have98/Spring-Webflux-BlockHound-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0

 

[Spring Webflux] BlockHound 적용하기

비동기 프로그램을 구현하기 위해 블로킹 메소드를 검출하는 BlockHound를 적용해보자

velog.io