| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- input
- math
- junit
- 정규식
- vscode
- js
- Visual Studio Code
- HashMap
- 스프링부트
- CSS
- 테스트자동화
- Eclipse
- SpringBoot
- ArrayList
- 배열
- java테스트
- junit5
- 자바스크립트
- list
- Array
- 단위테스트
- 자바문법
- string
- html
- 인텔리제이
- 문자열
- 자바
- javascript
- Java
- IntelliJ
- Today
- Total
어제 오늘 내일
[R2DBC] "WebFlux에서 JPA 쓰면 망합니다!" R2DBC와 Reactive DB 연결 본문
WebFlux 서버를 잘 만들어놓고 spring-boot-starter-data-jpa 의존성을 추가하는 순간, 그 서버의 성능은 나락으로 떨어집니다.
왜냐고요? WebFlux는 적은 수의 쓰레드(이벤트 루프)로 돌아가는데, JPA가 쿼리를 날리는 동안 그 소중한 쓰레드를 붙잡고 놔주지 않기(Blocking) 때문입니다.
이벤트 루프가 멈추면 서버 전체가 멈춥니다.
그래서 우리는 R2DBC라는 새로운 친구를 사귀어야 합니다.
1. R2DBC가 뭔가요?
Reactive Relational Database Connectivity의 약자입니다.
MySQL, PostgreSQL, H2 같은 관계형 DB를 논블로킹(Non-Blocking) 방식으로 사용할 수 있게 해주는 표준 API입니다.
- JDBC: 결과가 나올 때까지 기다린다. (
List<User>) - R2DBC: 결과를 나중에 주겠다고 약속한다. (
Flux<User>)
2. 의존성 추가 ()
JPA는 잊으세요. 이제 R2DBC 드라이버가 필요합니다.
dependencies {
// 1. 스프링 데이터 R2DBC
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
// 2. R2DBC용 DB 드라이버 (MySQL 예시)
// implementation 'dev.miku:r2dbc-mysql' // (구버전)
implementation 'com.mysql:mysql-connector-j' // (최신: JDBC 드라이버가 R2DBC도 지원하거나 별도 r2dbc-mysql 사용)
runtimeOnly 'io.asyncer:r2dbc-mysql:1.0.2' // 많이 쓰는 구현체
// H2를 쓴다면
// runtimeOnly 'io.r2dbc:r2dbc-h2'
}
3. 설정 파일 ()
jdbc:가 아니라 r2dbc:로 시작합니다.
spring:
r2dbc:
url: r2dbc:mysql://localhost:3306/mydb
username: root
password: password
pool:
initial-size: 10
max-size: 20
4. 리포지토리 만들기 ()
JpaRepository와 거의 비슷하지만, 리턴 타입이 다릅니다.
// JpaRepository 아님!
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
// 1. 이름으로 찾기 (0~N명)
Flux<User> findByName(String name);
// 2. 이메일로 찾기 (0~1명)
Mono<User> findByEmail(String email);
// 3. 쿼리 직접 짜기 (@Query)
@Query("SELECT * FROM users WHERE age > :age")
Flux<User> findByAgeGreaterThan(int age);
}
save():Mono<User>리턴 (저장된 객체)delete():Mono<Void>리턴 (결과 없음)findById():Mono<User>리턴
5. 엔티티(Entity) 정의하기
JPA의 @Entity와 비슷하지만, 여기선 @Table과 @Id만 씁니다.
(R2DBC는 JPA처럼 복잡한 ORM이 아닙니다. 심플한 매퍼에 가깝습니다.)
@Table("users") // DB 테이블 이름
@Getter
@NoArgsConstructor
public class User {
@Id // PK
private Long id;
private String name;
private String email;
// 생성자 등...
}
6. 서비스에서 사용하기
이제 block() 없이 물 흐르듯 코드를 짜면 됩니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 저장하고 -> 로그 남기고 -> 리턴
public Mono<User> register(User user) {
return userRepository.save(user)
.doOnSuccess(u -> log.info("유저 저장 성공: {}", u.getId()));
}
// 조회해서 -> 이름 대문자로 바꾸기
public Flux<UserDto> getAllUsers() {
return userRepository.findAll()
.map(user -> new UserDto(user.getName().toUpperCase()));
}
}
7. 주의사항: JPA랑 뭐가 다른가요? (치명적 단점)
R2DBC를 쓸 때 가장 당황하는 부분입니다. JPA의 편리한 기능들이 없습니다.
- 지연 로딩(Lazy Loading) 없음:
@OneToMany같은 연관관계 매핑이 안 됩니다. 조인이 필요하면 쿼리를 직접 짜거나, 코드에서 두 번 조회(flatMap)해야 합니다. - 더티 체킹(Dirty Checking) 없음: 객체 값을 바꾼다고 DB가 업데이트되지 않습니다. 반드시
repository.save(user)를 명시적으로 호출해야 합니다. - 1차 캐시 없음: 조회할 때마다 DB에 쿼리가 날아갑니다.
즉, R2DBC는 "고성능"을 위해 "편의성"을 포기한 기술입니다.
마치며
오늘의 결론입니다.
- WebFlux에서는 JPA(Blocking) 대신 R2DBC(Non-Blocking)를 써야 한다.
ReactiveCrudRepository를 쓰면Mono,Flux타입으로 DB를 조회할 수 있다.- 연관관계 매핑이나 더티 체킹 같은 ORM 기능이 부족하므로, 쿼리 중심적인 사고가 필요하다.
이제 DB까지 비동기로 뚫었습니다.
그런데 말이죠, R2DBC에서는 트랜잭션(@Transactional)이 어떻게 동작할까요? 쓰레드가 바뀌면 트랜잭션 컨텍스트도 끊기지 않을까요?
다음 포스팅에서는 "R2DBC 트랜잭션은 어떻게 걸죠?" Reactive TransactionManager와 R2DBC Entity Template에 대해 알아보겠습니다.
'IT > SpringBoot' 카테고리의 다른 글
| [Test] "비동기 코드는 어떻게 테스트해요?" StepVerifier로 스트림 검증하기 (0) | 2026.04.04 |
|---|---|
| [R2DBC] "트랜잭션은 어떻게 걸죠?" Reactive Transactional과 R2dbcEntityTemplate (0) | 2026.04.03 |
| [WebClient] "RestTemplate은 이제 Deprecated!" 비동기 HTTP 요청의 정석, WebClient 사용법 (0) | 2026.04.02 |
| [WebFlux] "컨트롤러가 없다고?" 함수형 엔드포인트(Router & Handler) 작성법 (0) | 2026.04.02 |
| [Spring WebFlux] 어노테이션 기반 컨트롤러: MVC 개발자라면 10분 만에 적응 가능! (0) | 2026.04.01 |
