
이 이미지에서는 Spring WebFlux를 사용하는 서버가 클라이언트, API 서버, 그리고 관계형 데이터베이스(RDB)와 어떻게 통신하는지 설명하고 있습니다. 각 통신 과정에서 동기와 비동기 방식이 어떻게 적용되는지를 설명하는 흐름도입니다.
1. 클라이언트 → Spring WebFlux (Reactor-Netty)
- 비동기 통신: 클라이언트는
reactor-netty
를 사용하여 Spring WebFlux 서버로 요청을 비동기 방식으로 전송합니다. - Reactor-Netty: Spring WebFlux는 Reactor와 Netty를 기반으로 동작하며, 클라이언트로부터 들어오는 HTTP 요청을 비동기적으로 처리합니다. 클라이언트가 요청을 보내면, WebFlux 서버는 요청을 처리하는 동안 클라이언트의 연결을 유지하지 않고, 처리가 완료되면 결과를 반환합니다.
2. Spring WebFlux → API 서버 (WebClient 비동기)
- WebClient (비동기): WebFlux 서버는 외부 API 서버에 데이터를 요청할 때
WebClient
를 사용합니다.WebClient
는 Spring WebFlux에서 제공하는 비동기 HTTP 클라이언트로, 외부 API와 비동기적으로 통신합니다. 이 과정에서 서버는 블로킹되지 않고 비동기로 응답을 기다리게 됩니다. - 외부 API 서버로부터 응답이 올 때까지 Spring WebFlux는 다른 작업을 계속 처리할 수 있습니다.
3. Spring WebFlux → RDB (동기 JDBC 통신)
- 동기 JDBC 통신: Spring WebFlux 서버가 관계형 데이터베이스(RDB)에 접근할 때는 전통적인 JDBC를 사용합니다. JDBC는 동기 방식으로 동작하기 때문에, 데이터베이스 쿼리가 완료될 때까지 서버의 스레드가 블로킹됩니다.
- 이 단계에서 발생하는 동기적인 블로킹은 전체 비동기 체계에 적합하지 않으며, 이를 개선하기 위해선
R2DBC
와 같은 비동기 데이터베이스 드라이버를 사용하는 것이 좋습니다. 이 다이어그램에서는 동기적인 JDBC 통신으로 인해 성능 저하 또는 블로킹이 발생할 수 있음을 보여줍니다.
4. Spring WebFlux → 클라이언트
- 비동기 응답: Spring WebFlux가 API 서버 또는 RDB로부터 데이터를 처리한 후, 클라이언트에게 비동기적으로 응답을 반환합니다.
- 이 과정에서
reactor-netty
를 사용하여 비동기 방식으로 응답을 클라이언트에게 전달하며, 처리 중에 발생한 I/O 작업이 완료되기 전까지 클라이언트와 연결이 유지되지 않고 효율적인 자원 관리를 할 수 있습니다.
요약
- Spring WebFlux는 클라이언트와의 통신, API 서버와의 통신에서는 비동기적 처리 방식을 사용하지만, JDBC는 동기적 방식으로 RDB에 접근하여 이 부분에서 블로킹이 발생할 수 있습니다.
- WebClient는 비동기적으로 외부 API 서버에 요청을 보내는 데 사용됩니다.
- 성능 문제: 동기 JDBC를 사용하기 때문에 WebFlux의 비동기 처리의 장점이 줄어들 수 있으며, 이러한 동기 처리로 인해 서버의 자원 사용이 최적화되지 않을 수 있습니다.
이를 개선하기 위해서는 비동기 데이터베이스 처리(R2DBC)를 사용하여 완전한 비동기 스트림 기반 서버를 구성하는 것이 좋습니다.
R2DBC
R2DBC (Reactive Relational Database Connectivity)는 관계형 데이터베이스를 비동기적이고 논블로킹 방식으로 처리하기 위한 표준 API입니다. 기존의 JDBC(Java Database Connectivity)가 동기 방식으로 작동하면서 블로킹 I/O를 사용한다면, R2DBC는 비동기 방식으로 관계형 데이터베이스에 접근할 수 있도록 설계되었습니다. 이를 통해 리액티브 프로그래밍 패러다임과 잘 맞는 비동기적 데이터베이스 접근을 가능하게 합니다.
R2DBC의 주요 특징
- 비동기 및 논블로킹 I/O:
- R2DBC는 비동기적으로 데이터베이스와 통신합니다. 이는 데이터를 요청할 때 응답을 기다리면서 스레드가 블로킹되지 않음을 의미합니다. 대신, 요청에 대한 응답이 올 때까지 서버는 다른 작업을 수행할 수 있으며, 응답이 준비되면 비동기적으로 처리됩니다.
- 리액티브 스트림 지원:
- R2DBC는 Reactive Streams API를 기반으로 하여
Publisher
인터페이스를 사용해 비동기적으로 데이터를 전달합니다. 이를 통해Mono
와Flux
와 같은 리액티브 타입을 사용하여 데이터를 다룰 수 있으며, Spring WebFlux와 자연스럽게 통합됩니다.
- R2DBC는 Reactive Streams API를 기반으로 하여
- 효율적인 자원 사용:
- R2DBC는 요청이 발생할 때마다 스레드를 차단하지 않기 때문에 더 적은 스레드로도 많은 요청을 처리할 수 있어, 특히 I/O 바운드 시스템에서는 훨씬 더 효율적입니다.
- 기존 JDBC 기반의 시스템에서는 하나의 스레드가 데이터베이스 작업이 완료될 때까지 대기해야 하지만, R2DBC는 논블로킹 방식으로 이를 처리해 자원의 효율성을 극대화합니다.
- 리액티브 트랜잭션:
- R2DBC는 트랜잭션도 리액티브하게 처리할 수 있는 기능을 지원합니다. 리액티브 트랜잭션 관리는 트랜잭션 경계를 리액티브 타입으로 설정할 수 있어서, 비동기적인 작업에서 트랜잭션의 시작과 종료를 명확하게 할 수 있습니다.
R2DBC의 주요 구성 요소
- ConnectionFactory:
- 데이터베이스와의 연결을 관리하는 객체입니다. 리액티브 스타일로 데이터베이스에 연결할 수 있으며, 이를 통해
Connection
을 생성합니다.
- 데이터베이스와의 연결을 관리하는 객체입니다. 리액티브 스타일로 데이터베이스에 연결할 수 있으며, 이를 통해
- Connection:
- R2DBC에서 데이터베이스와의 연결을 나타냅니다. JDBC와 마찬가지로
Connection
객체를 통해 쿼리를 실행하고 결과를 처리할 수 있지만, 이 과정은 비동기적으로 수행됩니다.
- R2DBC에서 데이터베이스와의 연결을 나타냅니다. JDBC와 마찬가지로
- Statement:
- SQL 쿼리를 실행하는 객체로,
Statement
객체를 통해 SQL 문을 실행하고 결과를 비동기적으로 처리할 수 있습니다. R2DBC는 PreparedStatement와 유사한 바인딩 기능을 제공하여 매개변수를 바인딩할 수 있습니다.
- SQL 쿼리를 실행하는 객체로,
- Result:
- 쿼리 실행 후의 결과를 나타내며, 비동기적으로 결과를 수신합니다. 결과는
Publisher
로 반환되며,Flux
나Mono
를 사용하여 여러 결과나 단일 결과를 처리할 수 있습니다.
- 쿼리 실행 후의 결과를 나타내며, 비동기적으로 결과를 수신합니다. 결과는
R2DBC 사용 예시 (Spring Data R2DBC):
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
// Entity 클래스
public class User {
private Long id;
private String name;
private String email;
// getters and setters
}
// R2DBC Repository 인터페이스
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
// 커스텀 쿼리 사용 가능
@Query("SELECT * FROM users WHERE name = :name")
Flux<User> findByName(String name);
@Query("SELECT * FROM users WHERE email = :email")
Mono<User> findByEmail(String email);
}
위 코드에서, UserRepository
는 ReactiveCrudRepository
를 확장하여 R2DBC 리포지토리로 동작합니다. 이 리포지토리는 리액티브 타입인 Mono
와 Flux
를 사용해 데이터를 비동기적으로 처리합니다.
Spring Data R2DBC 통합
Spring Framework는 Spring Data R2DBC
를 제공하여 R2DBC를 쉽게 사용할 수 있도록 지원합니다. Spring Data R2DBC는 전통적인 Spring Data JPA와 유사한 방식으로 R2DBC를 사용할 수 있게 해주며, 리액티브 리포지토리와 트랜잭션 관리도 지원합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId> <!-- 사용할 RDBMS의 R2DBC 드라이버 -->
</dependency>
R2DBC 사용의 장점
- 높은 확장성:
- 비동기 논블로킹 방식으로 데이터베이스에 접근하므로, 적은 스레드로 더 많은 트래픽을 처리할 수 있어 고성능과 확장성을 제공합니다.
- 효율적인 자원 관리:
- JDBC처럼 스레드를 차단하지 않고, 자원 소비를 최소화하면서도 고속으로 데이터를 처리할 수 있습니다.
- 리액티브 시스템과의 자연스러운 통합:
- Spring WebFlux와 같은 리액티브 시스템과 잘 맞아, 전반적인 애플리케이션이 비동기적으로 동작할 수 있게 해줍니다.
R2DBC의 단점
- 제한된 데이터베이스 지원:
- R2DBC는 아직 모든 데이터베이스에서 지원되는 것은 아닙니다. 현재 MySQL, PostgreSQL, H2, MS SQL Server 등이 지원되며, Oracle과 같은 일부 데이터베이스는 지원이 제한적입니다.
- 성숙도 부족:
- R2DBC는 아직 JDBC만큼 성숙한 기술이 아닙니다. JDBC는 오랫동안 사용된 반면, R2DBC는 비교적 최근에 등장한 기술이므로, JDBC에서 사용할 수 있는 다양한 기능이 아직 완전하지 않을 수 있습니다.
실습
R2DBC를 실습해보겠습니다.
- Docker MySQL DB 구성 및 설정
- Spring WebFlux 서버 구성
mysql docker 설정
docker run --name r2dbc-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=r2dbc -d mysql:8 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
c84a98097ed4ca2a7440dbcd30080cb9f397d46eca2afca286718acecc726f71
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c84a98097ed4 mysql:8 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp r2dbc-mysql
해당 Docker 명령어는 mysql:8
이미지를 기반으로 MySQL 데이터베이스 컨테이너를 실행하는 명령입니다. 명령어를 분석해보면 다음과 같습니다:
명령어
docker run --name r2dbc-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=r2dbc -d mysql:8 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
각 옵션에 대한 설명
docker run
:- 도커 컨테이너를 실행하는 기본 명령입니다.
--name r2dbc-mysql
:- 생성되는 컨테이너의 이름을
r2dbc-mysql
로 지정합니다. 이 이름을 통해 컨테이너를 쉽게 식별할 수 있습니다.
- 생성되는 컨테이너의 이름을
-p 3306:3306
:- 로컬 호스트의 포트 3306을 컨테이너 내의 MySQL의 기본 포트 3306에 매핑합니다. 즉, 호스트의 포트 3306으로 들어오는 요청이 컨테이너 내부의 포트 3306으로 전달됩니다. 이를 통해 외부에서 MySQL 서버에 접근할 수 있습니다.
-e MYSQL_ROOT_PASSWORD=r2dbc
:- 환경 변수를 설정하는 옵션입니다.
MYSQL_ROOT_PASSWORD
환경 변수를 설정하여 MySQL의 루트 계정의 비밀번호를r2dbc
로 지정합니다. MySQL 컨테이너가 시작될 때 이 환경 변수를 사용해 초기 루트 비밀번호가 설정됩니다.
- 환경 변수를 설정하는 옵션입니다.
-d
:- 컨테이너를 백그라운드(detached mode)에서 실행하도록 지정하는 옵션입니다. 이를 사용하면 컨테이너가 백그라운드에서 실행되며, 터미널은 곧바로 명령어 입력을 받게 됩니다.
mysql:8
:- MySQL 버전 8을 기반으로 하는 도커 이미지를 사용합니다.
mysql:8
은 Docker Hub에 등록된 MySQL 버전 8의 공식 이미지입니다.
- MySQL 버전 8을 기반으로 하는 도커 이미지를 사용합니다.
--character-set-server=utf8mb4
:- MySQL의 기본 문자 세트를
utf8mb4
로 설정합니다.utf8mb4
는 유니코드를 지원하는 4바이트 문자 세트로, 이모지 같은 유니코드 문자를 처리할 수 있습니다.
- MySQL의 기본 문자 세트를
--collation-server=utf8mb4_unicode_ci
:- MySQL의 기본 정렬 규칙을
utf8mb4_unicode_ci
로 설정합니다. 이 정렬 방식은 유니코드 규칙에 따라 문자열을 비교하고 정렬합니다.
- MySQL의 기본 정렬 규칙을
docker exec -it r2dbc-mysql mysql -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.4.2 MySQL Community Server - GPL
Copyright (c) 2000, 2024, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use r2dbc;
ERROR 1049 (42000): Unknown database 'r2dbc'
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.02 sec)
mysql> create database r2dbc;
Query OK, 1 row affected (0.02 sec)
mysql> use r2dbc;
Database changed
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| r2dbc |
| sys |
+--------------------+
5 rows in set (0.01 sec)
mysql> show tables;
Empty set (0.01 sec)
mysql>
mysql> create table users
-> (id bigint auto_increment primary key,
-> name varchar(128),
-> email varchar(255),
-> created_at datetime default CURRENT_TIMESTAMP not null,
-> updated_at datetime default CURRENT_TIMESTAMP not null);
Query OK, 0 rows affected (0.04 sec)
mysql> create table posts(
-> id bigint auto_increment primary key,
-> user_id bigint,
-> title varchar(30),
-> content varchar(200),
-> created_at datetime default CURRENT_TIMESTAMP not null,
-> updated_at datetime default CURRENT_TIMESTAMP not null);
Query OK, 0 rows affected (0.05 sec)
mysql> show tables;
+-----------------+
| Tables_in_r2dbc |
+-----------------+
| posts |
| users |
+-----------------+
2 rows in set (0.01 sec)
mysql> create index idx_user_id on posts (user_id);
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> desc posts;
+------------+--------------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+-------------------+-------------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| user_id | bigint | YES | MUL | NULL | |
| title | varchar(30) | YES | | NULL | |
| content | varchar(200) | YES | | NULL | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updated_at | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+------------+--------------+------+-----+-------------------+-------------------+
6 rows in set (0.01 sec)
mysql> desc users;
+------------+--------------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+-------------------+-------------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| name | varchar(128) | YES | | NULL | |
| email | varchar(255) | YES | | NULL | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updated_at | datetime | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+------------+--------------+------+-----+-------------------+-------------------+
5 rows in set (0.00 sec)
요약
r2dbc
데이터베이스 생성:- MySQL에서
r2dbc
라는 새로운 데이터베이스를 생성하고 해당 데이터베이스로 전환.
- MySQL에서
users
테이블 생성:- 필드:
id
,name
,email
,created_at
,updated_at
. id
는 자동 증가(primary key),created_at
과updated_at
필드는 기본값으로CURRENT_TIMESTAMP
사용.
- 필드:
posts
테이블 생성:- 필드:
id
,user_id
,title
,content
,created_at
,updated_at
. user_id
는 사용자와 연결될 수 있는 외부 키로 사용 가능.id
는 자동 증가(primary key).
- 필드:
user_id
에 인덱스 생성:posts
테이블의user_id
필드에 인덱스(idx_user_id
) 추가.
- 테이블 구조 확인:
desc
명령을 사용해users
와posts
테이블의 필드와 데이터 타입 확인.
결과적으로, MySQL에서 r2dbc
데이터베이스를 생성하고, 사용자(users) 및 게시물(posts) 테이블을 성공적으로 구축하였으며, 인덱스까지 추가하는 작업을 수행했습니다.
WAS 서버 환경 구성
의존성 추가
build.gradle 파일의 의존성에 아래 2가지를 명세하여 줍니다.
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'io.asyncer:r2dbc-mysql:1.0.2'
환경변수 추가
spring:
application:
name: webflux1
r2dbc:
url: r2dbc:mysql://localhost:3306/r2dbc
username: root
password: r2dbc
logging:
level:
root: INFO
서버 기동 시 R2DBC로 DB간 통신 접속 확인
이벤트 리스너를 구현하여 WAS에서 R2DBC를 이용하여 서버 기동 시 MySQL 데이터베이스와 연결이 정상적으로 되는지 확인해 보겠습니다.
방법 1과 방법 2의 차이는 Spring에서 이벤트를 처리하는 방식과 이벤트 발생 시점에 있습니다.
방법 1: ApplicationListener 인터페이스 사용
@Component
@RequiredArgsConstructor
public class R2dbcConfig implements ApplicationListener {
private final DatabaseClient databaseClient;
@Override
public void onApplicationEvent(ApplicationEvent event) {
databaseClient.sql("SELECT 1").fetch().one().subscribe(
success -> {
log.info("R2DBC 접속 성공");
},
error -> {
log.info("R2DBC 접속 실패");
}
);
}
}
접속 성공
2024-09-27T15:13:12.240+09:00 INFO 74605 --- [webflux1] [ main] c.example.webflux1.Webflux1Application : Started Webflux1Application in 0.876 seconds (process running for 1.083)
2024-09-27T15:13:12.667+09:00 INFO 74605 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
2024-09-27T15:13:12.673+09:00 INFO 74605 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
2024-09-27T15:13:12.685+09:00 INFO 74605 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
2024-09-27T15:13:12.687+09:00 INFO 74605 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
2024-09-27T15:13:12.689+09:00 INFO 74605 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
2024-09-27T15:13:12.691+09:00 INFO 74605 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
접속 실패
2024-09-27T15:14:07.740+09:00 INFO 74665 --- [webflux1] [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080
2024-09-27T15:14:07.756+09:00 INFO 74665 --- [webflux1] [ main] c.example.webflux1.Webflux1Application : Started Webflux1Application in 0.873 seconds (process running for 1.071)
2024-09-27T15:14:08.106+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-2] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.132+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-4] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.133+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-3] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.157+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-5] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.157+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-6] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.157+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-7] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.183+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-8] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.186+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-1] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.188+09:00 INFO 74665 --- [webflux1] [ctor-tcp-nio-10] c.example.webflux1.config.R2dbcConfig : R2DBC 잡석 실패
2024-09-27T15:14:08.188+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-9] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.188+09:00 WARN 74665 --- [webflux1] [ctor-tcp-nio-10] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.206+09:00 INFO 74665 --- [webflux1] [actor-tcp-nio-3] c.example.webflux1.config.R2dbcConfig : R2DBC 잡석 실패
2024-09-27T15:14:08.206+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-3] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.206+09:00 INFO 74665 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 잡석 실패
2024-09-27T15:14:08.206+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-2] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.207+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-4] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.207+09:00 INFO 74665 --- [webflux1] [actor-tcp-nio-5] c.example.webflux1.config.R2dbcConfig : R2DBC 잡석 실패
2024-09-27T15:14:08.207+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-5] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.228+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-8] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.228+09:00 INFO 74665 --- [webflux1] [actor-tcp-nio-6] c.example.webflux1.config.R2dbcConfig : R2DBC 잡석 실패
2024-09-27T15:14:08.229+09:00 INFO 74665 --- [webflux1] [actor-tcp-nio-7] c.example.webflux1.config.R2dbcConfig : R2DBC 잡석 실패
2024-09-27T15:14:08.229+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-6] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.229+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-7] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.230+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-9] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.242+09:00 WARN 74665 --- [webflux1] [ctor-tcp-nio-10] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.254+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-1] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.267+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-2] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:14:08.280+09:00 WARN 74665 --- [webflux1] [actor-tcp-nio-3] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
설명:
ApplicationListener
인터페이스를 구현함으로써 모든 ApplicationEvent에 대해 이벤트 리스너로 작동하게 됩니다. 즉,ApplicationEvent
가 발생할 때마다onApplicationEvent()
메서드가 호출됩니다.ApplicationEvent
는 애플리케이션의 다양한 이벤트를 포괄하기 때문에, 여러 이벤트가 발생할 수 있고, 그때마다onApplicationEvent()
가 호출되어 R2DBC 접속을 시도할 수 있습니다. 이로 인해 여러 번의 로그가 출력될 수 있습니다.ApplicationListener
는 특정 이벤트에 한정되지 않고 모든 종류의 이벤트를 수신할 수 있기 때문에, 불필요하게 여러 번 호출될 가능성이 높습니다.
방법 2: @EventListener와 ApplicationReadyEvent 사용
@Component
@RequiredArgsConstructor
public class R2dbcConfig {
private final DatabaseClient databaseClient;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
databaseClient.sql("SELECT 1").fetch().one().subscribe(
success -> {
log.info("R2DBC 접속 성공");
},
error -> {
log.info("R2DBC 접속 실패");
}
);
}
}
접속 성공
2024-09-27T15:10:35.593+09:00 INFO 74480 --- [webflux1] [actor-tcp-nio-2] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 성공
접속 실패
2024-09-27T15:11:39.904+09:00 INFO 74532 --- [webflux1] [ main] c.example.webflux1.Webflux1Application : Started Webflux1Application in 0.843 seconds (process running for 1.036)
2024-09-27T15:11:41.099+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-2] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.122+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-4] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.122+09:00 INFO 74532 --- [webflux1] [actor-tcp-nio-3] c.example.webflux1.config.R2dbcConfig : R2DBC 접속 실패
2024-09-27T15:11:41.122+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-3] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.142+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-5] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.157+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-6] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.174+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-7] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.190+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-8] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.209+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-9] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.226+09:00 WARN 74532 --- [webflux1] [ctor-tcp-nio-10] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.244+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-1] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
2024-09-27T15:11:41.257+09:00 WARN 74532 --- [webflux1] [actor-tcp-nio-2] i.a.r.mysql.client.MessageDuplexCodec : Connection has been closed by peer
설명:
1. @EventListener
어노테이션을 사용하여 특정 이벤트에 대해서만 리스너를 설정할 수 있습니다.
2. @EventListener(ApplicationReadyEvent.class)
는 애플리케이션이 완전히 로드된 후에만 실행됩니다. 즉, 이 이벤트는 애플리케이션이 시작된 후 한 번만 발생하므로, R2DBC 접속 시도도 한 번만 실행됩니다.
3. 따라서, ApplicationReadyEvent에 대해서만 반응하게 되어 중복된 실행이 방지됩니다.
차이점 요약:
1. 이벤트 처리 방식:
- 방법 1:
ApplicationListener
인터페이스를 사용하여 모든 ApplicationEvent를 처리합니다. - 방법 2:
@EventListener
를 사용하여 특정 이벤트(ApplicationReadyEvent)에만 반응합니다.
2. 실행 시점:
- 방법 1: 여러 종류의 이벤트마다 호출될 수 있기 때문에 R2DBC 접속 시도가 여러 번 발생할 수 있습니다.
- 방법 2: 애플리케이션이 완전히 준비된 시점인 ApplicationReadyEvent에서 한 번만 실행되므로 중복 실행이 방지됩니다.
3. 복잡성:
- 방법 1은 모든 이벤트에 대해 반응할 수 있어 좀 더 유연하지만, 불필요한 중복 호출이 발생할 수 있습니다.
- 방법 2는 특정 이벤트에만 반응하여 더 직관적이고 중복 로그 방지에 효과적입니다.
결론: 방법 2가 특정 이벤트에만 반응하고 중복 호출을 방지할 수 있어 더 적합한 방식입니다.
EnableR2dbcRepositories
@EnableR2dbcRepositories
는 Spring Data R2DBC를 사용할 때 R2DBC 리포지토리를 활성화하기 위해 사용하는 어노테이션입니다. 이 어노테이션을 통해 Spring Boot 애플리케이션에서 R2DBC 리포지토리 인터페이스를 자동으로 스캔하고 빈으로 등록하게 됩니다.
주요 기능
1. R2DBC 리포지토리 활성화:
- 이 어노테이션은 Spring Data R2DBC 리포지토리 인터페이스를 찾아서 자동으로 구현체를 생성하고 빈으로 등록합니다.
- R2DBC는 비동기적 데이터베이스 연결을 제공하며, 이 어노테이션은 R2DBC 리포지토리 계층을 활성화하여 비동기 방식으로 데이터베이스 작업을 처리할 수 있도록 해줍니다.
2. 베이스 패키지 스캔:
@EnableR2dbcRepositories
는 특정 패키지를 스캔하여 리포지토리 인터페이스를 자동으로 감지하고 활성화합니다. 기본적으로 애플리케이션 클래스가 속한 패키지를 기준으로 리포지토리들을 스캔하지만,basePackages
속성을 사용하여 직접 스캔할 패키지를 지정할 수 있습니다.
3. 리포지토리 인터페이스 감지:
- R2DBC 리포지토리 인터페이스는 일반적으로
ReactiveCrudRepository
를 상속받아 정의되며, 이 어노테이션은 이러한 인터페이스들을 자동으로 감지하고 처리합니다.
사용 예시1
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
@SpringBootApplication
@EnableR2dbcRepositories(basePackages = "com.example.repository")
public class R2dbcApplication {
public static void main(String[] args) {
SpringApplication.run(R2dbcApplication.class, args);
}
}
주요 속성
basePackages
: 리포지토리 인터페이스를 찾을 패키지를 지정합니다. 기본값은 애플리케이션의 루트 패키지입니다.repositoryFactoryBeanClass
: 리포지토리 구현체를 생성하기 위한 팩토리 빈 클래스를 지정할 수 있습니다. 기본적으로는ReactiveRepositoryFactoryBean
을 사용합니다.transactionManagerRef
: 사용할 트랜잭션 관리자의 이름을 지정할 수 있습니다.
기본 동작
Spring Boot에서 @EnableR2dbcRepositories
는 Spring Data JPA에서 사용하는 @EnableJpaRepositories
와 유사하게 동작합니다. Spring Boot는 리포지토리 인터페이스에 대해 자동으로 구현체를 생성해 주고, 이 구현체는 비동기적 R2DBC 기반으로 데이터베이스와 상호작용하게 됩니다.
리포지토리 인터페이스는 일반적으로 ReactiveCrudRepository
나 ReactiveSortingRepository
를 상속받아 정의됩니다.
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import com.example.domain.User;
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
}
위 예시에서 UserRepository
는 R2DBC를 통해 데이터베이스와 비동기적으로 상호작용하는 리포지토리로 동작하며, 이 리포지토리를 @EnableR2dbcRepositories
어노테이션이 활성화하게 됩니다.
여기서는 아래와 같이 R2DBC 리포지토리를 계층을 활성화 시키도록 합니다.
사용 예시2
// 생략
@EnableR2dbcRepositories
public class R2dbcConfig {
// 생략
엔티티 설정
package com.example.webflux1.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table("users")
public class User {
@Id
private Long id;
private String name;
private String email;
@CreatedDate
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
}
// 생략
@EnableR2dbcAuditing
public class R2dbcConfig {
// 생략
1. @EnableR2dbcAuditing
: 엔티티의 생성 및 수정 날짜를 자동으로 기록하는 auditing 기능을 활성화합니다.
2. @CreatedDate
와 @LastModifiedDate
: 엔티티가 생성될 때와 수정될 때 자동으로 날짜/시간을 기록합니다.
3. @Table("users")
와 @Id
: User
엔티티를 users
테이블과 매핑하고, id
필드를 기본 키로 설정합니다.
4. R2DBC 접속 테스트: DatabaseClient
를 사용해 애플리케이션 실행 후 데이터베이스 연결을 테스트합니다.
Spring WebFlux 기반의 사용자 및 게시물 CRUD API 구현
1. 사용자(User) 관리 기능
UserController
, UserService
, 그리고 UserR2dbcRepository
를 통해 사용자 데이터를 처리합니다.
UserController
UserController
는 사용자의 CRUD 요청을 처리하는 엔드포인트를 제공합니다.
- POST /users: 새로운 사용자를 생성합니다.
UserCreateRequest
DTO를 통해 사용자 데이터를 입력받고, 응답으로 생성된 사용자의 정보를 반환합니다. public Mono<UserResponse> create(@RequestBody UserCreateRequest request) { return userSerivce.create(request.getName(), request.getEmail()) .map(UserResponse::of); }
- GET /users: 모든 사용자를 조회합니다.
public Flux<UserResponse> findAllUsers() { return userSerivce.findAll().map(UserResponse::of); }
- GET /users/{id}: 단일 사용자를 조회합니다. 만약 해당 사용자가 없을 경우
404 Not Found
를 반환합니다. public Mono<ResponseEntity<UserResponse>> findSingleUser(@PathVariable(name="id") Long id) { return userSerivce.findById(id) .map(user -> ResponseEntity.ok(UserResponse.of(user))) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); }
- DELETE /users/{id}: 특정 사용자를 삭제합니다. 삭제 성공 시
204 No Content
상태를 반환합니다. public Mono<ResponseEntity<Void>> deleteUser(@PathVariable(name = "id") Long id) { return userSerivce.findById(id) .flatMap(user -> userSerivce.deleteById(id) .then(Mono.just(ResponseEntity.noContent().<Void>build()))) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); }
- PUT /users/{id}: 특정 사용자의 정보를 업데이트합니다. 업데이트 성공 시
200 OK
를 반환하고, 해당 사용자가 없으면404 Not Found
를 반환합니다. public Mono<ResponseEntity<UserResponse>> updateUser( @PathVariable(name="id") Long id, @RequestBody UserUpdateRequest request) { return userSerivce.update(id, request.getName(), request.getEmail()) .map(user -> ResponseEntity.ok(UserResponse.of(user))) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); }
UserService
UserService
는 사용자 데이터를 처리하는 핵심 로직을 담당합니다.
- create(String name, String email): 새로운 사용자를 생성합니다.
public Mono<User> create(String name, String email) { return userR2dbcRepository.save(User.builder().name(name).email(email).build()); }
- findAll(): 모든 사용자 목록을 반환합니다.
public Flux<User> findAll() { return userR2dbcRepository.findAll(); }
- findById(Long id): 특정 사용자를 ID로 조회합니다.
public Mono<User> findById(Long id) { return userR2dbcRepository.findById(id); }
- deleteById(Long id): 특정 사용자를 ID로 삭제합니다.
public Mono<?> deleteById(Long id) { return userR2dbcRepository.deleteById(id); }
- update(Long id, String name, String email): 특정 사용자의 정보를 업데이트합니다.
public Mono<User> update(Long id, String name, String email) { return userR2dbcRepository.findById(id) .flatMap(user -> { user.setName(name); user.setEmail(email); return userR2dbcRepository.save(user); }) .switchIfEmpty(Mono.empty()); }
2. 게시물(Post) 관리 기능
PostControllerV2
, PostServiceV2
, 그리고 PostR2dbcRepository
를 통해 게시물 데이터를 처리합니다.
PostControllerV2
PostControllerV2
는 게시물의 CRUD 요청을 처리하는 엔드포인트를 제공합니다.
- POST /v2/posts: 새로운 게시물을 생성합니다.
public Mono<PostResponseV2> save(@RequestBody PostCreateRequest postCreateRequest) { return postServiceV2.create(postCreateRequest.getUserId(), postCreateRequest.getTitle(), postCreateRequest.getContent()) .map(PostResponseV2::of); }
- GET /v2/posts: 모든 게시물을 조회합니다.
public Flux<PostResponseV2> findAll() { return postServiceV2.findAll().map(PostResponseV2::of); }
- GET /v2/posts/{id}: 특정 게시물을 ID로 조회합니다.
public Mono<ResponseEntity<PostResponseV2>> findById(@PathVariable(name="id") Long id) { return postServiceV2.findById(id) .map(p -> ResponseEntity.ok().body(PostResponseV2.of(p))) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); }
- DELETE /v2/posts/{id}: 특정 게시물을 ID로 삭제합니다.
public Mono<ResponseEntity<?>> deleteById(@PathVariable(name="id") Long id) { return postServiceV2.deleteById(id).then(Mono.just(ResponseEntity.ok().build())); }
- PUT /v2/posts/{id}: 특정 게시물의 정보를 업데이트합니다.
public Mono<ResponseEntity<PostResponseV2>> updateById(@PathVariable(name="id") Long id, @RequestBody PostUpdateRequest postUpdateRequest) { return postServiceV2.updateById(id, postUpdateRequest.getTitle(), postUpdateRequest.getContent()) .map(p -> ResponseEntity.ok().body(PostResponseV2.of(p))) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); }
PostServiceV2
PostServiceV2
는 게시물 데이터를 처리하는 핵심 로직을 담당합니다.
- create(Long userId, String title, String content): 새로운 게시물을 생성합니다.
public Mono<Post> create(Long userId, String title, String content) { return postR2dbcRepository.save(Post.builder().userId(userId).title(title).content(content).build()); }
- findAll(): 모든 게시물을 반환합니다.
public Flux<Post> findAll() { return postR2dbcRepository.findAll(); }
- findById(Long id): 특정 게시물을 ID로 조회합니다.
public Mono<Post> findById(Long id) { return postR2dbcRepository.findById(id); }
- deleteById(Long id): 특정 게시물을 ID로 삭제합니다.
public Mono<Void> deleteById(Long id) { return postR2dbcRepository.deleteById(id); }
- updateById(Long id, String title, String content): 특정 게시물의 정보를 업데이트합니다.
public Mono<Post> updateById(Long id, String title, String content) { return postR2dbcRepository.findById(id) .flatMap(post -> { post.setTitle(title); post.setContent(content); return postR2dbcRepository.save(post); }); }
'프레임워크 > 자바 스프링' 카테고리의 다른 글
Spring MVC와 Spring Webflux 성능비교 (0) | 2024.10.08 |
---|---|
Reactive Redis (1) | 2024.10.07 |
Spring Webflux 실습 - 2 (0) | 2024.09.26 |
Spring Webflux 실습 - 2 (0) | 2024.09.26 |
Spring Webflux 실습 - 1 (1) | 2024.09.25 |