Convert Mono Object to Another Mono Object in Spring WebFlux

1. 소개

Spring WebFlux는 비동기적이고 논블로킹 커뮤니케이션을 지원하는 반응형 프로그래밍 프레임워크입니다. WebFlux 작업의 주요 측면 중 하나는 단일 비동기 결과를 나타내는 Mono 객체를 다루는 것입니다. 실제 애플리케이션에서는 데이터를 풍부하게 하거나 외부 서비스 호출을 처리하거나 페이로드를 재구성하기 위해 하나의 Mono 객체를 다른 것으로 변환해야 할 경우가 많습니다.

이 튜토리얼에서는 Project Reactor에서 제공하는 다양한 접근 방식을 사용하여 Mono 객체를 다른 Mono 객체로 변환하는 방법을 탐구합니다.

2. Mono 객체 변환

다양한 방법으로 Mono 객체를 변환하기 전에 코딩 예제를 설정하겠습니다. 우리는 책 빌리기 예제를 통해 다양한 변환 방법을 시연할 것입니다. 이 시나리오를 캡처하기 위해 세 가지 주요 클래스를 사용하겠습니다.

User 클래스는 도서관 사용자를 나타냅니다:

public class User {
    private String userId;
    private String name;
    private String email;
    private boolean active;

    // 표준 setter 및 getter
}

각 사용자는 userId로 고유하게 식별되며 nameemail과 같은 개인 세부정보를 가집니다. 추가로, 사용자가 현재 책을 빌릴 수 있는지 나타내는 active 플래그가 있습니다.

Book 클래스는 도서관의 책 컬렉션을 나타냅니다:

public class Book {
    private String bookId;
    private String title;
    private double price;
    private boolean available;

    // 표준 setter 및 getter
}

각 책은 bookId로 식별되며 titleprice와 같은 속성을 가집니다. available 플래그는 책이 대여 가능한지 여부를 나타냅니다.

BookBorrowResponse 클래스는 대출 작업의 결과를 캡슐화합니다:

public class BookBorrowResponse {
    private String userId;
    private String bookId;
    private String status;

    // 표준 setter 및 getter
}

이 클래스는 과정에 관련된 userIdbookId를 연결하며 대출이 수락되었는지 거부되었는지를 나타내는 status 필드를 제공합니다.

3. map()를 사용한 동기 변환

map 연산자는 Mono 내부의 데이터에 동기 함수를 적용합니다. 형식 지정, 필터링 또는 간단한 계산과 같은 경량 작업에 적합합니다. 예를 들어, Mono 사용자에서 이메일 주소를 가져오고 싶다면 map을 사용하여 변환할 수 있습니다:

@Test
void givenUserId_whenTransformWithMap_thenGetEmail() {
    String userId = "U001";
    Mono<User> userMono = Mono.just(new User(userId, "John", "john@example.com"));
    Mockito.when(userService.getUser(userId))
      .thenReturn(userMono);

    Mono<String> userEmail = userService.getUser(userId)
      .map(User::getEmail);

    StepVerifier.create(userEmail)
      .expectNext("john@example.com")
      .verifyComplete();
}

4. flatMap()을 사용한 비동기 변환

flatMap() 메서드는 Mono에서 방출된 각 항목을 다른 Publisher로 변환합니다. 변환이 다른 비동기 프로세스를 요구할 때, 예를 들어 다른 API 호출하거나 데이터베이스를 쿼리하는 경우 특히 유용합니다. flatMap()은 변환 결과가 Mono인 경우 결과를 단일 시퀀스로 평탄화합니다.

책 대출 시스템을 살펴보겠습니다. 사용자가 책을 빌려 달라고 요청하면 시스템은 사용자의 회원 상태를 확인하고 책이 대여 가능한지 확인합니다. 두 가지 모두 통과하면 시스템은 대출 요청을 처리하고 BookBorrowResponse를 반환합니다:

