본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 요약
오늘 강의에서는 대기열 시스템의 핵심 기능인 대기열 스케줄러 개발과 대기열 이탈 처리를 다루었다. 스케줄러는 대기 중인 사용자를 활성화 상태로 전환하는 역할을 하며, 대기열에서 활성화된 사용자는 이탈된 후에도 바로 리다이렉트 할 수 있도록 토큰을 발급하는 기능으로 변경했다. 해당 토큰 발급 api에서 defer를 사용해서 모노를 생성하여 응답하였다. 이번 기회에 Mono의 생성 방식인 just, defer, create의 차이점에 대해 찾아보았다.
Mono 생성 방식의 평가 시점 차이
Reactive Programming에서 Publisher를 생성하는 방식은 평가 시점에 따라 동작이 완전히 달라진다.
Mono.just - 즉시 평가 방식
- Mono.just()는 Mono가 생성되는 순간 값을 평가하고 저장한다.
- 이후 구독이 발생하면 이미 저장된 값을 방출한다.
@Test
void whenUsingMonoJust_thenValueIsCreatedEagerly() {
String[] value = {"Hello"};
Mono<String> mono = Mono.just(value[0]); // 이 시점에 "Hello" 평가됨
value[0] = "World"; // 배열 값 변경
mono.subscribe(actualValue -> assertEquals("Hello", actualValue));
// 여전히 "Hello" 반환
}
- 아래 코드에서 Mono는 생성 시점의 값인 "Hello"를 저장하며, 이후 배열이 "World"로 변경되어도 구독 시 "Hello"를 반환한다.
적용 시나리오
- 이미 알고 있는(already know) element가 있을 때 사용한다.
- 정적 값이나 상수
- 이미 계산이 완료된 데이터
- 부수 효과가 없는 단순 변환
주의사항
- 파라미터로 전달된 메서드는 Mono 생성 시점에 즉시 실행된다.
- 동기적 블로킹 메서드를 호출해야 하고 그 결과를 지연 평가하려면 Mono.fromCallable()을 사용해야 한다.
Mono.fromCallable(() -> expensiveOperation()); // 구독 시점에 실행
Mono.defer - 구독 시점 평가
Mono.defer()는 구독이 발생할 때마다 Supplier를 실행하여 Mono를 새로 생성한다. 각 구독마다 독립적인 Mono 인스턴스가 만들어진다. defer는 값을 생성 시점에 캡처하지 않고, 구독이 발생하면 그때의 상태를 반영한다.
@Test
void whenUsingMonoDefer_thenValueIsCreatedLazily() {
String[] value = {"Hello"};
Mono<String> mono = Mono.defer(() -> Mono.just(value[0]));
value[0] = "World"; // 배열 값 변경
mono.subscribe(actualValue -> assertEquals("World", actualValue));
// 구독 시점의 "World" 반환
}
public Mono<String> getGreetingMono(String name) {
return Mono.defer(() -> {
String greeting = "Hello, " + name;
return Mono.just(greeting);
});
}
@Test
void givenNameIsAlice_whenMonoSubscribed_thenShouldReturnGreetingForAlice() {
Mono<String> mono = generator.getGreetingMono("Alice");
StepVerifier.create(mono)
.expectNext("Hello, Alice")
.verifyComplete();
}
@Test
void givenNameIsBob_whenMonoSubscribed_thenShouldReturnGreetingForBob() {
Mono<String> mono = generator.getGreetingMono("Bob");
StepVerifier.create(mono)
.expectNext("Hello, Bob")
.verifyComplete();
}
- 메서드 파라미터를 기반으로 각기 다른 인사말을 생성하는 경우를 보자.
- 동일한 메서드를 호출하지만, defer 덕분에 각 구독 시점에 전달된 name 값을 기반으로 서로 다른 결과를 생성한다.
적용 시나리오
- 데이터를 동적으로 생성하거나(dynamically derive) 외부 요인, 사용자 입력에 의존하는 계산이 필요할 때 사용한다.
- 데이터베이스 쿼리나 네트워크 호출 같은 비용이 큰 작업을 실제 필요할 때까지 지연
- 구독 시점에 조건에 따라 다른 값 생성
- 기존 Mono 스트림에서 완전히 새로운 Mono 스트림 생성
Mono.create - 세밀한 제어
Mono.create()는 수동으로 값을 생성(manually produced)하며, MonoSink를 통해 명시적으로 성공이나 에러를 제어할 수 있다.
public Mono<String> performOperation(boolean success) {
return Mono.create(sink -> {
if (success) {
sink.success("Operation Success");
} else {
sink.error(new RuntimeException("Operation Failed"));
}
});
}
@Test
void givenSuccessScenario_whenMonoSubscribed_thenShouldReturnSuccessValue() {
Mono<String> mono = generator.performOperation(true);
StepVerifier.create(mono)
.expectNext("Operation Success")
.verifyComplete();
}
@Test
void givenErrorScenario_whenMonoSubscribed_thenShouldReturnError() {
Mono<String> mono = generator.performOperation(false);
StepVerifier.create(mono)
.expectErrorMatches(throwable ->
throwable instanceof RuntimeException &&
throwable.getMessage().equals("Operation Failed"))
.verify();
}
적용 시나리오
- 복잡한 로직을 구현하거나 외부 소스와 통합할 때 필요한 유연성을 제공한다.
- 값 방출, 에러 처리, 백프레셔 관리를 위한 복잡한 로직 구현
- 레거시 시스템이나 복잡한 비동기 작업 통합
- 세밀한 제어가 필요한 경우
주요 차이점 정리
| 특성 | Mono.just() | Mono.defer() | Mono.create() |
| 실행 시점 | Eager (생성 시) | Lazy (구독 시) | Lazy (수동 방출) |
| 값의 상태 | 정적/미리 정의된 값 | 구독마다 동적 값 | 수동으로 생성된 값 |
| 사용 케이스 | 데이터가 준비되어 있고 변하지 않을 때 | 데이터를 요청 시 생성해야 할 때 | 복잡한 로직이나 외부 소스 통합 시 |
| 에러 처리 | 에러 처리 없음 | Mono 생성 중 에러 처리 가능 | 성공과 에러를 명시적으로 제어 |
| 성능 | 정적이고 이미 알고 있는 값에 효율적 | 동적이거나 비용 큰 작업에 유용 | 복잡하거나 비동기 코드에 적합 |
참고 출처
- https://projectreactor.io/docs/core/release/reference/apdx-operatorChoice.html
- https://www.baeldung.com/reactive-mono-just-defer-create
- https://medium.com/@suman.maity112/chapter-4-building-blocks-of-spring-reactive-86b6bf13a109




https://fastcampus.info/4oKQD6b