Webflux 개발

이정원·2024년 11월 11일
post-thumbnail

1.Webflux란

Spring WebFlux는 비동기 및 논블로킹(non-blocking) 프로그래밍 모델을 기반으로 하는 Spring Framework의 웹 스택이다. WebFlux는 Reactive Streams 표준을 구현하며, 비동기식 스트리밍 데이터 처리에 적합하다. 리액터(Reactor) 라이브러리를 기반으로 하고 있어, Mono와 Flux라는 리액티브 타입을 사용하여 비동기 데이터를 처리한다. 또한 요청 처리 시 스레드가 블로킹되지 않기 때문에 높은 동시성을 지원한다. 이는 대량의 요청을 처리할 때 리소스 효율성을 극대화한다.

2.Webflux Toy Project

요구사항

1.메모리 기반 저장소로 간단한 사용자 가입,조회,탈퇴하는 로직

2.개별 MVC 서버를 두어 WebClient를 통한 다수의 서버에 비동기 요청을 보내고, 그 응답을 집계하여 하나의 응답으로 반환하는 작업


개발을 진행하며 중요한 부분이라 생각된것을 정리해보고자 한다.

전체 소스 코드:https://github.com/jeongwwon/Webflux

reactor는 구독자(subscriber)가 없으면 데이터 흐름을 시작하지 않는데, Spring Webflux가 내부적으로 구독자 역할을 하여 반환값을 Http body로 mapping 해준다.

2-1.Webflux 메모리 기반 CRUD 개발

2-1-1.DTO(Data Transfer Object)

DTO (Data Transfer Object)는 애플리케이션의 다양한 계층 사이에서 데이터를 운반하기 위해 사용하는 객체이다. 주로 표현 계층과 서비스 계층 사이에서 데이터를 전달하거나, 클라이언트와 서버 간의 데이터 교환 시 사용된다. DTO는 객체의 상태를 전송할 때 특정 데이터를 캡슐화하여, 필요하지 않은 데이터 노출을 막고 코드의 명확성을 높이는 역할을 한다.

@RequestBody 를 통해 전달 받은 http body의 key를 정의한 객체의 필드 값으로 매핑하여 데이터를 사용한다.

UserCreateRequest

@Data
@AllArgsConstructor
public class UserCreateRequest {
    private String name;
    private String email;
}

저장 컨트롤러

@PostMapping("")
    public Mono<UserResponse> createUser(@RequestBody UserCreateRequest request) {
        return userService.create(request.getName(), request.getEmail())
                .map(UserResponse::of);
    }

저장 서비스

private final UserRepository userRepository;

public Mono<User> create(String name, String email) {
     return userRepository.save(User.builder().name(name).email(email).build());
}

userService에서 전달받은 name과 email 값을 builder를 통해 인스턴스를 생성하고 save를 실행하면 UserRepositoryImpl이 상속받은 유일한 구현체이기 때문에 ConcurrentHashMap에 동시성 처리 후 저장, Mono<User>를 반환한다. DTO를 통한 캡슐화로 데이터 검증 및 보안 강화와 데이터 전달이 명확해진다.

2-1-2.리액티브 타입의 평탄화(flattening reactive types)

함수 내부에 비동기 작업과 리액티브 타입을 반환할때 map을 사용하면 중첩된 구조가 되어 평탄화되지 않은 리액티브 스트림을 생성하게 된다. 이런 경우 flatmap을 사용해야 한다.

User Service

public Mono<User> update(Long id, String name, String email) {
        return userRepository.findById(id)
                .flatMap(u -> {
                    u.setName(name);
                    u.setEmail(email);
                    return userRepository.save(u);
                });
    }

UserRepositoryImpl

@Override
    public Mono<User> save(User user) {
        var now = LocalDateTime.now();
        if (user.getId() == null) {
            user.setId(sequence.getAndAdd(1));
            user.setCreatedAt(now);
        }
        user.setUpdatedAt(now);
        userHashMap.put(user.getId(), user);
        return Mono.just(user);
    }

해당 업데이트에서 id에 해당하는 user의 정보를 변경한뒤 save한다. save함수는 Mono<User>를 반환하여 최종적으로 Mono<Mono<User>>와 같이 된다. 때문에 flatMap을 사용하여 평탄화한다.

2-2.Webflux 외부 API 비동기 요청 집계

WebClient를 이용해서 다수의 서버에 동시요청을하고 aggregation하여 Client에게 응답할수 있다. 본 프로젝트에서 하나의 MVC 서버를 구동하여 비동기 요청을 통해 어떻게 처리되는지 확인한다.

2-2-1. Webclient

비동기 및 논블로킹 방식의 HTTP 요청을 처리할 수 있는 클라이언트이며 요청 및 응답이 완료될 때까지 Thread를 블로킹하지 않는다. 높은 동시성 처리 성능을 제공하므로, 대규모 애플리케이션에서 유용하다. 또한 GET, POST, PUT, DELETE 등 모든 HTTP 메서드를 지원하며 헤더,베이스,URL 등 다양한 설정 추가가 가능하다.

WebClient Config

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(){
        return WebClient.builder().build();
    }
}

Bean으로 등록해야 스프링 webflux로 부터 주입받을수 있다.

@Service
@RequiredArgsConstructor
public class PostClient {
    private final WebClient webClient;
    private final String url="http://127.0.0.1:8090";

    public Mono<PostResponse> getPost(Long id){
        String uriString = UriComponentsBuilder.fromHttpUrl(url)
                .path("/posts/%d".formatted(id))
                .buildAndExpand()
                .toUriString();

        return webClient.get()
                .uri(uriString)
                .retrieve()
                .bodyToMono(PostResponse.class);

    }
}

MVC 서버의 PORT번호를 8090으로 변경한 후 아래의 서비스를 통해 로직을 처리한다.

Post Service

@Service
@RequiredArgsConstructor
public class PostService {
    private final PostClient postClient;

    public Mono<PostResponse> getPostContent(Long id){
        return postClient.getPost(id)
                .onErrorResume(error->Mono.just(new PostResponse(id.toString(),"Fallback data %d".formatted(id))));
    }
    public Flux<PostResponse> getMultipleContent(List<Long> idList){
            return Flux.fromIterable(idList)
                    .flatMap(this::getPostContent)
                    .log();
    }
}

2-2-3.WebTestClient

WebTestClient는 테스트 중 실제 네트워크 요청을 보내지 않고도 HTTP 요청과 응답을 시뮬레이션할 수 있어 빠르고 효율적인 테스트를 가능하게 한다.


@WebFluxTest(UserController.class)
@AutoConfigureWebTestClient
class UserControllerTest {
    @Autowired
    private WebTestClient webTestClient;

    @MockBean
    private UserService userService;

    @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());
                });
    }
 }
  • @WebFluxTest(UserController.class): UserController에 대한 웹 레이어 테스트를 수행한다. 해당 컨트롤러만 로드하며, 다른 빈들은 로드하지 않는다.

  • @AutoConfigureWebTestClient: WebTestClient를 자동으로 구성해준다.

  • @MockBean: UserService를 모킹하여 실제 구현체 대신 테스트에 사용할 수 있다.

  • when(...).thenReturn(...): Mockito 라이브러리에서 제공하는 메서드로, when안의 메서드는 실제 실행되지 않고 호출 되었을때 thenReturn을 통해 정의한 인스턴스를 반환한다.

0개의 댓글