SERVER SIDE/JAVA

WebFlux를 이용한 RESTful API 샘플

하리하라​ 2025. 5. 14. 14:54
/**
 * WebFlux를 이용한 RESTful API 샘플
 * 
 * 필요한 의존성:
 * - spring-boot-starter-webflux
 * - spring-boot-starter-data-mongodb-reactive (MongoDB 사용)
 * - lombok
 */

// 1. 모델 클래스
package com.example.demo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "products")
public class Product {
    @Id
    private String id;
    private String name;
    private String description;
    private Double price;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

// 2. 리포지토리 인터페이스
package com.example.demo.repository;

import com.example.demo.model.Product;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends ReactiveMongoRepository<Product, String> {
    // ReactiveMongoRepository는 기본적인 CRUD 연산을 제공합니다.
    // 필요한 추가 쿼리 메서드는 여기에 정의할 수 있습니다.
}

// 3. 서비스 클래스
package com.example.demo.service;

import com.example.demo.model.Product;
import com.example.demo.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    @Autowired
    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // 모든 상품 조회
    public Flux<Product> getAllProducts() {
        return productRepository.findAll();
    }

    // ID로 상품 조회
    public Mono<Product> getProductById(String id) {
        return productRepository.findById(id);
    }

    // 상품 생성
    public Mono<Product> createProduct(Product product) {
        product.setCreatedAt(LocalDateTime.now());
        product.setUpdatedAt(LocalDateTime.now());
        return productRepository.save(product);
    }

    // 상품 수정
    public Mono<Product> updateProduct(String id, Product product) {
        return productRepository.findById(id)
                .flatMap(existingProduct -> {
                    existingProduct.setName(product.getName());
                    existingProduct.setDescription(product.getDescription());
                    existingProduct.setPrice(product.getPrice());
                    existingProduct.setUpdatedAt(LocalDateTime.now());
                    return productRepository.save(existingProduct);
                });
    }

    // 상품 삭제
    public Mono<Void> deleteProduct(String id) {
        return productRepository.deleteById(id);
    }
}

// 4. 컨트롤러 클래스
package com.example.demo.controller;

import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    // 모든 상품 조회 (GET)
    @GetMapping
    public Flux<Product> getAllProducts() {
        return productService.getAllProducts();
    }

    // ID로 상품 조회 (GET)
    @GetMapping("/{id}")
    public Mono<ResponseEntity<Product>> getProductById(@PathVariable String id) {
        return productService.getProductById(id)
                .map(product -> ResponseEntity.ok(product))
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    // 상품 생성 (POST)
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Product> createProduct(@RequestBody Product product) {
        return productService.createProduct(product);
    }

    // 상품 수정 (PUT)
    @PutMapping("/{id}")
    public Mono<ResponseEntity<Product>> updateProduct(@PathVariable String id, @RequestBody Product product) {
        return productService.updateProduct(id, product)
                .map(updatedProduct -> ResponseEntity.ok(updatedProduct))
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    // 상품 삭제 (DELETE)
    @DeleteMapping("/{id}")
    public Mono<ResponseEntity<Void>> deleteProduct(@PathVariable String id) {
        return productService.deleteProduct(id)
                .then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)))
                .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }
}

// 5. 애플리케이션 메인 클래스
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebfluxRestfulApiApplication {

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

// 6. 애플리케이션 설정 파일 (src/main/resources/application.properties)
// application.properties
server.port=8080
spring.data.mongodb.uri=mongodb://localhost:27017/productdb

// 테스트를 위한 WebClient 샘플
package com.example.demo.client;

import com.example.demo.model.Product;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class ProductWebClient {

    private final WebClient webClient;
    private final String baseUrl = "http://localhost:8080/api/products";

    public ProductWebClient() {
        this.webClient = WebClient.create();
    }

    // 모든 상품 조회
    public Flux<Product> getAllProducts() {
        return webClient.get()
                .uri(baseUrl)
                .retrieve()
                .bodyToFlux(Product.class);
    }

    // ID로 상품 조회
    public Mono<Product> getProductById(String id) {
        return webClient.get()
                .uri(baseUrl + "/{id}", id)
                .retrieve()
                .bodyToMono(Product.class);
    }

    // 상품 생성
    public Mono<Product> createProduct(Product product) {
        return webClient.post()
                .uri(baseUrl)
                .bodyValue(product)
                .retrieve()
                .bodyToMono(Product.class);
    }

    // 상품 수정
    public Mono<Product> updateProduct(String id, Product product) {
        return webClient.put()
                .uri(baseUrl + "/{id}", id)
                .bodyValue(product)
                .retrieve()
                .bodyToMono(Product.class);
    }

    // 상품 삭제
    public Mono<Void> deleteProduct(String id) {
        return webClient.delete()
                .uri(baseUrl + "/{id}", id)
                .retrieve()
                .bodyToMono(Void.class);
    }
}

 

WebFlux를 이용한 RESTful API 샘플. WebFlux는 Spring Framework의 반응형(reactive) 웹 프레임워크로, 비동기 논블로킹 방식으로 작동.

 
이 샘플 코드는 Spring WebFlux를 사용하여 간단한 제품(Product) 관리 API를 구현. 코드에 다음과 같은 주요 구성요소가 포함.
 
 

주요 구성요소

  1. 모델 클래스 (Product)
    • MongoDB에 저장될 제품 정보를 표현하는 엔티티 클래스
    • ID, 이름, 설명, 가격, 생성/수정 시간을 포함
  2. 리포지토리 인터페이스 (ProductRepository)
    • ReactiveMongoRepository를 확장하여 반응형 데이터 액세스 제공
    • 기본 CRUD 작업을 위한 메서드 자동 생성
  3. 서비스 클래스 (ProductService)
    • 비즈니스 로직 처리
    • Mono와 Flux를 반환하는 반응형 메서드 구현
  4. 컨트롤러 클래스 (ProductController)
    • RESTful API 엔드포인트 정의
    • WebFlux를 사용한 비동기 요청 처리
  5. WebClient 예제
    • API 호출을 위한 반응형 클라이언트 구현

API 엔드포인트

  • GET /api/products: 모든 제품 조회
  • GET /api/products/{id}: ID로 특정 제품 조회
  • POST /api/products: 새 제품 생성
  • PUT /api/products/{id}: 기존 제품 수정
  • DELETE /api/products/{id}: 제품 삭제

이 샘플의 특징

  • 비동기 논블로킹 방식으로 구현되어 높은 동시성 처리가 가능
  • Mono와 Flux 타입을 사용하여 반응형 프로그래밍 구현
  • 함수형 스타일의 코드로 작성되어 가독성과 유지보수성 향상
  • MongoDB와 같은 반응형 데이터베이스와 연동 가능

이 코드를 실행하려면 MongoDB가 설치되어 있거나 애플리케이션 속성에서 MongoDB URI를 적절히 수정해야 한다. 코드의 의존성으로는 spring-boot-starter-webflux, spring-boot-starter-data-mongodb-reactive, lombok이 필요.