public Mono<BookBorrowResponse> borrowBook(String userId, String bookId) {
    return userService.getUser(userId)
      .flatMap(user -> {
          if (!user.isActive()) {
              return Mono.error(new RuntimeException("User is not an active member"));
          }
          return bookService.getBook(bookId);
      })
      .flatMap(book -> {
          if (!book.isAvailable()) {
              return Mono.error(new RuntimeException("Book is not available"));
          }
          return Mono.just(new BookBorrowResponse(userId, bookId, "Accepted"));
      });
}

이 예제에서 사용자 및 책 세부정보 검색과 같은 작업은 비동기이며 Mono 객체를 반환합니다. flatMap()을 사용하여 이러한 작업을 읽기 쉽고 논리적인 방식으로 체인할 수 있으며, 여러 수준의 Mono를 중첩하지 않고도 해결할 수 있습니다. 시퀀스의 각 단계는 이전 단계의 결과에 의존합니다. 예를 들어 사용자가 활성화되어 있는 경우에만 책의 가용성을 확인합니다. flatMap()은 이러한 결정을 동적으로 내릴 수 있도록 하여 흐름을 반응적으로 유지합니다.

5. transform() 메서드를 사용한 재사용 가능한 로직

transform() 메서드는 재사용 가능한 로직을 캡슐화할 수 있는 다용도 도구입니다. 애플리케이션의 여러 부분에서 변환을 반복하는 대신, 한 번 정의하고 필요할 때마다 적용할 수 있습니다. 이는 코드 재사용성, 관심사의 분리 및 가독성을 촉진합니다.

세금 및 할인을 적용하여 책의 최종 가격을 반환해야 하는 예제를 살펴보겠습니다:

public Mono<Book> applyDiscount(Mono<Book> bookMono) {
    return bookMono.map(book -> {
        book.setPrice(book.getPrice() - book.getPrice() * 0.2);
        return book;
    });
}

public Mono<Book> applyTax(Mono<Book> bookMono) {
    return bookMono.map(book -> {
        book.setPrice(book.getPrice() + book.getPrice() * 0.1);
        return book;
    });
}

public Mono<Book> getFinalPricedBook(String bookId) {
    return bookService.getBook(bookId)
      .transform(this::applyTax)
      .transform(this::applyDiscount);
}

이 예제에서 applyDiscount() 메서드는 20% 할인을 적용하고, applyTax() 메서드는 10% 세금을 적용합니다. transform 메서드는 두 메서드를 파이프라인에 적용하고 최종 가격이 있는 BookMono를 반환합니다.

6. 여러 소스에서 데이터 병합

zip() 메서드는 여러 Mono 객체를 결합하여 단일 결과를 생성합니다. 결과를 동시에 병합하지 않고 모든 Mono 객체가 발행할 때까지 기다렸다가 결합 함수가 적용됩니다.

사용자 정보와 책 정보를 가져와서 BookBorrowResponse를 만드는 책 대출 예제를 다시 살펴보겠습니다:

public Mono<BookBorrowResponse> borrowBookZip(String userId, String bookId) {
    Mono<User> userMono = userService.getUser(userId)
      .switchIfEmpty(Mono.error(new RuntimeException("User not found")));
    Mono<Book> bookMono = bookService.getBook(bookId)
      .switchIfEmpty(Mono.error(new RuntimeException("Book not found")));
    return Mono.zip(userMono, bookMono,
      (user, book) -> new BookBorrowResponse(userId, bookId, "Accepted"));
}

이 구현에서 zip() 메서드는 응답을 생성하기 전에 사용자와 책 정보가 모두 준비되었는지 확인합니다. 사용자 또는 책 검색이 실패하면 (예: 사용자가 존재하지 않거나 책이 사용 불가인 경우) 오류가 전파되며 결합된 Mono는 적절한 오류 신호로 종료됩니다.

7. 조건부 변환

filter()switchIfEmpty() 메서드를 결합함으로써, 우리는 조건부 로직을 적용하여 Mono 객체를 변환할 수 있습니다. 조건부가 true일 경우 원래 Mono가 반환되고, false일 경우 switchIfEmpty()가 제공하는 다른 Mono로 전환됩니다.

