이미지에서 보여지는 구조를 바탕으로 분석한 결과, 이 시스템은 Spring WebFlux를 이용해 비동기적으로 API 서버 및 RDBMS와 통신하는 구조를 나타내고 있습니다. 주요 통신 경로를 살펴보면:
- Reactor-Netty (Async): 클라이언트와 Spring WebFlux 간의 통신은 Reactor-Netty를 통해 비동기적으로 이루어집니다. 이는 WebFlux의 기본 비동기 처리 방식입니다.
- WebClient (Async): Spring WebFlux는 API 서버와 WebClient를 이용해 비동기 통신을 하고 있습니다. 이 역시 논블로킹 방식으로 처리됩니다.
- R2DBC (Async): 관계형 데이터베이스(RDB)와의 통신은 R2DBC를 통해 비동기적으로 처리됩니다. 이를 통해 많은 양의 데이터도 효율적으로 처리할 수 있습니다.
- Redis (Sync): 문제점으로 지적될 수 있는 부분은 Redis와의 통신이 **동기적(sync)**으로 이루어지고 있다는 것입니다. 이 부분이 전체 비동기 구조에서 병목 현상을 일으킬 가능성이 있습니다.
- 응답 반환: 최종적으로 Spring WebFlux는 클라이언트에 비동기적으로 응답을 반환하는 구조입니다.
병목 현상 해결 방안: Reactive Redis 적용
Redis와의 동기 통신이 전체 비동기적 아키텍처의 흐름을 방해할 수 있기 때문에, 이를 Reactive Redis로 전환하여 성능을 최적화할 수 있습니다. Spring Data Redis의 리액티브 지원을 통해 Redis와의 통신을 논블로킹 방식으로 전환하는 것이 중요합니다.
Reactive Redis는 Spring Data Redis와 Spring WebFlux를 결합하여 Redis와 비동기, 논블로킹(non-blocking) 방식으로 상호작용할 수 있도록 지원하는 기술입니다. Redis는 일반적으로 빠른 메모리 기반 데이터 저장소로 사용되지만, Spring WebFlux의 리액티브 모델과 결합하여 더 높은 성능과 확장성을 제공합니다. Reactive Redis는 Spring WebFlux와 함께 비동기 I/O 처리의 장점을 활용할 수 있도록 하며, 이를 통해 더 많은 요청을 효율적으로 처리할 수 있습니다.
주요 특징
- 비동기 I/O:
- Reactive Redis는 I/O 작업을 비동기적으로 처리하여 블로킹 없이 데이터 입출력이 가능합니다. 따라서 성능 저하 없이 많은 요청을 동시에 처리할 수 있습니다.
- 리액티브 프로그래밍 지원:
Flux
와Mono
를 기반으로 Redis와 상호작용하는 리액티브 타입을 사용합니다. 이로 인해 데이터를 스트리밍 방식으로 처리하거나, 단일 결과를 처리할 때 유연하게 대응할 수 있습니다.
- Reactive Streams 기반:
- Reactive Redis는 Reactive Streams 표준을 준수하므로, 다른 리액티브 라이브러리(Spring WebFlux, Reactor 등)와 쉽게 통합할 수 있습니다.
- 레디스의 다양한 데이터 타입 지원:
- Redis의 기본 데이터 타입인
string
,list
,set
,hash
,sorted set
등을 비동기적으로 다룰 수 있으며, Pub/Sub과 같은 기능도 리액티브 방식으로 처리할 수 있습니다.
- Redis의 기본 데이터 타입인
- Pub/Sub 지원:
- Redis의 Pub/Sub 기능을 리액티브 방식으로 사용할 수 있습니다. 메시지 브로커로서의 역할을 하며, 여러 서비스 간 비동기 메시징을 손쉽게 처리할 수 있습니다.
Reactive Redis 사용을 위한 주요 의존성
Reactive Redis를 사용하려면 spring-boot-starter-data-redis-reactive
를 의존성에 추가해야 합니다. Maven에서는 아래와 같이 추가할 수 있습니다:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
기본적인 Reactive Redis 사용 예시
- Redis 구성 설정:
@Configuration
public class RedisConfig {
@Bean
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
return new LettuceConnectionFactory("localhost", 6379); // Lettuce 클라이언트를 사용
}
@Bean
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext(new StringRedisSerializer())
.value(new GenericJackson2JsonRedisSerializer())
.build();
return new ReactiveRedisTemplate<>(factory, serializationContext);
}
}
LettuceConnectionFactory
는 Lettuce 클라이언트를 통해 Redis에 연결하는 설정입니다.ReactiveRedisTemplate
는 비동기 방식으로 Redis 작업을 처리할 수 있는 템플릿입니다.
- 기본적인 CRUD 작업 예시:
@Service
public class RedisService {
private final ReactiveRedisTemplate<String, Object> redisTemplate;
public RedisService(ReactiveRedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public Mono<Boolean> saveValue(String key, String value) {
return redisTemplate.opsForValue().set(key, value);
}
public Mono<String> getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
public Mono<Boolean> deleteValue(String key) {
return redisTemplate.opsForValue().delete(key);
}
}
saveValue
: 주어진key
와value
를 Redis에 저장합니다.Mono<Boolean>
을 반환하여 성공 여부를 알려줍니다.getValue
: 주어진key
에 대한 값을 Redis에서 조회합니다.Mono<String>
을 반환합니다.deleteValue
: 주어진key
에 대한 값을 Redis에서 삭제합니다.
- Pub/Sub 예시:
@Service
public class RedisPubSubService {
private final ReactiveRedisTemplate<String, String> redisTemplate;
public RedisPubSubService(ReactiveRedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public Mono<Long> publishMessage(String channel, String message) {
return redisTemplate.convertAndSend(channel, message); // 메시지 전송
}
public Flux<String> subscribeToChannel(String channel) {
return redisTemplate.listenToChannel(channel) // 채널 구독
.map(message -> message.getMessage());
}
}
publishMessage
: 지정된 채널에 메시지를 보냅니다.subscribeToChannel
: 지정된 채널을 구독하여 들어오는 메시지를 리액티브 방식으로 처리합니다.
Reactive Redis의 장점
- 확장성: 비동기 논블로킹 방식으로 더 많은 연결과 요청을 처리할 수 있어 고성능 애플리케이션에 적합합니다.
- 경량화된 리소스 사용: 비동기 I/O 처리를 통해 리소스를 효율적으로 사용합니다.
- 리액티브 시스템과의 통합: Spring WebFlux와 같은 리액티브 시스템에서 Redis를 사용할 때 더 높은 성능을 기대할 수 있습니다.
Reactive Redis를 사용할 때의 주의사항
- 블로킹 연산 회피: Redis 작업 자체는 비동기적으로 처리되지만, 비동기 메서드 내부에서 블로킹 연산을 사용하면 시스템 성능이 저하될 수 있습니다.
- 적절한 Redis 클라이언트 사용:
Lettuce
나Reactor
기반의 Redis 클라이언트를 사용하는 것이 중요합니다. 이는 비동기 프로그래밍을 자연스럽게 지원하는 클라이언트입니다.
Requirements
도커를 이용하여 redis 서버 구성 및 설치
docker run -it -p 6379:6379 redis:6.2
1:C 07 Oct 2024 10:03:18.400 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 07 Oct 2024 10:03:18.400 # Redis version=6.2.14, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 07 Oct 2024 10:03:18.400 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 07 Oct 2024 10:03:18.400 * monotonic clock: POSIX clock_gettime
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.2.14 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 1
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | https://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
1:M 07 Oct 2024 10:03:18.401 # Server initialized
1:M 07 Oct 2024 10:03:18.403 * Ready to accept connections
- 별도의 터미널을 하나 더 생성하여 레디스의 로그를 볼 수 있도록 monitor를 이용합니다.
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b29f75309e64 redis:6.2 "docker-entrypoint.s…" 7 seconds ago Up 6 seconds 0.0.0.0:6379->6379/tcp happy_proskuriakova
docker exec -it happy_proskuriakova redis-cli PING
PONG
docker exec -it happy_proskuriakova redis-cli monitor
OK
redis-reactive 의존성 라이브러리 설치
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
환경변수 등록
application.yaml로 이동하여 작성합니다.
spring:
data:
redis:
host: 127.0.0.1
port: 6379
스프링 서버가 정상 기동 되는지 기동하여 확인하기~
RedisConfig 설정
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import java.time.Duration;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RedisConfig implements WebFluxConfigurer {
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
// Redis에 PING을 보내서 연결 상태 확인
reactiveRedisTemplate.opsForValue().get("1")
.doOnSuccess(i -> log.info("성공"))
.doOnError((err) -> log.error("실패: {}", err.getMessage()))
.subscribe();
reactiveRedisTemplate.opsForList().leftPush("list1", "hello").subscribe();
reactiveRedisTemplate.opsForValue().set("sampleKey1", "sample" , Duration.ofSeconds(10)).subscribe();
}
}
2024-10-07T21:06:05.412+09:00 INFO 20174 --- [webflux1] [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080
2024-10-07T21:06:05.417+09:00 INFO 20174 --- [webflux1] [ main] c.example.webflux1.Webflux1Application : Started Webflux1Application in 1.374 seconds (process running for 1.594)
2024-10-07T21:06:05.584+09:00 INFO 20174 --- [webflux1] [ioEventLoop-5-1] c.example.webflux1.config.RedisConfig : 성공
redis-cli monitor
위 코드를 작성하고 서버를 다시 기동하여 봅니다. 그리고 redis 컨테이너에서 아래와 같이 redis-cli monitor 로그 기록을 확인 할 수 있습니다.
docker exec -it happy_proskuriakova redis-cli monitor
OK
1728296619.868656 [0 172.17.0.1:59602] "HELLO" "3"
1728296619.881989 [0 172.17.0.1:59602] "GET" "1"
1728302530.128949 [0 172.17.0.1:59604] "HELLO" "3"
1728302530.152002 [0 172.17.0.1:59604] "GET" "1"
1728302732.946106 [0 172.17.0.1:59606] "HELLO" "3"
1728302732.959581 [0 172.17.0.1:59606] "GET" "1"
1728302732.963350 [0 172.17.0.1:59606] "LPUSH" "list1" "hello"
1728302765.573177 [0 172.17.0.1:59608] "HELLO" "3"
1728302765.585918 [0 172.17.0.1:59608] "GET" "1"
1728302765.589436 [0 172.17.0.1:59608] "LPUSH" "list1" "hello"
1728302765.592404 [0 172.17.0.1:59608] "SET" "sampleKey1" "sample" "EX" "10"
도커를 이용하여 MySQL 서버 설치 및 구성과 데이터베이스 구성
- Docker MySQL 컨테이너 실행:결과:컨테이너 상태 확인:결과:
docker ps
docker run --name mysql-r2dbc -e MYSQL_ROOT_PASSWORD=r2dbc -e MYSQL_DATABASE=r2dbc -p 3306:3306 -d mysql:8.0
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 31f05f8bf1bd mysql:8.0 "docker-entrypoint.s…" 6 seconds ago Up 5 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql-r2dbc b29f75309e64 redis:6.2 "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:6379->6379/tcp happy_proskuriakova
- 명령어:
31f05f8bf1bda2b07077a1a4992e7dd2c44707f51198af7a4e7879538cf588ad
- 명령어:
- MySQL 접속:MySQL 로그인 시 비밀번호:로그인 후 MySQL 콘솔에 성공적으로 접속되었음을 확인:
docker exec -it mysql-r2dbc mysql -p
Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 8.0.39 MySQL Community Server - GPL
Enter password: r2dbc
- 명령어:
- 데이터베이스 목록 확인:결과:
r2dbc
데이터베이스가 존재함을 확인. show databases;
+--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | r2dbc | | sys | +--------------------+ 5 rows in set (0.00 sec)
- 명령어:
- scema.sql 파일 생성 및 정의
src/main/resources 경로에 scema.sql 파일을 생성하고 아래와 같이 정의합니다.
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
email VARCHAR(100),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS posts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
title VARCHAR(255),
content TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
CONSTRAINT fk_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
);
그리고 application.yaml 파일에 scema.sql 파일이 읽혀지도록 수정해줍니다.
spring:
sql:
init:
mode: always # 서버가 시작될 때 schema.sql을 실행
- 데이터베이스 전환:결과:
use r2dbc;
Database changed
- 명령어:
- 테이블 확인 (처음에 테이블이 없었음):결과:
show tables;
Empty set (0.00 sec)
- 명령어:
- 테이블 재확인 (테이블 생성 후):결과:
show tables;
+-----------------+ | Tables_in_r2dbc | +-----------------+ | posts | | users | +-----------------+ 2 rows in set (0.00 sec)
- 명령어:
posts
테이블 스키마 확인:결과:desc posts;
+------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint | NO | PRI | NULL | auto_increment | | user_id | bigint | YES | MUL | NULL | | | title | varchar(255) | YES | | NULL | | | content | text | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------+--------------+------+-----+---------+----------------+ 6 rows in set (0.02 sec)
- 명령어:
users
테이블 스키마 확인:결과:desc users;
+------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint | NO | PRI | NULL | auto_increment | | name | varchar(100) | YES | | NULL | | | email | varchar(100) | YES | | NULL | | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | +------------+--------------+------+-----+---------+----------------+ 5 rows in set (0.00 sec)
- 명령어:
요약:
- Docker로 실행한 MySQL 컨테이너에 접속하여
r2dbc
데이터베이스가 생성되었음을 확인. - 테이블이 존재 하지 않으므로 scema.sql파일을 구성하고 기동시 생성 하도록 환경변수를 등록. 이후
posts
와users
테이블이 성공적으로 생성되었음을 확인. - 각 테이블의 스키마는
auto_increment
로 설정된id
필드를 포함하며,posts
테이블은user_id
를 외래키로 참조하고 있습니다.
RedisConfig 수정
reactiveRedisUserTemplate을 커스텀하게 만들어서 사용해보겠습니다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RedisConfig implements WebFluxConfigurer {
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@Bean
public ReactiveRedisTemplate<String, User> reactiveRedisUserTemplate(ReactiveRedisConnectionFactory connectionFactory){
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
Jackson2JsonRedisSerializer<User> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, User.class);
RedisSerializationContext<String, User> serializationContext = RedisSerializationContext
.<String, User>newSerializationContext()
.key(RedisSerializer.string())
.value(jsonSerializer)
.hashKey(RedisSerializer.string())
.hashValue(jsonSerializer)
.build();
return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
}
}
1. 메서드 정의:
@Bean
public ReactiveRedisTemplate<String, User> reactiveRedisUserTemplate(ReactiveRedisConnectionFactory connectionFactory) { ... }
- 이 메서드는
ReactiveRedisTemplate<String, User>
타입의 빈을 생성합니다. 이 템플릿은 Redis에 저장될String
타입의 키와User
객체를 처리하는 데 사용됩니다. @Bean
어노테이션은 Spring 컨테이너에서 이 메서드의 반환값을 빈으로 등록한다는 의미입니다.ReactiveRedisConnectionFactory
는 Redis 연결을 관리하는 팩토리 객체로, 이 템플릿에서 사용됩니다.
2. ObjectMapper 설정:
var objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
ObjectMapper
는 JSON 직렬화 및 역직렬화를 담당하는 Jackson 라이브러리의 핵심 객체입니다.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false
: JSON 역직렬화 시, 매핑되지 않은 추가 필드가 있어도 오류를 발생시키지 않도록 설정합니다.registerModule(new JavaTimeModule())
: Java 8의LocalDateTime
등 시간 관련 클래스의 직렬화 및 역직렬화를 지원하기 위해JavaTimeModule
을 등록합니다.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS)
: 날짜와 시간을 직렬화할 때 타임스탬프 형식이 아닌 ISO 8601 형식(예:2024-01-01T10:00:00
)으로 직렬화하도록 설정합니다.
3. Jackson2JsonRedisSerializer 설정:
Jackson2JsonRedisSerializer<User> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, User.class);
Jackson2JsonRedisSerializer<User>
는User
객체를 JSON 형식으로 직렬화/역직렬화하기 위한 도구입니다.ObjectMapper
를 사용해User
객체를 Redis에 저장할 때 JSON으로 변환하고, 읽어올 때 JSON을User
객체로 변환합니다.
4. RedisSerializationContext 설정:
RedisSerializationContext<String, User> serializationContext = RedisSerializationContext
.<String, User>newSerializationContext()
.key(RedisSerializer.string())
.value(jsonSerializer)
.hashKey(RedisSerializer.string())
.hashValue(jsonSerializer)
.build();
RedisSerializationContext
는 Redis에서 데이터를 직렬화/역직렬화할 때 사용하는 방식과 규칙을 정의합니다..key(RedisSerializer.string())
: Redis에 저장될 키를String
형식으로 직렬화합니다..value(jsonSerializer)
: Redis에 저장될 값을Jackson2JsonRedisSerializer<User>
로 직렬화합니다. 즉,User
객체는 JSON 형식으로 저장됩니다..hashKey(RedisSerializer.string())
: Redis의 해시 자료구조에서 해시의 키도String
형식으로 직렬화합니다..hashValue(jsonSerializer)
: 해시의 값은User
객체를 JSON으로 직렬화합니다.
5. ReactiveRedisTemplate 생성:
return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
ReactiveRedisTemplate<String, User>
는 Redis에 데이터를 읽고 쓰기 위한 템플릿입니다. 이 템플릿은String
타입의 키와User
객체의 값을 처리할 수 있습니다.- Redis와 비동기적으로 통신하며, 리액티브 프로그래밍을 지원합니다. 즉, Redis에서 데이터를 비동기적으로 처리할 수 있는 기능을 제공합니다.
요약:
- 이 코드는
User
객체를 Redis에 JSON 형식으로 저장하고 불러오기 위한 리액티브 템플릿을 설정합니다. ReactiveRedisTemplate<String, User>
는 키를String
으로, 값을User
로 처리할 수 있도록 설정되었고, 이를 통해 Redis에서 비동기적으로 데이터를 저장하고 조회할 수 있습니다.- 이 템플릿은
Jackson2JsonRedisSerializer
를 사용해User
객체를 JSON으로 직렬화하고 역직렬화합니다.
UserService 캐시 설정
findbyId
기존 findById 메서드를 리팩토링합니다.
캐시로 붙여서 사용해봅시다.
- Redis에서
getUserCacheKey(id)
로 값을 조회. - 값이 있으면 즉시 반환.
- 값이 없으면 MySQL에서 조회한 후, 그 값을 Redis에 저장하고 반환.
private static String getUserCacheKey(Long id) {
return "users:%d".formatted(id);
}
public Mono<User> findById(Long id){
return reactiveRedisTemplate.opsForValue()
.get(getUserCacheKey(id)) // 1. Redis 조회
.switchIfEmpty( // 3. 값이 없으면 MySQL DB에서 조회 후 Redis에 저장
userR2dbcRepository.findById(id)
.flatMap(u -> reactiveRedisTemplate.opsForValue()
.set(getUserCacheKey(id), u)
.then(Mono.just(u)) // 4. 저장 후 해당 유저 객체 반환
)
);
}
정상적으로 코드 작성이 되었다면 클라이언트 툴로 테스트 해봅니다. 그렇게 되면 redis-cli monitor에서는 아래와 같이 결과가 출력됩니다.
동일한 쿼리를 여러번 하게 되면 GET 요청을 계속 하게 됩니다.
1728307107.566492 [0 172.17.0.1:59786] "GET" "users:3"
1728307107.616743 [0 172.17.0.1:59786] "SET" "users:3" "{\"id\":3,\"name\":\"greg100\",\"email\":\"greg100@fastcampus.co.kr\",\"createdAt\":[2023,7,8,0,45,9],\"updatedAt\":[2023,7,8,16,47,19]}"
1728307462.544276 [0 172.17.0.1:59786] "GET" "users:3"
1728307463.757065 [0 172.17.0.1:59786] "GET" "users:3"
1728307464.366049 [0 172.17.0.1:59786] "GET" "users:3"
1728307464.737965 [0 172.17.0.1:59786] "GET" "users:3"
update 메소드
수정 전 코드
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()); // 유저를 찾지 못한 경우 Mono.empty() 반환
}
수정 후 코드
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)
.flatMap(updatedUser ->{
return reactiveRedisTemplate
.unlink(getUserCacheKey(id))
.thenReturn(updatedUser); // 저장 후 업데이트된 유저 반환
});
})
.switchIfEmpty(Mono.empty()); // 유저를 찾지 못한 경우 Mono.empty() 반환
}
주요 차이점 및 설명
- 캐시 삭제 로직 추가 (
unlink
)- 수정 후 코드는 DB에서 유저 정보를 업데이트한 후, Redis 캐시에서 해당 유저 정보를 삭제하는 로직이 추가되었습니다.
reactiveRedisTemplate.unlink(getUserCacheKey(id))
을 통해, Redis에서 해당 유저에 대한 캐시 데이터를 비동기적으로 삭제합니다.unlink
: Redis의 비동기 삭제 명령어로, 캐시 키를 즉시 삭제하는 대신, 비동기적으로 백그라운드에서 제거됩니다. 성능에 더 유리할 수 있습니다.
thenReturn
사용unlink
작업이 끝나면,thenReturn(updatedUser)
을 통해 삭제 작업이 완료된 후에 업데이트된 유저 객체를 반환합니다.thenReturn
은 리액티브 스트림에서 어떤 작업(여기서는unlink
)이 끝난 후, 그 결과 대신 특정 값을 반환하게 해주는 역할을 합니다. 이 경우unlink
작업의 결과는 무시하고,updatedUser
객체를 반환합니다.
생소한 문법 설명
1. Mono
와 flatMap
Mono
는 Spring WebFlux나 Reactor에서 사용되는 리액티브 프로그래밍의 기본 단위로, 하나의 비동기적인 값을 처리할 때 사용됩니다.- 예:
Mono<User>
는 비동기적으로 하나의User
객체를 처리합니다.
- 예:
flatMap
은 비동기 작업을 순차적으로 연결하는 데 사용됩니다. 첫 번째 작업이 끝난 후, 두 번째 비동기 작업을 처리할 때 사용합니다.- 예:
findById(id)
로 유저를 비동기적으로 찾은 후, 그 유저 정보를 기반으로 다른 비동기 작업을 하고 싶을 때flatMap
을 사용합니다.
- 예:
2. switchIfEmpty
switchIfEmpty
는Mono
나Flux
가 빈 값일 경우 대체 동작을 정의할 때 사용됩니다.- 예를 들어,
findById(id)
에서 유저를 찾지 못하면Mono.empty()
로 빈 응답을 반환하도록 설정한 부분입니다.
3. unlink
unlink
는 Redis에서 키를 삭제하는 명령어입니다. Redis에서 특정 키에 대한 캐시를 비동기적으로 삭제하는 역할을 합니다.- 기존의
del
과 비슷하지만,unlink
는 비동기적으로 키 삭제 작업을 처리하기 때문에 성능에 더 유리할 수 있습니다.
4. thenReturn
thenReturn(T value)
는 이전 비동기 작업의 결과는 무시하고,value
값을 반환하게 해줍니다. 즉, 캐시 삭제가 끝난 후, 유저 객체를 반환하게 하는 데 사용됩니다.- 예를 들어,
unlink
작업이 끝난 후, 삭제 결과와 상관없이updatedUser
를 반환합니다.
요약
Mono
,flatMap
: 비동기적으로 값을 처리하고, 작업을 연결하는 리액티브 스트림의 기본 문법입니다.switchIfEmpty
: 값이 없을 때 대체 작업을 정의하는데 사용됩니다.unlink
: Redis에서 키를 비동기적으로 삭제하는 명령어입니다.thenReturn
: 이전 작업의 결과를 무시하고, 원하는 값을 반환할 때 사용됩니다.
deleteById 메서드
public Mono<?> deleteById(Long id){
return userR2dbcRepository.deleteById(id)
.then(reactiveRedisTemplate.unlink(getUserCacheKey(id)))
.then(Mono.empty());
}
코드 설명
deleteById
(유저 삭제)userR2dbcRepository.deleteById(id)
는 데이터베이스에서 해당id
에 해당하는 유저를 삭제하는 비동기 작업을 수행합니다.- 이 작업은
Mono<Void>
를 반환하는데, 작업이 성공적으로 완료되면 이후의 작업을 이어서 실행합니다.
unlink
(Redis 캐시 삭제).then(reactiveRedisTemplate.unlink(getUserCacheKey(id)))
는 첫 번째 작업(DB에서 유저 삭제)이 완료되면, 이어서 Redis 캐시에서 해당 유저의 데이터를 삭제하는 비동기 작업을 수행합니다.unlink
는 Redis에서 키를 비동기적으로 삭제하는 명령어로, 주어진getUserCacheKey(id)
를 사용하여 캐시된 유저 데이터를 삭제합니다.
Mono.empty()
반환- 마지막
.then(Mono.empty())
는 최종적으로 빈 응답(Mono.empty()
)을 반환하여, 작업이 완료되었음을 나타냅니다. - 이는 응답으로 특별한 데이터를 보내지 않고, 단지 작업이 성공적으로 완료되었다는 신호를 전달합니다.
- 마지막
전체 흐름
- DB에서 유저 삭제:
userR2dbcRepository.deleteById(id)
로 MySQL이나 R2DBC 기반 데이터베이스에서 해당 유저 데이터를 삭제. - Redis 캐시 삭제:
unlink(getUserCacheKey(id))
로 해당 유저의 캐시 데이터를 Redis에서 삭제. - 빈 응답 반환: 모든 작업이 끝나면
Mono.empty()
를 반환해, 작업이 정상적으로 완료되었음을 나타냄.
간단 요약:
- DB에서 유저 삭제 → Redis 캐시 삭제 → 작업 완료 응답(Mono.empty()).
- 이 코드는 유저와 관련된 데이터를 DB와 캐시 둘 다에서 삭제하는 흐름을 비동기적으로 처리하는 방식입니다.
findById 수정
Duration.ofSeconds(30)
코드를 추가하였습니다. 30초동안 캐시를 보관하고 이후는 캐시를 삭제하도록 설정했습니다.
public Mono<User> findById(Long id){
return reactiveRedisTemplate.opsForValue()
.get(getUserCacheKey(id)) // 1. Redis 조회
.switchIfEmpty( // 3. 값이 없으면 MySQL DB에서 조회 후 Redis에 저장
userR2dbcRepository.findById(id)
.flatMap(u -> reactiveRedisTemplate.opsForValue()
.set(getUserCacheKey(id), u, Duration.ofSeconds(30))
.then(Mono.just(u)) // 4. 저장 후 해당 유저 객체 반환
)
);
}
client tool로 query하게 된다면 redis-cli monitor에서는 아래와 같이 나타나게 될 것입니다.
최초 캐시를 30초동안 생성한다는 query가 보입니다. 30초동안 캐시를 유지하다가 30초 이후 삭제되고 다시 요청하면 캐시를 생성하게 되는 흐름입니다.
1728308468.970243 [0 172.17.0.1:59808] "SET" "users:3" "{\"id\":3,\"name\":\"greg100\",\"email\":\"greg100@fastcampus.co.kr\",\"createdAt\":[2023,7,8,0,45,9],\"updatedAt\":[2023,7,8,16,47,19]}" "EX" "30"
1728308482.126222 [0 172.17.0.1:59808] "GET" "users:3"
1728308482.700058 [0 172.17.0.1:59808] "GET" "users:3"
1728308484.383023 [0 172.17.0.1:59808] "GET" "users:3"
1728308524.138225 [0 172.17.0.1:59808] "GET" "users:3"
1728308524.164335 [0 172.17.0.1:59808] "SET" "users:3" "{\"id\":3,\"name\":\"greg100\",\"email\":\"greg100@fastcampus.co.kr\",\"createdAt\":[2023,7,8,0,45,9],\"updatedAt\":[2023,7,8,16,47,19]}" "EX" "30"
'프레임워크 > 자바 스프링' 카테고리의 다른 글
BlockHound: Java 비동기 애플리케이션에서 블로킹 호출을 감지하는 도구 (6) | 2024.10.08 |
---|---|
Spring MVC와 Spring Webflux 성능비교 (0) | 2024.10.08 |
webflux - R2DBC 실습 (0) | 2024.09.26 |
Spring Webflux 실습 - 2 (0) | 2024.09.26 |
Spring Webflux 실습 - 2 (0) | 2024.09.26 |