R2DBC
DB 동기처리의 문제
비동기 어플리케이션에서는 처리 절차 중간에 동기가 포함되면 병목 -> 성능 저하
2번 부분까지 보았을 3번 db 커넥션 부분에서 jdbc 혹은 orm을 사용하면, 1번 2번이 비동기로 처리하더라도 3번을 동기로 처리하면 3번 응답이 올 때까지 해당 스레드는 4번까지 처리를 못하고 기다리게 된다.
즉, 처리 흐름에서 하나라도 동기가 있게 되면 전체적인 성능에 문제
이러한 문제를 해결하기 위해 나온 것이 R2DBC
R2DBC
- 데이터베이스 async 처리
- reactice stream
- nonblocking
- 라이브러리지만 일종의 오픈 스택 -> 벤더 사에서 제공하는 것이 -> 일반적 SPI(Service Provider Interface)
- Spring Webflux 생태계와 어울림
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
- spring.io 에서 제공 안되는 것들도 있기 때문에 오픈소스 라이브러리를 체크해서 반영해야 함
Spring Mvc vs. Spring Webflux
호환이 되는 것을 찾아야 함
둘간의 전환 작업은 간단하지 않다 -> 바꾸게 되면 전부 바꿔야 함
공식 문서
엔티티 설정,
레포지토리 구현,
애노테이션을 사용하여 설정,
-> 스프링 repository와 유사함
-> 쿼리 메서드 예시
예시 코드
@Component
@Slf4j
@RequiredArgsConstructor
@EnableR2dbcRepositories
@EnableR2dbcAuditing
public class R2dbcConfig implements ApplicationListener<ApplicationReadyEvent> {
private final DatabaseClient databaseClient;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// reactor: publisher, subscriber
databaseClient.sql("SELECT 1").fetch().one()
.subscribe(
success -> {
log.info("Initialize r2dbc database connection.");
},
error -> {
log.error("Failed to initialize r2dbc database connection.");
SpringApplication.exit(event.getApplicationContext(), () -> -110);
}
);
}
}
public interface UserR2dbcRepository extends ReactiveCrudRepository<User, Long> {
Flux<User> findByName(String name);
Flux<User> findByNameOrderByIdDesc(String name);
@Modifying
@Query("DELETE FROM users WHERE name = :name")
Mono<Void> deleteByName(String name);
}
Redis
Reactvice Redis
-> 레딩스에 비동기, nonblocking 처리를 위한 라이브러리
-> 레디스 서버와 비동기적인 상호작용을 위해 Redis 서버와 비동기적으로 연결
- 필요성?
- R2DBC와 마찬가지로 전체 요청의 비동기처리 중에 Redis를 동기적으로 처리하면, 이 과정에서 전체 흐름의 병목이 가능하다.
- 대용량 트래픽에서 Webflux를 사용하는 환경이라면 Reactive Redis를 사용할 가능성이 매우 많다.
- Spring Data Reactive Redis -> Lettuce
- Lettuce에서 Netty와 함께 비동기적 처리가 가능하다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
예시 코드
@Configuration
@RequiredArgsConstructor
@Slf4j
public class RedisConfig implements ApplicationListener<ApplicationReadyEvent> {
private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
reactiveRedisTemplate.opsForValue().get("1")
.doOnSuccess(i -> log.info("Initialize to redis connection"))
.doOnError((err) -> log.error("Failed to initialize redis connection: {}", err.getMessage()))
.subscribe();
}
@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);
}
}
Redis에서 먼저 조회하고 없으면 영속성 레벨에서 가져오는 코드
1. 레디스로부터 저장된 키 값 확인
2. 있으면 리턴
3. 없으면 안의 로직을 탐 -> db에서 가져오고 레디스에 세팅하고 다시 모노로 감싸서 전달
public Mono<User> findById(Long id) {
return reactiveRedisTemplate.opsForValue()
.get(getUserCacheKey(id))
.switchIfEmpty(userR2dbcRepository.findById(id)
.flatMap(u -> reactiveRedisTemplate.opsForValue()
.set(getUserCacheKey(id), u, Duration.ofSeconds(30))
.then(Mono.just(u)))
);
}
delete와 update 로직도 레디스에 반영하기
public Mono<Void> deleteById(Long id) {
return userR2dbcRepository.deleteById(id)
.then(reactiveRedisTemplate.unlink(getUserCacheKey(id)))
.then(Mono.empty());
}
public Mono<User> update(Long id, String name, String email) {
return userR2dbcRepository.findById(id)
.flatMap(u -> {
u.setName(name);
u.setEmail(email);
return userR2dbcRepository.save(u);
})
.flatMap(u -> reactiveRedisTemplate.unlink(getUserCacheKey(id)).then(Mono.just(u)));
}
reference :
- fastcampus 시그니처 백엔드 path 초격차 패키지 course 7
- https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/
반응형
'Lecture' 카테고리의 다른 글
webflux 리액티브 프록그래밍에서 블록킹을 디버깅하는 도구 blockhound 간단 사용법 (1) | 2023.10.21 |
---|---|
Jmeter를 이용한 Spring MVC Vs. Webflux 성능 비교 (0) | 2023.10.21 |
객체 필드 중복을 해결하는 다양한 방법들과 객체지향 패러다임을 이용한 순수 코드 방식의 해결법(feat. DDD의 Aggregate와의 연관성) (0) | 2023.07.22 |
RestDocs로 API 문서를 만들어보자 (MockMvc 테스트, 커스터마이징 옵션 설정하기) (0) | 2023.07.13 |
스프링 배치 병렬처리, mock과 static mock, AssertFile을 이용한 배치 로직 테스트 (0) | 2023.07.12 |