사용자가 활성 상태일 때만 할인을 적용하고, 그렇지 않으면 할인 없이 반환하고자 하는 시나리오를 고려해봅시다:

public Mono<Book> conditionalDiscount(String userId, String bookId) {
    return userService.getUser(userId)
      .filter(User::isActive)
      .flatMap(user -> bookService.getBook(bookId).transform(this::applyDiscount))
      .switchIfEmpty(bookService.getBook(bookId))
      .switchIfEmpty(Mono.error(new RuntimeException("Book not found")));
}

이 예제에서는 userId를 사용하여 UserMono를 가져옵니다. 필터 메서드는 사용자가 활성 상태인지 확인합니다. 사용자가 활성 상태인 경우, 할인을 적용하고 BookMono를 반환합니다. 사용자가 비활성 상태인 경우, Mono는 비워지고, switchIfEmpty() 메서드가 또 다른 책을 할인 없이 가져옵니다. 마지막으로, 책이 존재하지 않는 경우 또 다른 switchIfEmpty()이 적절한 오류를 전파하여 흐름의 강건성과 직관성을 보장합니다.

8. 변환 중 오류 처리

오류 처리는 변환에서 복원력을 보장하여 우아한 대체 메커니즘이나 다른 데이터 소스를 허용합니다. 변환이 실패할 경우 적절한 오류 처리는 우아한 복구, 문제 로깅 또는 대체 데이터 반환에 도움을 줍니다.

onErrorResume() 메서드는 오류가 발생했을 때 대체 Mono를 제공하여 복구하는 데 사용됩니다. 이것은 기본 데이터를 제공하거나 대체 소스에서 데이터를 가져오고자 할 때 특히 유용합니다.

책 대출 예제로 돌아가 봅시다. User 또는 Book 객체를 가져오는 중 오류가 발생하면, 우리는 실패를 우아하게 처리하여 “Rejected” 상태를 가진 BookBorrowResponse 객체를 반환합니다:

public Mono<BookBorrowResponse> handleErrorBookBorrow(String userId, String bookId) {
    return borrowBook(userId, bookId)
      .onErrorResume(ex -> Mono.just(new BookBorrowResponse(userId, bookId, "Rejected")));
}

이 오류 처리 전략은 실패 시나리오에서조차 시스템이 예측 가능하게 반응하므로 사용자 경험을 원활하게 유지합니다.

9. Mono 객체 변환을 위한 모범 사례

Mono 객체를 변환할 때는 반응형 파이프라인이 깨끗하고 효율적이며 유지 관리가 용이하도록 몇 가지 모범 사례를 따르는 것이 중요합니다. 데이터 보강이나 수정과 같은 간단한 동기 변환이 필요할 경우 map() 메서드는 탁월한 선택이 되고, 비동기적 워크플로우가 필요한 작업에는 flatMap()이 이상적입니다. 파이프라인을 깨끗하고 재사용 가능하게 유지하기 위해 transform() 메서드로 로직을 캡슐화하여 모듈화와 관심사의 분리를 촉진합니다. 가독성을 유지하기 위해 체이닝을 중첩 연산보다 선호해야 합니다.

오류 처리는 복원력을 보장하는 데 중요한 역할을 합니다. onErrorResume()과 같은 메서드를 사용함으로써 기본 응답이나 대체 데이터 소스를 제공하는 방식으로 우아하게 오류를 관리할 수 있습니다. 마지막으로 모든 단계에서 입력과 출력을 검증하여 문제가 하류로 전파되는 것을 방지함으로써 견고하고 확장 가능한 파이프라인을 보장합니다.

10. 결론

이 튜토리얼에서는 하나의 Mono 객체를 다른 객체로 변환하는 다양한 방법을 배웠습니다. map(), flatMap(), 또는 transform() 중 어떤 연산자가 적합한지 이해하는 것이 중요합니다. 이러한 기술과 모범 사례를 적용함으로써 Spring WebFlux에서 유연하고 유지 관리 가능한 반응형 파이프라인을 구축할 수 있습니다.

언제든지 이 문서에서 사용된 모든 코드 스니펫은 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다