본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 21. · 4 Views
반응형 마이크로서비스 완벽 가이드
Spring WebFlux와 Reactor를 활용한 반응형 마이크로서비스 구축 방법을 초급 개발자를 위해 쉽게 풀어냅니다. 실무에서 바로 적용할 수 있는 패턴과 팁을 담았습니다.
목차
1. Spring WebFlux 설정
어느 날 김개발 씨는 회사 선배로부터 새로운 프로젝트를 맡게 되었습니다. "이번에는 반응형으로 개발해볼까요?" 선배의 말에 김개발 씨는 당황했습니다.
지금까지 Spring MVC만 사용해왔는데 반응형이라니요?
Spring WebFlux는 논블로킹, 반응형 웹 애플리케이션을 구축하기 위한 프레임워크입니다. 마치 여러 손님을 동시에 응대하는 능숙한 웨이터처럼, 하나의 스레드로 수천 개의 요청을 효율적으로 처리할 수 있습니다.
기존 Spring MVC와 달리 적은 리소스로 높은 처리량을 달성할 수 있습니다.
다음 코드를 살펴봅시다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.projectreactor:reactor-core'
}
// application.yml
server:
port: 8080
spring:
webflux:
base-path: /api
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 그동안 Spring Boot로 REST API를 만들어왔지만, 항상 하나의 요청에 하나의 스레드가 할당되는 방식이 당연하다고 생각했습니다.
그런데 오늘 박시니어 씨가 새로운 프로젝트를 제안합니다. "김 개발자님, 이번 프로젝트는 동시 접속자가 많을 것 같아요.
WebFlux로 개발해봅시다." 반응형 프로그래밍이란 무엇일까요? 쉽게 비유하자면, 반응형 프로그래밍은 마치 패스트푸드점의 주문 시스템과 같습니다.
손님이 주문하면 주문서를 받고 바로 다음 손님을 받습니다. 음식이 완성되면 그때 손님을 호출합니다.
손님마다 직원 한 명씩 배정하지 않아도 효율적으로 운영할 수 있습니다. 반응형 프로그래밍도 이처럼 논블로킹 방식으로 여러 작업을 효율적으로 처리합니다.
기존 방식의 문제점은 무엇이었을까요? 전통적인 Spring MVC는 서블릿 기반입니다.
요청 하나당 스레드 하나가 할당되고, 데이터베이스 응답을 기다리는 동안 그 스레드는 아무 일도 하지 못한 채 대기합니다. 동시 접속자가 1000명이라면 1000개의 스레드가 필요합니다.
스레드는 생성 비용이 높고 메모리를 많이 차지합니다. 서버가 감당할 수 있는 한계가 명확했습니다.
바로 이런 문제를 해결하기 위해 Spring WebFlux가 등장했습니다. WebFlux는 Reactor라는 반응형 라이브러리 위에서 동작합니다.
소수의 이벤트 루프 스레드만으로 수천 개의 동시 요청을 처리할 수 있습니다. 스레드가 작업을 기다리며 블로킹되지 않고, 다른 작업을 계속 처리합니다.
리소스 효율이 극대화됩니다. 프로젝트 설정은 어떻게 할까요?
먼저 build.gradle 파일에 spring-boot-starter-webflux 의존성을 추가합니다. 이것 하나로 WebFlux의 모든 필요한 라이브러리가 자동으로 포함됩니다.
Spring MVC의 spring-boot-starter-web 대신 사용하면 됩니다. 중요한 점은 WebFlux와 Spring MVC를 동시에 사용할 수 없다는 것입니다.
둘은 서로 다른 서버 모델을 사용하기 때문입니다. WebFlux는 기본적으로 Netty 서버를 사용합니다.
Tomcat 같은 전통적인 서블릿 컨테이너가 아닙니다. application.yml 설정도 간단합니다.
포트 번호와 base-path 정도만 설정하면 기본적인 준비는 끝입니다. WebFlux는 관례 설정(Convention over Configuration) 원칙을 따르기 때문에, 복잡한 설정 없이도 바로 시작할 수 있습니다.
실제 회사에서는 어떻게 사용할까요? 대규모 트래픽을 처리해야 하는 전자상거래 플랫폼을 예로 들어봅시다.
블랙프라이데이 같은 특정 시기에 동시 접속자가 평소의 10배로 늘어난다면, 전통적인 방식으로는 서버를 10배 늘려야 합니다. 하지만 WebFlux를 사용하면 동일한 서버 자원으로 훨씬 많은 트래픽을 감당할 수 있습니다.
네이버, 카카오 같은 대형 서비스들이 이런 이유로 반응형 아키텍처를 채택하고 있습니다. 주의할 점도 있습니다.
모든 상황에서 WebFlux가 정답은 아닙니다. CPU 집약적인 작업이 많다면 오히려 전통적인 방식이 더 나을 수 있습니다.
WebFlux는 I/O 대기 시간이 긴 작업에 최적화되어 있습니다. 또한 학습 곡선이 가파릅니다.
반응형 프로그래밍 패러다임 자체가 생소하기 때문입니다. 박시니어 씨는 김개발 씨에게 조언합니다.
"처음에는 어렵게 느껴질 거예요. 하지만 개념을 이해하면 정말 강력한 도구가 됩니다." WebFlux를 제대로 활용하면 적은 비용으로 높은 성능을 얻을 수 있습니다.
클라우드 시대에 서버 비용 절감은 곧 경쟁력입니다.
실전 팁
💡 - WebFlux와 Spring MVC는 동시에 사용할 수 없으므로 프로젝트 초기에 명확히 선택하세요
- I/O 대기가 많은 마이크로서비스 환경에서 WebFlux의 장점이 극대화됩니다
- 학습을 위해 작은 사이드 프로젝트부터 시작하는 것을 추천합니다
2. 반응형 Controller 작성
김개발 씨는 드디어 첫 번째 WebFlux API를 작성하기 시작했습니다. 하지만 기존에 사용하던 방식과 뭔가 달랐습니다.
리턴 타입이 Mono와 Flux라니, 이게 대체 뭘까요?
반응형 Controller는 데이터를 즉시 반환하지 않고, 데이터 스트림을 반환합니다. Mono는 0개 또는 1개의 데이터를, Flux는 0개 이상의 여러 데이터를 비동기적으로 전달합니다.
마치 편지를 쓰는 대신 우편함 주소를 알려주는 것처럼, 실제 데이터가 아닌 데이터가 흐를 파이프를 제공하는 것입니다.
다음 코드를 살펴봅시다.
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// 단일 사용자 조회 - Mono 사용
@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id);
}
// 전체 사용자 목록 - Flux 사용
@GetMapping
public Flux<User> getAllUsers() {
return userService.findAll();
}
}
김개발 씨는 첫 번째 엔드포인트를 작성하려고 코드를 열었습니다. 평소처럼 User 객체를 반환하려고 했지만, 박시니어 씨가 옆에서 말합니다.
"WebFlux에서는 **Mono<User>**를 반환해야 해요." Mono와 Flux는 대체 무엇일까요? 쉽게 비유하자면, 이것들은 약속 증서와 같습니다.
실제 물건을 지금 주는 것이 아니라, "나중에 물건을 줄게"라는 약속을 먼저 건네는 것입니다. Mono는 물건 하나에 대한 약속이고, Flux는 여러 물건에 대한 약속입니다.
중요한 것은 약속을 받는 순간 실제 데이터는 아직 없다는 점입니다. 데이터는 나중에 **구독(subscribe)**할 때 비로소 전달됩니다.
왜 이렇게 복잡하게 만들었을까요? 기존 Spring MVC에서는 메서드가 실제 객체를 반환했습니다.
하지만 그 객체를 얻기 위해 데이터베이스 조회를 기다리는 동안, 스레드는 멈춰서 대기해야 했습니다. 100ms가 걸리든 1초가 걸리든, 그 시간 동안 스레드는 다른 일을 할 수 없었습니다.
반응형 방식은 다릅니다. 메서드는 즉시 Mono를 반환하고 끝납니다.
실제 데이터베이스 조회는 백그라운드에서 진행됩니다. 스레드는 블로킹되지 않고 다른 요청을 처리하러 갑니다.
데이터가 준비되면 그때 응답이 클라이언트에게 전달됩니다. 이 모든 과정이 비동기로 일어납니다.
코드를 자세히 살펴봅시다. @GetMapping("/{id}") 메서드는 **Mono<User>**를 반환합니다.
사용자 한 명의 정보를 조회하는 API이기 때문입니다. 반면 @GetMapping 메서드는 **Flux<User>**를 반환합니다.
여러 사용자 목록을 조회하기 때문입니다. 중요한 것은 우리가 직접 구독하지 않는다는 점입니다.
Spring WebFlux 프레임워크가 자동으로 구독하고 응답을 처리해줍니다. 우리는 그저 Mono나 Flux를 반환하기만 하면 됩니다.
실무에서는 어떻게 활용할까요? 실시간 알림 서비스를 개발한다고 가정해봅시다.
사용자가 새 메시지를 받을 때마다 푸시 알림을 보내야 합니다. Flux를 사용하면 메시지가 도착하는 대로 스트리밍 방식으로 클라이언트에게 전달할 수 있습니다.
클라이언트는 연결을 유지한 채로 계속해서 새 메시지를 받습니다. 이것이 바로 Server-Sent Events (SSE) 또는 WebSocket의 기반 기술입니다.
자주 하는 실수가 있습니다. 초보자들은 종종 Mono를 받아서 **.block()**을 호출합니다.
이렇게 하면 반응형의 모든 장점이 사라집니다. 스레드가 다시 블로킹되기 때문입니다.
WebFlux에서는 절대로 block()을 호출하면 안 됩니다. 대신 map, flatMap 같은 연산자로 데이터를 변환하고 조합해야 합니다.
김개발 씨는 처음에는 헷갈렸지만, 몇 개의 API를 작성하다 보니 패턴이 보이기 시작했습니다. "아, 비동기로 처리될 데이터는 항상 Mono나 Flux로 감싸는구나!" 반응형 Controller를 마스터하면 높은 동시성을 다루는 시스템을 자신 있게 개발할 수 있습니다.
실전 팁
💡 - Mono는 단건 조회, Flux는 목록 조회에 사용하세요
- 절대로 Controller나 Service에서 .block()을 호출하지 마세요
- 반환 타입만 Mono/Flux로 바꾸면 Spring이 자동으로 비동기 처리합니다
3. Reactive Repository 사용
API는 만들었지만, 이제 데이터베이스와 연결해야 합니다. 김개발 씨는 평소처럼 JPA를 사용하려 했지만, 박시니어 씨가 고개를 저었습니다.
"JPA는 블로킹 방식이에요. R2DBC를 사용해야 합니다."
**R2DBC(Reactive Relational Database Connectivity)**는 관계형 데이터베이스를 반응형으로 접근하는 라이브러리입니다. 기존 JDBC가 블로킹 방식으로 동작한다면, R2DBC는 완전한 논블로킹 방식으로 데이터베이스와 통신합니다.
ReactiveCrudRepository를 상속받으면 자동으로 반응형 CRUD 메서드를 사용할 수 있습니다.
다음 코드를 살펴봅시다.
// build.gradle에 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'io.r2dbc:r2dbc-postgresql'
}
// User Entity
@Table("users")
public class User {
@Id
private String id;
private String name;
private String email;
}
// Repository
public interface UserRepository extends ReactiveCrudRepository<User, String> {
Flux<User> findByName(String name);
Mono<User> findByEmail(String email);
}
김개발 씨는 지금까지 Spring Data JPA를 사용해왔습니다. CrudRepository를 상속받고, 메서드 이름만으로 쿼리가 자동 생성되는 마법 같은 경험을 했습니다.
그런데 WebFlux에서는 왜 JPA를 쓸 수 없는 걸까요? JPA의 근본적인 한계가 있습니다.
JPA는 오래전에 설계된 기술입니다. 당시에는 블로킹 I/O가 표준이었습니다.
데이터베이스 드라이버인 JDBC 자체가 블로킹 방식으로 동작합니다. 쿼리를 보내고 응답이 올 때까지 스레드가 멈춰서 기다립니다.
이것은 반응형 프로그래밍의 철학과 정면으로 충돌합니다. 그래서 등장한 것이 R2DBC입니다.
R2DBC는 "Reactive Relational Database Connectivity"의 약자입니다. JDBC의 반응형 버전이라고 생각하면 됩니다.
데이터베이스와의 모든 통신이 논블로킹으로 이루어집니다. 쿼리를 보내고 기다리지 않고, 결과가 준비되면 콜백으로 받습니다.
설정은 어떻게 할까요? 먼저 build.gradle에 spring-boot-starter-data-r2dbc 의존성을 추가합니다.
그리고 사용할 데이터베이스의 R2DBC 드라이버도 추가합니다. PostgreSQL을 쓴다면 r2dbc-postgresql, MySQL이라면 r2dbc-mysql을 추가하면 됩니다.
엔티티 클래스는 JPA와 비슷합니다. @Table 어노테이션으로 테이블 이름을 지정하고, @Id로 기본 키를 표시합니다.
하지만 @Entity 어노테이션은 사용하지 않습니다. JPA가 아니기 때문입니다.
훨씬 가볍고 단순합니다. Repository는 ReactiveCrudRepository를 상속받습니다.
기존 CrudRepository와 메서드 이름은 같지만, 반환 타입이 다릅니다. findById는 **Mono<User>**를 반환하고, findAll은 **Flux<User>**를 반환합니다.
쿼리 메서드도 동일한 규칙을 따릅니다. findByName은 여러 사용자를 반환할 수 있으므로 Flux를, findByEmail은 단일 사용자를 반환하므로 Mono를 사용합니다.
실제로 쿼리가 실행되는 시점은 언제일까요? 중요한 개념입니다.
Repository 메서드를 호출하는 순간에는 쿼리가 실행되지 않습니다. Mono나 Flux 객체만 반환됩니다.
실제 쿼리는 누군가 그 Mono나 Flux를 **구독(subscribe)**할 때 비로소 실행됩니다. Spring WebFlux가 자동으로 구독하므로, 우리는 신경 쓸 필요가 없습니다.
실무에서는 어떻게 활용될까요? 대규모 주문 처리 시스템을 생각해봅시다.
동시에 수천 건의 주문이 들어옵니다. 각 주문마다 데이터베이스에서 재고를 확인하고 업데이트해야 합니다.
R2DBC를 사용하면 각 요청이 데이터베이스 응답을 기다리며 스레드를 점유하지 않습니다. 결과적으로 동일한 서버로 10배 많은 주문을 처리할 수 있습니다.
주의할 점이 있습니다. R2DBC는 아직 JPA만큼 성숙하지 않습니다.
복잡한 연관관계 매핑이나 지연 로딩 같은 기능은 제한적입니다. 따라서 복잡한 도메인 모델보다는 간단한 CRUD 위주의 마이크로서비스에 적합합니다.
또한 모든 데이터베이스가 R2DBC를 지원하는 것은 아닙니다. 김개발 씨는 처음 몇 개의 쿼리를 작성하면서 깨달았습니다.
"생각보다 어렵지 않네요. JPA와 사용법이 거의 비슷한데, 반환 타입만 Mono와 Flux로 바뀐 것뿐이에요." 반응형 Repository를 활용하면 데이터베이스 I/O도 완전히 논블로킹으로 처리할 수 있습니다.
실전 팁
💡 - JPA와 R2DBC를 혼용하지 마세요. 한 프로젝트에서는 하나만 사용해야 합니다
- 복잡한 조인이 필요하다면 @Query로 직접 SQL을 작성하는 것도 방법입니다
- 트랜잭션은 @Transactional 대신 TransactionalOperator를 사용하세요
4. 반응형 스트림 처리
김개발 씨는 여러 API를 동시에 호출해서 결과를 조합해야 하는 상황에 처했습니다. 기존 방식이라면 순차적으로 호출하고 기다렸을 텐데, 반응형에서는 어떻게 해야 할까요?
반응형 스트림 처리는 **연산자(Operator)**를 사용해 데이터를 변환하고 조합합니다. map은 데이터를 변환하고, flatMap은 비동기 작업을 연결하며, zip은 여러 스트림을 결합합니다.
마치 레고 블록을 조립하듯이, 작은 연산자들을 조합해서 복잡한 비즈니스 로직을 구현합니다.
다음 코드를 살펴봅시다.
@Service
public class OrderService {
private final UserService userService;
private final ProductService productService;
// 여러 비동기 작업을 조합
public Mono<OrderResponse> createOrder(OrderRequest request) {
return userService.findById(request.getUserId())
.flatMap(user -> {
// 사용자 정보를 기반으로 다음 작업 실행
return productService.findById(request.getProductId())
.map(product -> new OrderResponse(user, product));
})
.switchIfEmpty(Mono.error(new RuntimeException("User or Product not found")));
}
}
김개발 씨는 주문 생성 API를 개발하고 있습니다. 사용자 정보도 조회해야 하고, 상품 정보도 조회해야 합니다.
두 정보를 합쳐서 주문을 만들어야 하는데, 어떻게 해야 할까요? 전통적인 방식이라면 간단했습니다.
먼저 userService.findById()를 호출하고 결과를 받습니다. 그다음 productService.findById()를 호출하고 결과를 받습니다.
두 결과를 조합해서 반환합니다. 하지만 이 방식은 순차적입니다.
사용자 조회에 100ms, 상품 조회에 100ms가 걸린다면 총 200ms가 필요합니다. 반응형에서는 연산자를 사용합니다.
map 연산자는 가장 기본적입니다. Mono나 Flux 안의 데이터를 다른 형태로 변환합니다.
예를 들어 **Mono<User>**를 **Mono<String>**으로 바꿀 수 있습니다. 중요한 점은 map 안의 함수는 동기적이어야 한다는 것입니다.
즉시 값을 반환해야 합니다. 그렇다면 flatMap은 무엇일까요?
flatMap은 비동기 작업을 연결할 때 사용합니다. map은 일반 값을 반환하지만, flatMap은 Mono나 Flux를 반환하는 함수를 받습니다.
위 코드에서 userService.findById()는 Mono<User>를 반환합니다. 그 결과를 받아서 productService.findById()를 호출하는데, 이것도 Mono<Product>를 반환합니다.
이런 경우 flatMap을 사용해야 합니다. 만약 map을 사용하면 어떻게 될까요?
Mono<Mono<Product>> 같은 이상한 타입이 됩니다. Mono 안에 또 Mono가 들어간 것입니다.
이것은 우리가 원하는 것이 아닙니다. flatMap은 이 중첩을 자동으로 **평탄화(flatten)**해서 Mono<Product>로 만들어줍니다.
여러 작업을 병렬로 실행하려면 어떻게 할까요? Mono.zip을 사용합니다.
zip은 여러 Mono를 동시에 실행하고, 모든 결과가 준비되면 조합합니다. 예를 들어 사용자 조회와 상품 조회를 동시에 시작할 수 있습니다.
각각 100ms가 걸려도, 병렬로 실행하면 총 100ms면 충분합니다. 실무 예제를 더 살펴봅시다.
대시보드 API를 만든다고 가정합시다. 사용자 정보, 최근 주문 목록, 추천 상품을 모두 조회해서 한 번에 반환해야 합니다.
세 개의 API 호출이 필요합니다. Mono.zip을 사용하면 세 호출을 동시에 실행하고, 모두 완료되면 결과를 조합해서 반환할 수 있습니다.
switchIfEmpty도 유용합니다. Mono가 비어있을 때, 즉 데이터가 없을 때 대체 값을 제공하거나 에러를 발생시킬 수 있습니다.
위 코드에서는 사용자나 상품을 찾지 못하면 RuntimeException을 발생시킵니다. 자주 하는 실수는 블로킹 코드를 섞는 것입니다.
flatMap 안에서 Thread.sleep()을 호출하거나, 블로킹 I/O를 수행하면 안 됩니다. 모든 작업은 논블로킹이어야 합니다.
블로킹 작업이 필요하다면 subscribeOn으로 별도 스레드 풀에서 실행해야 합니다. 김개발 씨는 처음에는 연산자가 너무 많아서 혼란스러웠습니다.
하지만 몇 가지 핵심 연산자만 익히면 대부분의 상황을 해결할 수 있다는 것을 깨달았습니다. 반응형 스트림 처리를 마스터하면 복잡한 비동기 로직도 깔끔하게 표현할 수 있습니다.
실전 팁
💡 - 동기 변환은 map, 비동기 작업 연결은 flatMap을 사용하세요
- 여러 작업을 병렬로 실행하려면 Mono.zip이나 Flux.merge를 활용하세요
- 에러 처리는 onErrorResume, onErrorReturn으로 우아하게 처리할 수 있습니다
5. WebClient 사용
김개발 씨는 외부 결제 API를 호출해야 하는 기능을 맡았습니다. 평소라면 RestTemplate을 사용했을 텐데, 박시니어 씨가 말합니다.
"반응형 환경에서는 WebClient를 써야 해요."
WebClient는 Spring의 반응형 HTTP 클라이언트입니다. RestTemplate이 블로킹 방식으로 동작한다면, WebClient는 완전한 논블로킹 방식으로 HTTP 요청을 처리합니다.
Mono와 Flux를 반환하므로 다른 반응형 컴포넌트와 자연스럽게 조합할 수 있습니다.
다음 코드를 살펴봅시다.
@Service
public class PaymentService {
private final WebClient webClient;
public PaymentService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("https://api.payment.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<PaymentResponse> processPayment(PaymentRequest request) {
return webClient.post()
.uri("/payments")
.bodyValue(request)
// 응답을 Mono로 받음
.retrieve()
.bodyToMono(PaymentResponse.class)
.timeout(Duration.ofSeconds(5))
.onErrorResume(e -> Mono.error(new PaymentException("Payment failed", e)));
}
}
김개발 씨는 지금까지 RestTemplate으로 외부 API를 호출해왔습니다. 간단하고 직관적이었습니다.
하지만 반응형 프로젝트에서 RestTemplate을 사용하면 어떤 문제가 생길까요? RestTemplate의 치명적인 단점이 있습니다.
RestTemplate은 요청을 보내고 응답이 올 때까지 스레드를 블로킹합니다. 외부 API가 느리면 우리 애플리케이션도 함께 느려집니다.
특히 마이크로서비스 환경에서는 여러 서비스를 연쇄적으로 호출하는데, 각 호출마다 블로킹이 발생하면 성능이 급격히 떨어집니다. WebClient는 이 문제를 해결합니다.
WebClient는 Netty 기반으로 동작하며 완전히 논블로킹입니다. 요청을 보내고 즉시 Mono를 반환합니다.
응답이 오면 그때 데이터가 스트림으로 전달됩니다. 스레드는 블로킹되지 않고 다른 작업을 처리할 수 있습니다.
어떻게 설정할까요? WebClient.Builder를 주입받아서 WebClient 인스턴스를 생성합니다.
baseUrl을 설정하면 매번 전체 URL을 입력할 필요가 없습니다. defaultHeader로 모든 요청에 공통으로 들어갈 헤더를 설정할 수 있습니다.
예를 들어 인증 토큰을 여기에 설정하면 편리합니다. 실제 요청은 어떻게 보낼까요?
post(), get(), put(), delete() 메서드로 HTTP 메서드를 선택합니다. **uri()**로 엔드포인트 경로를 지정하고, **bodyValue()**로 요청 본문을 전달합니다.
마지막으로 **retrieve()**를 호출하면 응답을 받을 준비가 됩니다. **bodyToMono()**는 응답 본문을 원하는 타입으로 변환합니다.
JSON 응답이라면 자동으로 역직렬화됩니다. PaymentResponse.class를 지정하면 Mono<PaymentResponse>가 반환됩니다.
여러 항목을 받을 때는 **bodyToFlux()**를 사용합니다. timeout도 중요합니다.
외부 API가 응답하지 않으면 무한정 기다릴 수 없습니다. timeout을 설정하면 지정된 시간 내에 응답이 오지 않으면 TimeoutException이 발생합니다.
보통 3~5초 정도가 적당합니다. 에러 처리는 어떻게 할까요?
onErrorResume으로 에러를 잡아서 다른 Mono로 변환할 수 있습니다. 위 코드에서는 모든 에러를 PaymentException으로 래핑합니다.
필요하다면 특정 에러만 잡아서 처리할 수도 있습니다. 실무에서는 어떻게 활용될까요?
전자상거래 플랫폼에서 주문을 처리한다고 가정합시다. 재고 확인 API, 결제 API, 배송 API를 순차적으로 호출해야 합니다.
WebClient를 사용하면 각 호출이 논블로킹으로 처리됩니다. flatMap으로 연결하면 전체 플로우가 깔끔하게 표현됩니다.
재시도(retry) 기능도 유용합니다. 외부 API는 간헐적으로 실패할 수 있습니다.
네트워크 순단이나 일시적인 서버 과부하 때문입니다. retry() 연산자를 추가하면 실패 시 자동으로 재시도합니다.
지수 백오프(exponential backoff)를 적용하면 더욱 안정적입니다. 주의할 점이 있습니다.
WebClient는 기본적으로 연결을 재사용합니다. 연결 풀을 관리하므로 매번 새로 생성하지 말고 싱글톤으로 재사용해야 합니다.
위 코드처럼 생성자에서 한 번만 만들고 계속 사용하는 것이 올바른 방법입니다. 김개발 씨는 WebClient를 사용하면서 외부 API 호출이 전체 성능에 미치는 영향이 크게 줄어드는 것을 확인했습니다.
WebClient를 마스터하면 마이크로서비스 간 통신을 효율적으로 구현할 수 있습니다.
실전 팁
💡 - WebClient는 싱글톤으로 재사용하세요. 매번 생성하면 성능 저하가 발생합니다
- timeout은 반드시 설정하세요. 외부 서비스 장애가 우리 서비스로 전파되는 것을 막습니다
- 실패 가능성이 높은 API는 retry()와 circuit breaker를 함께 사용하세요
6. 반응형 에러 처리
김개발 씨는 개발을 마치고 테스트를 하다가 당황했습니다. 에러가 발생해도 try-catch로 잡히지 않았습니다.
박시니어 씨가 설명합니다. "반응형에서는 에러도 스트림의 일부예요.
다르게 처리해야 합니다."
반응형 프로그래밍에서 에러는 **에러 신호(error signal)**로 스트림을 통해 전파됩니다. 일반적인 try-catch는 작동하지 않고, onErrorResume, onErrorReturn, onErrorMap 같은 연산자를 사용해야 합니다.
에러를 복구하거나 변환하거나 대체 값을 제공하는 등 유연하게 처리할 수 있습니다.
다음 코드를 살펴봅시다.
@Service
public class UserService {
private final UserRepository userRepository;
public Mono<User> getUserWithFallback(String userId) {
return userRepository.findById(userId)
// 에러 발생 시 기본 사용자 반환
.onErrorReturn(new User("guest", "Guest User"))
// 또는 다른 Mono로 대체
.onErrorResume(e -> {
log.error("Failed to get user: {}", userId, e);
return getDefaultUser();
})
// 또는 에러를 다른 타입으로 변환
.onErrorMap(e -> new UserNotFoundException("User not found: " + userId, e));
}
private Mono<User> getDefaultUser() {
return Mono.just(new User("default", "Default User"));
}
}
김개발 씨는 사용자를 조회하는 API를 작성했습니다. 사용자가 없을 때를 대비해서 try-catch로 예외를 잡으려고 했습니다.
하지만 아무리 해도 예외가 잡히지 않았습니다. 왜 try-catch가 작동하지 않을까요?
반응형 프로그래밍에서는 실행 시점이 다릅니다. userRepository.findById()를 호출하는 순간에는 실제로 쿼리가 실행되지 않습니다.
Mono 객체만 반환되고, 실제 실행은 나중에 구독할 때 일어납니다. try-catch는 코드가 실행되는 순간의 예외만 잡을 수 있습니다.
따라서 아무것도 잡히지 않습니다. 에러도 스트림의 신호입니다.
반응형 스트림에는 세 가지 신호가 있습니다. onNext(데이터), onComplete(완료), onError(에러)입니다.
에러가 발생하면 onError 신호가 스트림을 따라 전파됩니다. 마치 데이터가 흐르듯이 에러도 흐릅니다.
onErrorReturn은 가장 간단합니다. 에러가 발생하면 미리 정의한 기본 값을 반환합니다.
위 코드에서는 사용자 조회에 실패하면 게스트 사용자를 반환합니다. 서비스가 완전히 멈추는 것보다는 기본 값이라도 제공하는 것이 낫습니다.
onErrorResume은 더 유연합니다. 에러를 받아서 다른 Mono나 Flux를 반환할 수 있습니다.
예를 들어 메인 데이터베이스 조회에 실패하면 캐시에서 조회하거나, 백업 API를 호출할 수 있습니다. 에러를 로깅한 후 대체 방법을 시도하는 것입니다.
onErrorMap은 에러를 변환합니다. 발생한 에러를 다른 타입의 에러로 바꿉니다.
예를 들어 데이터베이스의 SQLException을 비즈니스 레벨의 UserNotFoundException으로 변환할 수 있습니다. 상위 레이어에서는 데이터베이스 에러의 세부 사항을 알 필요가 없습니다.
doOnError는 에러를 소비하지 않고 관찰만 합니다. 로깅이나 메트릭 기록 같은 부수 효과를 수행할 때 유용합니다.
에러는 그대로 전파되지만, 우리는 그 사실을 알 수 있습니다. 실무에서는 어떻게 활용될까요?
결제 시스템을 예로 들어봅시다. 메인 결제 게이트웨이가 실패하면 onErrorResume으로 백업 게이트웨이를 시도합니다.
그것도 실패하면 onErrorMap으로 PaymentFailedException을 발생시킵니다. 사용자에게는 명확한 에러 메시지를 제공하면서도, 내부적으로는 최선을 다해 복구를 시도하는 것입니다.
retry도 에러 처리의 일부입니다. 일시적인 네트워크 오류나 데이터베이스 순단 같은 경우, 재시도하면 성공할 수 있습니다.
**retry(3)**을 추가하면 에러 발생 시 최대 3번까지 재시도합니다. retryWhen으로 더 정교한 재시도 전략을 구현할 수도 있습니다.
timeout과 함께 사용하면 더 강력합니다. 외부 API 호출에 timeout을 설정하고, TimeoutException이 발생하면 onErrorResume으로 캐시된 데이터를 반환합니다.
이렇게 하면 외부 서비스가 느려져도 우리 서비스는 빠르게 응답할 수 있습니다. 주의할 점이 있습니다.
에러가 발생하면 스트림이 종료됩니다. 이후의 데이터는 전달되지 않습니다.
Flux에서 중간에 에러가 발생해도 계속 진행하고 싶다면 onErrorContinue를 사용해야 합니다. 각 항목마다 독립적으로 에러를 처리할 수 있습니다.
김개발 씨는 처음에는 에러 처리가 복잡하게 느껴졌습니다. 하지만 연산자들의 역할을 이해하고 나니, 오히려 try-catch보다 더 명확하고 유연하다는 것을 깨달았습니다.
반응형 에러 처리를 마스터하면 견고하고 회복력 있는 시스템을 만들 수 있습니다.
실전 팁
💡 - 에러 복구가 가능하면 onErrorResume, 단순 기본값이면 onErrorReturn을 사용하세요
- 모든 외부 호출에는 timeout과 에러 처리를 함께 적용하세요
- 전역 에러 핸들러는 @ControllerAdvice 대신 WebExceptionHandler를 구현하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Istio 설치와 구성 완벽 가이드
Kubernetes 환경에서 Istio 서비스 메시를 설치하고 구성하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어낸 가이드입니다. istioctl 설치부터 사이드카 주입까지 단계별로 학습합니다.
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
관찰 가능한 마이크로서비스 완벽 가이드
마이크로서비스 환경에서 시스템의 상태를 실시간으로 관찰하고 모니터링하는 방법을 배웁니다. Resilience4j, Zipkin, Prometheus, Grafana, EFK 스택을 활용하여 안정적이고 관찰 가능한 시스템을 구축하는 실전 가이드입니다.
Prometheus 메트릭 수집 완벽 가이드
Spring Boot 애플리케이션의 메트릭을 Prometheus로 수집하고 모니터링하는 방법을 배웁니다. Actuator 설정부터 PromQL 쿼리까지 실무에 필요한 모든 내용을 다룹니다.