스프링 WebFlux 데이터베이스
스프링 WebFlux 데이터베이스 4 - 스프링 WebFlux Reactive MongoDB 단일 연결
https://youtu.be/CPYkBOAptHg?si=ZYAy0ZyqbKecDlFK
스프링 WebFlux 데이터베이스 4 - 스프링 WebFlux Reactive MongoDB 단일 연결
- 스프링 WebFlux 데이터베이스 4 - 스프링 WebFlux Reactive MongoDB 단일 연결
- Spring WebFlux에서 Reactive MongoDB 연결하기
- MongoDB Client의 두 가지 접근 방식
- WebFlux와 Reactive MongoDB 조합
- 필요한 프로젝트 의존성
- 일반 MongoDB와 Reactive MongoDB 의존성 구분
- MongoDB 연결 정보
- application.properties에서 MongoDB 연결하기
- application.yml로 설정하기
- URI 방식의 장점
- Credential을 소스 코드에 직접 저장하지 않기
- MongoDB Collection과 Document
- Document 클래스 작성
- JPA Entity와 MongoDB Document의 차이
- ReactiveMongoRepository 작성
- MongoRepository와 ReactiveMongoRepository 차이
- Flux를 이용한 전체 데이터 조회
- Mono를 이용한 단일 데이터 조회
- Service 작성
- Controller 작성
- 전체 데이터 조회 흐름
- 실행
- ReactiveMongoRepository의 Query Method
- 데이터 저장
- 데이터 삭제
- Reactive 흐름에서 block()을 사용하지 않기
- 데이터가 존재하지 않는 경우
- MongoDB와 R2DBC의 차이
- MySQL R2DBC와 Reactive MongoDB 비교
- Repository 전체 구조 비교
- MongoDB Document 설계에서 생각할 점
- Reactive MongoDB를 사용하면 무조건 성능이 좋아질까?
- MongoDB Index도 중요하다
- Docker 환경에서 MongoDB 연결
- 운영 환경에서 연결 상태 확인
- Reactive MongoDB에서 Error 처리
- ReactiveMongoTemplate이 필요한 경우
- 실무에서 Repository와 Document를 바로 Controller에서 사용해도 될까?
- 전체 프로젝트 구조
- 전체 코드 예제
- 전체 실행 구조
- WebFlux + R2DBC + Reactive MongoDB 관계
- 정리
Spring WebFlux에서 Reactive MongoDB 연결하기
Spring WebFlux는 비동기·논블로킹 방식으로 요청을 처리하는 Reactive Web Stack이다.
WebFlux 애플리케이션에서 데이터베이스를 사용할 때도 요청 처리 흐름과 데이터 접근 방식을 함께 고려해야 한다.
MongoDB를 예로 들면 Spring에서는 크게 동기 방식과 Reactive 방식으로 접근할 수 있다.
Spring Application
↓
MongoDB Client
↓
MongoDB
이때 WebFlux의 Reactive 처리 모델을 데이터베이스 접근까지 이어가고 싶다면 Reactive MongoDB를 사용하는 방식이 자연스럽다.
전체 구조는 다음과 같다.
flowchart LR
C[Client] --> W[Spring WebFlux]
W --> S[Service]
S --> R[ReactiveMongoRepository]
R --> M[(MongoDB)]
Repository에서 MongoDB 데이터를 조회하면 결과를 List<T>나 Optional<T>처럼 즉시 반환하는 대신 Reactor의 Mono<T>, Flux<T> 형태로 처리할 수 있다.
MongoDB
↓
ReactiveMongoRepository
↓
Mono / Flux
↓
Service
↓
Controller
↓
Client
MongoDB Client의 두 가지 접근 방식
Spring에서 MongoDB를 사용할 때 데이터 접근 방식을 크게 다음처럼 구분할 수 있다.
동기 방식
Spring Data MongoDB
↓
MongoRepository
그리고 Reactive 방식은 다음과 같다.
Reactive 방식
Spring Data Reactive MongoDB
↓
ReactiveMongoRepository
두 방식 모두 MongoDB에 접근하지만 애플리케이션의 처리 모델에서 차이가 있다.
동기 방식
동기 방식에서는 데이터베이스 작업이 완료될 때까지 호출 흐름이 기다리는 형태로 이해할 수 있다.
Request
↓
MongoDB Query
↓
결과 대기
↓
Result
Repository 역시 다음과 같은 형태의 결과를 사용하는 경우가 많다.
List<UserDocument> findAll();
또는:
Optional<UserDocument> findById(String id);
Reactive 방식
Reactive MongoDB에서는 조회 결과가 Reactive Stream으로 반환된다.
여러 건을 조회하면:
Flux<UserDocument>
한 건을 조회하면:
Mono<UserDocument>
형태를 사용할 수 있다.
MongoDB Query
↓
Reactive Stream
↓
Flux / Mono
WebFlux의 요청 처리 모델과 MongoDB 데이터 접근 흐름을 Reactive 방식으로 연결할 수 있다는 것이 핵심이다.
WebFlux와 Reactive MongoDB 조합
WebFlux 애플리케이션의 흐름을 다음과 같이 구성할 수 있다.
WebFlux Controller
↓
Reactive Service
↓
ReactiveMongoRepository
↓
MongoDB
각 계층에서 Reactive 타입을 유지한다.
Repository
→ Flux / Mono
Service
→ Flux / Mono
Controller
→ Flux / Mono
예를 들어 MongoDB에 저장된 전체 데이터를 조회한다면 다음과 같은 흐름이 만들어질 수 있다.
sequenceDiagram
participant C as Client
participant CT as Controller
participant S as Service
participant R as ReactiveMongoRepository
participant DB as MongoDB
C->>CT: GET /users
CT->>S: findAll()
S->>R: findAll()
R->>DB: Query
DB-->>R: Documents
R-->>S: Flux<UserDocument>
S-->>CT: Flux<UserDocument>
CT-->>C: Response
필요한 프로젝트 의존성
WebFlux와 Reactive MongoDB를 연결하기 위해 기본적으로 다음 요소가 필요하다.
Spring Reactive Web
Spring Data Reactive MongoDB
Lombok
각각의 역할은 다음과 같다.
| 의존성 | 역할 |
|---|---|
| Spring WebFlux | Reactive HTTP 요청 및 응답 처리 |
| Reactive MongoDB | MongoDB Reactive 데이터 접근 |
| Lombok | Getter, Setter 등의 반복 코드 감소 |
Gradle 프로젝트라면 다음과 같은 형태로 구성할 수 있다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
Reactive MongoDB 연결에서 핵심적인 의존성은 다음이다.
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
일반 MongoDB와 Reactive MongoDB 의존성 구분
MongoDB 관련 의존성을 선택할 때 동기 방식과 Reactive 방식을 구분해야 한다.
동기 Repository를 사용한다면 일반적인 MongoDB Starter를 사용하는 구조가 있다.
Spring Data MongoDB
↓
MongoRepository
Reactive 방식에서는 다음 구조를 사용한다.
Spring Data Reactive MongoDB
↓
ReactiveMongoRepository
WebFlux 기반 애플리케이션에서 MongoDB 접근까지 Reactive 방식으로 구성하려는 경우 spring-boot-starter-data-mongodb-reactive를 사용하는 형태로 구성할 수 있다.
MongoDB 연결 정보
MongoDB에 연결하려면 일반적으로 다음 정보가 필요하다.
Host
Port
Database
Username
Password
예를 들어 다음 환경을 사용한다고 가정하자.
Host
localhost
Port
27017
Database
testdb
Username
appuser
Password
password
MongoDB URI는 다음과 같은 형태가 될 수 있다.
mongodb://appuser:password@localhost:27017/testdb
Spring Boot 설정에 이 URI를 등록하면 MongoDB Connection 설정에 사용할 수 있다.
application.properties에서 MongoDB 연결하기
단일 MongoDB 연결에서는 설정 파일을 이용할 수 있다.
예를 들어:
spring.data.mongodb.uri=mongodb://appuser:password@localhost:27017/testdb
구조를 나누면 다음과 같다.
mongodb://
↓
username
↓
password
↓
host
↓
port
↓
database
Spring Boot는 설정값을 기반으로 MongoDB 연결에 필요한 구성을 생성한다.
전체 흐름은 다음과 같다.
application.properties
↓
MongoDB Connection 설정
↓
Reactive MongoDB
↓
MongoDB
application.yml로 설정하기
YAML 형식을 사용한다면 다음처럼 작성할 수 있다.
spring:
data:
mongodb:
uri: mongodb://appuser:password@localhost:27017/testdb
Connection URI 하나로 MongoDB 주소와 인증 정보를 구성할 수 있다.
URI 방식의 장점
MongoDB 연결 정보를 URI로 구성하면 연결 정보를 하나의 문자열로 관리할 수 있다.
mongodb://appuser:password@localhost:27017/testdb
이를 구조적으로 보면 다음과 같다.
Protocol
mongodb://
Credential
appuser:password
Server
localhost:27017
Database
testdb
특히 MongoDB의 Connection Option이 추가되는 경우 URI에 Query Parameter를 포함하는 형태로 확장할 수도 있다.
Credential을 소스 코드에 직접 저장하지 않기
로컬 테스트에서는 다음처럼 작성할 수 있다.
spring:
data:
mongodb:
uri: mongodb://appuser:password@localhost:27017/testdb
하지만 운영 환경에서 ID와 Password가 포함된 URI를 Git Repository에 그대로 저장하는 것은 피하는 것이 좋다.
환경 변수로 분리할 수 있다.
spring:
data:
mongodb:
uri: ${MONGODB_URI}
실행 환경에서는 다음 값을 전달한다.
MONGODB_URI=mongodb://appuser:password@mongodb:27017/testdb
즉 다음처럼 책임을 분리한다.
application.yml
→ 환경 변수 이름
Environment / Secret
→ 실제 Credential
MongoDB Collection과 Document
관계형 데이터베이스와 MongoDB는 데이터를 표현하는 방식이 다르다.
RDB에서는 일반적으로 다음 구조를 사용한다.
Database
↓
Table
↓
Row
MongoDB에서는 다음과 같이 생각할 수 있다.
Database
↓
Collection
↓
Document
비교하면 다음과 같다.
| 관계형 데이터베이스 | MongoDB |
|---|---|
| Table | Collection |
| Row | Document |
| Column | Document Field |
| Primary Key | _id |
예를 들어 users Collection에 다음 Document가 존재한다고 하자.
{
"_id": "100",
"name": "Yunsik"
}
Spring 애플리케이션에서는 이 Document와 Mapping되는 Java 클래스를 작성할 수 있다.
Document 클래스 작성
MongoDB Collection과 연결될 객체를 만든다.
예를 들어 다음과 같다.
package com.example.mongo.document;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document(collection = "users")
public class UserDocument {
@Id
private String id;
private String name;
}
여기에서:
@Document(collection = "users")
는 해당 클래스가 MongoDB의 users Collection과 연결된다는 의미다.
그리고:
@Id
private String id;
는 MongoDB Document의 식별자와 연결된다.
구조를 보면 다음과 같다.
MongoDB
users Collection
↓
UserDocument
JPA Entity와 MongoDB Document의 차이
JPA에서는 다음과 같은 형태에 익숙할 수 있다.
@Entity
@Table(name = "users")
public class UserEntity {
}
MongoDB에서는 관계형 테이블이 아니라 Collection과 Document를 사용한다.
따라서 다음과 같은 Mapping을 사용할 수 있다.
@Document(collection = "users")
public class UserDocument {
}
개념 차이를 정리하면 다음과 같다.
JPA
@Entity
@Table
Database Table
MongoDB
@Document
Collection
Document
MongoDB는 JPA 기반 관계형 ORM과는 다른 데이터 모델을 가진다는 점을 구분해야 한다.
ReactiveMongoRepository 작성
Document를 만들었다면 Repository를 정의한다.
Reactive MongoDB에서는 ReactiveMongoRepository를 사용할 수 있다.
package com.example.mongo.repository;
import com.example.mongo.document.UserDocument;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
public interface UserRepository
extends ReactiveMongoRepository<UserDocument, String> {
}
제네릭 타입은 다음 의미를 가진다.
ReactiveMongoRepository
<
UserDocument,
String
>
첫 번째 타입:
UserDocument
는 Repository가 관리할 Document다.
두 번째 타입:
String
은 ID 타입이다.
MongoRepository와 ReactiveMongoRepository 차이
동기 방식에서는 MongoRepository를 사용할 수 있다.
public interface UserRepository
extends MongoRepository<UserDocument, String> {
}
Reactive 방식에서는 다음을 사용한다.
public interface UserRepository
extends ReactiveMongoRepository<UserDocument, String> {
}
반환 타입에서도 차이가 나타난다.
MongoRepository
findAll()
→ List<UserDocument>
ReactiveMongoRepository
findAll()
→ Flux<UserDocument>
단일 Document를 조회하면:
findById()
→ Mono<UserDocument>
형태를 사용할 수 있다.
Flux를 이용한 전체 데이터 조회
MongoDB Collection의 전체 데이터를 조회한다고 가정해보자.
Repository에서는 기본 findAll()을 사용할 수 있다.
Flux<UserDocument> users =
userRepository.findAll();
데이터가 다음과 같이 여러 건이라면:
User 1
User 2
User 3
Reactive Stream은 다음과 같이 생각할 수 있다.
Flux<UserDocument>
User 1
↓
User 2
↓
User 3
↓
Complete
즉 여러 개의 Document를 Reactive Stream으로 전달한다.
Mono를 이용한 단일 데이터 조회
하나의 Document를 ID로 조회한다면 Mono를 사용할 수 있다.
Mono<UserDocument> user =
userRepository.findById("100");
Mono는 다음 상태를 표현할 수 있다.
0개
또는
1개
따라서 단일 Document 조회에 적합하다.
Service 작성
Repository를 Service 계층에서 사용한다.
package com.example.mongo.service;
import com.example.mongo.document.UserDocument;
import com.example.mongo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Flux<UserDocument> findAll() {
return userRepository.findAll();
}
public Mono<UserDocument> findById(String id) {
return userRepository.findById(id);
}
}
Reactive 타입을 그대로 유지한다.
Repository
→ Flux / Mono
Service
→ Flux / Mono
중간에서 일반 Collection으로 변환하고 기다리는 방식을 사용하지 않는 것이 중요하다.
Controller 작성
Controller에서도 Reactive 타입을 반환할 수 있다.
package com.example.mongo.controller;
import com.example.mongo.document.UserDocument;
import com.example.mongo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/users")
public Flux<UserDocument> findAll() {
return userService.findAll();
}
@GetMapping("/users/{id}")
public Mono<UserDocument> findById(
@PathVariable String id
) {
return userService.findById(id);
}
}
전체 조회:
GET /users
↓
Flux<UserDocument>
단건 조회:
GET /users/100
↓
Mono<UserDocument>
전체 데이터 조회 흐름
클라이언트가 다음 요청을 보낸다고 가정한다.
GET /users
전체 흐름은 다음과 같다.
sequenceDiagram
participant C as Client
participant CT as WebFlux Controller
participant S as Service
participant R as ReactiveMongoRepository
participant M as MongoDB
C->>CT: GET /users
CT->>S: findAll()
S->>R: findAll()
R->>M: Query users Collection
M-->>R: Documents
R-->>S: Flux<UserDocument>
S-->>CT: Flux<UserDocument>
CT-->>C: Response
결과를 반환하기까지 Reactive 흐름을 유지한다.
실행
애플리케이션을 실행한다.
./gradlew bootRun
Spring Boot 애플리케이션이 정상적으로 실행되면 MongoDB 연결 설정과 Repository 구성이 정상적으로 처리된 상태에서 API를 호출할 수 있다.
전체 데이터를 조회한다.
curl http://localhost:8080/users
MongoDB의 users Collection에 다음 데이터가 존재한다고 가정한다.
{
"_id": "1",
"name": "apple"
}
{
"_id": "2",
"name": "banana"
}
응답은 다음과 같은 형태로 확인할 수 있다.
[
{
"id": "1",
"name": "apple"
},
{
"id": "2",
"name": "banana"
}
]
단일 데이터도 조회할 수 있다.
curl http://localhost:8080/users/1
ReactiveMongoRepository의 Query Method
ReactiveMongoRepository에서도 Spring Data의 메서드 이름 기반 Query를 사용할 수 있다.
예를 들어 이름으로 데이터를 조회한다고 가정하자.
public interface UserRepository
extends ReactiveMongoRepository<UserDocument, String> {
Flux<UserDocument> findByName(String name);
}
호출하면:
userRepository.findByName("apple");
결과는 Reactive 타입으로 반환된다.
Flux<UserDocument>
이처럼 기본 CRUD뿐 아니라 Query Method도 Reactive Stream 형태로 사용할 수 있다.
데이터 저장
새로운 Document를 저장할 수도 있다.
Service:
public Mono<UserDocument> save(
UserDocument user
) {
return userRepository.save(user);
}
Controller:
@PostMapping("/users")
public Mono<UserDocument> save(
@RequestBody UserDocument user
) {
return userService.save(user);
}
전체 흐름은 다음과 같다.
POST /users
↓
Controller
↓
Service
↓
ReactiveMongoRepository.save()
↓
MongoDB
↓
Mono<UserDocument>
데이터 삭제
ID를 이용해 데이터를 삭제하는 구조도 사용할 수 있다.
public Mono<Void> deleteById(String id) {
return userRepository.deleteById(id);
}
Controller:
@DeleteMapping("/users/{id}")
public Mono<Void> deleteById(
@PathVariable String id
) {
return userService.deleteById(id);
}
삭제는 반환할 Document가 반드시 필요하지 않으므로 Mono<Void> 형태로 표현할 수 있다.
Reactive 흐름에서 block()을 사용하지 않기
ReactiveMongoRepository를 사용하면서 다음과 같이 작성하면 Reactive 흐름이 중간에서 Blocking으로 변경된다.
UserDocument user =
userRepository.findById("100")
.block();
block()은 결과가 올 때까지 호출 Thread를 기다리게 만든다.
WebFlux와 Reactive MongoDB를 사용한다면 가능한 한 Reactive Operator를 이용해 흐름을 이어가는 것이 자연스럽다.
예를 들어 다음과 같이 처리할 수 있다.
public Mono<String> findUserName(
String id
) {
return userRepository.findById(id)
.map(UserDocument::getName);
}
구조는 다음과 같다.
MongoDB
↓
Mono<UserDocument>
↓
map()
↓
Mono<String>
데이터가 존재하지 않는 경우
Reactive MongoDB에서도 단일 조회 결과가 없으면 Empty Stream으로 표현될 수 있다.
Mono.empty()
예를 들어:
userRepository.findById(id)
에서 데이터가 없을 때 Error로 변경하고 싶다면 다음과 같은 Reactive 흐름을 구성할 수 있다.
return userRepository.findById(id)
.switchIfEmpty(
Mono.error(
new IllegalArgumentException(
"user not found"
)
)
);
즉 Reactive Programming에서는 결과 없음도 Stream 상태 중 하나로 다룬다.
MongoDB와 R2DBC의 차이
앞에서 MySQL을 Reactive 방식으로 연결할 때 R2DBC를 사용했다.
MySQL
→ R2DBC
MongoDB에서는 R2DBC를 사용하지 않는다.
MongoDB는 관계형 데이터베이스가 아니기 때문이다.
Reactive MongoDB 연결은 MongoDB를 위한 Reactive Driver와 Spring Data Reactive MongoDB를 이용한다.
따라서 다음처럼 구분해야 한다.
관계형 DB
MySQL
PostgreSQL
↓
R2DBC
반면:
MongoDB
↓
Reactive MongoDB Driver
↓
Spring Data Reactive MongoDB
즉 Reactive = R2DBC라고 이해하면 안 된다.
R2DBC는 Reactive Relational Database Connectivity이기 때문에 관계형 데이터베이스를 위한 기술이다.
MySQL R2DBC와 Reactive MongoDB 비교
| 구분 | MySQL + R2DBC | Reactive MongoDB |
|---|---|---|
| DB 유형 | 관계형 DB | Document DB |
| Reactive 접근 | R2DBC | Mongo Reactive Driver |
| Mapping | @Table | @Document |
| Repository | ReactiveCrudRepository, R2dbcRepository | ReactiveMongoRepository |
| 다건 결과 | Flux<T> | Flux<T> |
| 단건 결과 | Mono<T> | Mono<T> |
상위의 Reactive Programming 모델은 비슷하지만 실제 데이터베이스 Driver와 Mapping 방식은 다르다.
Repository 전체 구조 비교
MySQL R2DBC에서는:
public interface DataRepository
extends ReactiveCrudRepository<DataEntity, Long> {
}
MongoDB에서는:
public interface UserRepository
extends ReactiveMongoRepository<UserDocument, String> {
}
두 Repository 모두 Reactive 타입을 사용한다.
Mono
Flux
따라서 Service와 Controller 계층에서는 유사한 Reactive Pipeline을 구성할 수 있다.
MongoDB Document 설계에서 생각할 점
MongoDB는 관계형 데이터베이스와 데이터 모델이 다르기 때문에 단순히 RDB의 Table 구조를 그대로 Collection으로 복사하는 방식이 항상 적절한 것은 아니다.
예를 들어 RDB에서는 다음과 같이 여러 Table을 JOIN할 수 있다.
users
orders
order_items
MongoDB에서는 Document 내부에 관련 데이터를 Embedding하는 구조도 고려할 수 있다.
{
"_id": "user-1",
"name": "Yunsik",
"addresses": [
{
"city": "Seoul"
}
]
}
따라서 MongoDB를 사용할 때는 단순 연결 방법뿐 아니라 Document 단위의 데이터 접근 패턴도 함께 고려해야 한다.
Reactive MongoDB를 사용하면 무조건 성능이 좋아질까?
Reactive 방식이라는 이유만으로 모든 시스템의 성능이 자동으로 좋아지는 것은 아니다.
다음과 같은 요소가 함께 영향을 준다.
Query 설계
Index
Document 크기
Connection Pool
MongoDB 서버 성능
네트워크
동시 요청량
애플리케이션 로직
Reactive Programming은 높은 I/O 동시성을 효율적으로 처리하기 위한 모델이지 잘못된 Query나 Index 문제까지 해결해주는 기술은 아니다.
예를 들어 Index가 필요한 조회에 Index가 없다면 Reactive MongoDB를 사용하더라도 MongoDB Query 자체가 느릴 수 있다.
MongoDB Index도 중요하다
예를 들어 다음 Query를 자주 수행한다고 가정한다.
findByEmail()
데이터가 수백만 건인데 email에 적절한 Index가 없다면 Collection Scan이 발생할 수 있다.
구조적으로:
ReactiveMongoRepository
↓
MongoDB Query
↓
Index 없음
↓
느린 검색
이 된다.
즉 Reactive Stack을 구축하면서 데이터베이스의 Query와 Index 설계를 별도로 관리해야 한다.
Docker 환경에서 MongoDB 연결
Spring Boot와 MongoDB를 각각 Container로 실행한다면 localhost 사용에 주의해야 한다.
예를 들어 다음 구조가 있다고 하자.
Docker Network
webflux-app
mongodb
애플리케이션 Container에서 다음 주소를 사용하면:
localhost:27017
자기 자신을 의미한다.
Docker Compose의 MongoDB Service 이름이 mongodb라면 다음처럼 구성할 수 있다.
spring:
data:
mongodb:
uri: mongodb://appuser:password@mongodb:27017/testdb
구조는 다음과 같다.
flowchart LR
A[WebFlux Container] --> M[MongoDB Container]
M --> D[(testdb)]
운영 환경에서 연결 상태 확인
애플리케이션 프로세스가 실행되고 있다고 해서 MongoDB까지 정상이라는 의미는 아니다.
Spring Boot
→ UP
MongoDB
→ DOWN
일 수도 있다.
따라서 운영에서는 다음과 같은 항목을 관찰할 필요가 있다.
MongoDB Connection 상태
Query Latency
Error Rate
Connection Pool
MongoDB CPU
MongoDB Memory
Slow Query
애플리케이션과 데이터베이스를 하나의 시스템으로 관찰해야 한다.
Reactive MongoDB에서 Error 처리
MongoDB 연결이 끊기거나 Query가 실패하면 Reactive Stream에 Error Signal이 전달될 수 있다.
onNext
onComplete
onError
예를 들어 다음과 같이 Logging을 연결할 수 있다.
return userRepository.findAll()
.doOnError(error -> {
// Error Logging
});
상황에 따라 다음과 같은 Operator도 사용할 수 있다.
onErrorResume(...)
하지만 데이터베이스 장애를 무조건 빈 데이터로 바꿔버리면 장애를 숨길 수 있기 때문에 Error Handling 정책을 명확하게 설계해야 한다.
ReactiveMongoTemplate이 필요한 경우
단순 CRUD나 Query Method만으로 충분하다면 ReactiveMongoRepository가 편리하다.
ReactiveMongoRepository
→ Repository 추상화
하지만 더 세밀한 MongoDB Query를 직접 구성해야 하는 경우에는 ReactiveMongoTemplate과 같은 API를 사용하는 구조도 고려할 수 있다.
개념적으로 다음과 같이 역할을 나눌 수 있다.
단순 CRUD
메서드 이름 기반 Query
→ ReactiveMongoRepository
복잡한 동적 Query
세부적인 MongoDB Operation
→ ReactiveMongoTemplate
처음부터 모든 작업을 Template으로 작성할 필요는 없고 요구사항에 따라 선택하면 된다.
실무에서 Repository와 Document를 바로 Controller에서 사용해도 될까?
간단한 연결 확인만 필요하다면 Controller에서 Repository를 직접 호출할 수도 있다.
Controller
↓
Repository
하지만 실제 애플리케이션에서는 일반적으로 다음과 같이 계층을 나누는 편이 관리하기 쉽다.
Controller
↓
Service
↓
Repository
그리고 Document를 API 응답으로 그대로 공개하기보다 DTO를 분리할 수도 있다.
MongoDB
↓
UserDocument
↓
Service
↓
UserResponse
↓
Client
예를 들어:
public record UserResponse(
String id,
String name
) {
}
Service에서 변환한다.
public Flux<UserResponse> findAll() {
return userRepository.findAll()
.map(user ->
new UserResponse(
user.getId(),
user.getName()
)
);
}
전체 프로젝트 구조
기본적인 구조는 다음과 같이 구성할 수 있다.
com.example.mongo
├── MongoApplication.java
│
├── controller
│ └── UserController.java
│
├── service
│ └── UserService.java
│
├── repository
│ └── UserRepository.java
│
└── document
└── UserDocument.java
각 계층의 역할은 다음과 같다.
Controller
→ HTTP 요청과 응답
Service
→ 애플리케이션 로직
Repository
→ Reactive MongoDB 접근
Document
→ MongoDB Collection Mapping
전체 코드 예제
Document:
package com.example.mongo.document;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document(collection = "users")
public class UserDocument {
@Id
private String id;
private String name;
}
Repository:
package com.example.mongo.repository;
import com.example.mongo.document.UserDocument;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
public interface UserRepository
extends ReactiveMongoRepository<UserDocument, String> {
}
Service:
package com.example.mongo.service;
import com.example.mongo.document.UserDocument;
import com.example.mongo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Flux<UserDocument> findAll() {
return userRepository.findAll();
}
public Mono<UserDocument> findById(String id) {
return userRepository.findById(id);
}
}
Controller:
package com.example.mongo.controller;
import com.example.mongo.document.UserDocument;
import com.example.mongo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/users")
public Flux<UserDocument> findAll() {
return userService.findAll();
}
@GetMapping("/users/{id}")
public Mono<UserDocument> findById(
@PathVariable String id
) {
return userService.findById(id);
}
}
설정:
spring:
data:
mongodb:
uri: ${MONGODB_URI}
전체 실행 구조
모든 계층을 연결하면 다음과 같다.
flowchart TD
C[Client] --> CT[WebFlux Controller]
CT --> S[Service]
S --> R[ReactiveMongoRepository]
R --> RM[Spring Data Reactive MongoDB]
RM --> DB[(MongoDB)]
DB --> RM
RM --> R
R --> S
S --> CT
CT --> C
Reactive 타입 기준으로 보면 다음과 같다.
MongoDB
↓
ReactiveMongoRepository
↓
Mono / Flux
↓
Service
↓
Mono / Flux
↓
Controller
↓
HTTP Response
WebFlux + R2DBC + Reactive MongoDB 관계
앞의 데이터베이스 구성과 함께 보면 Reactive 데이터 접근 기술을 다음처럼 구분할 수 있다.
Spring WebFlux
↓
관계형 Database
→ R2DBC
MongoDB
→ Reactive MongoDB
두 기술 모두 Reactor의 Mono, Flux와 연결할 수 있지만 하위 Database Driver와 데이터 모델은 서로 다르다.
flowchart TD
W[Spring WebFlux]
W --> R[R2DBC]
W --> M[Reactive MongoDB]
R --> SQL[(MySQL)]
M --> MDB[(MongoDB)]
따라서 Reactive 애플리케이션이라고 해서 모든 데이터베이스를 하나의 데이터 접근 기술로 처리하는 것이 아니라 데이터베이스 종류에 맞는 Reactive Driver와 Spring Data 모듈을 선택해야 한다.
정리
Spring WebFlux에서 MongoDB를 Reactive 방식으로 연결하려면 Spring Data Reactive MongoDB를 사용할 수 있다.
필요한 핵심 의존성은 다음과 같다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
단일 MongoDB Connection은 설정 파일에 URI를 등록하여 구성할 수 있다.
spring:
data:
mongodb:
uri: mongodb://appuser:password@localhost:27017/testdb
MongoDB Collection과 연결할 객체는 @Document를 이용해 정의할 수 있다.
@Document(collection = "users")
public class UserDocument {
@Id
private String id;
private String name;
}
Reactive Repository는 ReactiveMongoRepository를 상속하여 작성한다.
public interface UserRepository
extends ReactiveMongoRepository<UserDocument, String> {
}
여러 데이터 조회는 Flux를 사용한다.
public Flux<UserDocument> findAll() {
return userRepository.findAll();
}
단일 데이터 조회는 Mono를 사용할 수 있다.
public Mono<UserDocument> findById(String id) {
return userRepository.findById(id);
}
전체적으로 다음 Reactive 흐름을 만들 수 있다.
Client
↓
Spring WebFlux
↓
Controller
↓
Service
↓
ReactiveMongoRepository
↓
MongoDB
앞에서 MySQL과 같은 관계형 데이터베이스를 Reactive 방식으로 처리할 때는 R2DBC를 사용했다면, MongoDB에서는 MongoDB 자체의 Reactive 접근 방식과 ReactiveMongoRepository를 사용한다는 차이가 있다.
한 줄 요약
Spring WebFlux에서 MongoDB까지 Reactive 흐름을 유지하려면 Spring Data Reactive MongoDB와 ReactiveMongoRepository를 사용하고, MongoDB Collection을 @Document로 매핑한 뒤 Repository부터 Service와 Controller까지 Mono와 Flux 기반으로 연결하면 된다.