이 가이드는 비차단(non-blocking) Lettuce 드라이버를 사용하여 Redis와 상호 작용하기 위해 Spring Data를 사용하는 기능적(functional) 반응형(reactive) 애플리케이션을 생성하는 과정을 안내합니다.
Reactive Redis는 비동기 및 논블로킹 방식으로 동작합니다. 예를 들어, ReactiveRedisOperations를 사용하여 Redis에 요청을 보내면, 해당 메서드는 호출되고 데이터를 요청한 후 즉시 반환됩니다. 그러나 데이터가 준비될 때까지 다른 작업을 수행할 수 있습니다. 결과는 콜백(callback)이나 리액티브 스트림(Reactive Streams)을 통해 처리됩니다. 이 경우 데이터의 처리와 제어권은 콜백이나 리액티브 스트림을 처리하는 컴포넌트에게 있습니다.
대부분의 일반 Redis 라이브러리는 동기적으로 작동합니다. 따라서 Redis 명령어를 호출하면 해당 명령이 완료될 때까지 대기하며, 결과가 반환될 때까지 다른 작업을 수행할 수 없습니다. 이러한 방식에서는 명령어를 호출한 코드가 결과가 준비될 때까지 제어권을 가지게 됩니다. 그러나 일부 비동기 Redis 라이브러리의 경우, 명령어를 호출하고 결과를 기다리지 않고 다른 작업을 수행할 수 있게 해주며, 결과는 콜백 등을 통해 처리됩니다.
요약하자면, Reactive Redis에서는 데이터 처리 및 제어권이 콜백이나 리액티브 스트림을 처리하는 컴포넌트에게 있으며, 일반 Redis에서는 데이터 처리 및 제어권이 명령어를 호출한 코드에게 있거나, 비동기 라이브러리의 경우 콜백을 통해 처리됩니다.
Redis는 디스크나 메모리와 같은 물리적인 저장 공간에 데이터를 보관하는 인메모리 데이터베이스입니다. 기본적으로 Redis는 메모리에 데이터를 저장하며, 디스크에도 영구적으로 저장할 수 있도록 설정할 수 있습니다.
Redis의 데이터는 키-값(Key-Value) 형태로 저장되며, 각 키에는 해당하는 값이 할당됩니다. Redis는 다양한 데이터 구조를 지원하며, 문자열, 리스트, 해시, 집합, 정렬 집합 등 다양한 데이터 타입을 저장할 수 있습니다.
데이터가 메모리에 저장되므로 Redis는 빠른 읽기와 쓰기 속도를 제공합니다. 그러나 메모리의 용량 한계가 있기 때문에 Redis의 설정에 따라 메모리가 가득 차게 되면 LRU(Least Recently Used) 등의 정책에 따라 데이터를 삭제하거나 디스크에 영구적으로 저장합니다.
따라서 Redis는 디스크에 영속적으로 데이터를 저장할 수 있고, 캐시, 세션 관리, 메시지 브로커 등 다양한 용도로 사용되는데, 이때 디스크 저장 옵션을 설정하면 Redis는 메모리와 디스크 간에 데이터를 보관합니다.
Spring Data Redis 및 Project Reactor를 사용하여 Redis 데이터 저장소와 반응적으로 상호 작용하고 차단(blocking) 없이 Coffee
객체를 저장하고 검색하는 Spring 애플리케이션을 구축합니다. 이 애플리케이션은 Reactive Streams 사양을 기반으로 하는 Reactor의 Publisher
구현, 즉 Mono
(0 또는 1 값을 반환하는 publisher용) 및 Flux
(0~n 값을 반환하는 publisher용)를 사용합니다.
메시징 애플리케이션을 구축하기 전에 메시지 수신 및 전송을 처리할 서버를 설정해야 합니다.
Redis는 메시징 시스템과 함께 제공되는 오픈 소스 BSD 라이선스 키-값 데이터 저장소입니다. 서버는 https://redis.io/download
에서 무료로 사용할 수 있습니다.
설치 과정은 다른 포스트 참고
커피 카탈로그에 보관하려는 커피 유형을 나타내는 클래스를 만듭니다. : src/main/java/com/example/demo/Coffee.java
package com.example.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Coffee {
private String id;
private String name;
}
이 예제에서는 Lombok을 사용하여 생성자 및 소위 "데이터 클래스" 메서드(accessors/mutators,
equals()
,toString()
, &hashCode()
)에 대한 상용구 코드를 제거했습니다.
반응형 Redis 작업을 지원하는 Spring Bean을 포함하는 클래스를 만듭니다. src/main/java/com/example/demo/CoffeeConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class CoffeeConfiguration {
@Bean
ReactiveRedisOperations<String, Coffee> redisOperations(ReactiveRedisConnectionFactory factory) {
Jackson2JsonRedisSerializer<Coffee> serializer = new Jackson2JsonRedisSerializer<>(Coffee.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, Coffee> builder =
RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
RedisSerializationContext<String, Coffee> context = builder.value(serializer).build();
return new ReactiveRedisTemplate<>(factory, context);
}
}
@Configurable
과 @Configuration
차이간단히 말해서, @Configurable
은 객체를 스프링 컨텍스트로 주입하기 위해 사용되는 반면, @Configuration
은 스프링의 설정 정보를 포함하고 있는 클래스임을 선언하는 데 사용됩니다.
@Configurable
:
@Configurable
은 스프링에서 자동 와이어링(의존성 주입)을 허용하기 위해 사용됩니다.EnableLoadTimeWeaving
설정과 함께 사용됩니다.@Configurable
을 붙이고 AspectJ 또는 load-time weaving을 설정하여 해당 클래스의 인스턴스가 스프링 빈처럼 관리되도록 할 수 있습니다.@Configuration
:
@Configuration
은 스프링 설정 클래스를 정의할 때 사용됩니다.@Bean
어노테이션을 사용하여 빈을 생성하고 구성합니다.@ComponentScan
, @Import
, @PropertySource
등 다른 설정 어노테이션과 함께 사용되며, 스프링 애플리케이션 컨텍스트를 초기화하는 데 필요한 설정 정보를 제공합니다.이 코드는 Redis와 상호작용하기 위한 스프링 빈을 구성하는 CoffeeConfiguration
클래스입니다. 여기서 사용된 주요 요소들을 설명해드릴게요.
@Configuration
: 이 어노테이션은 이 클래스가 스프링의 구성 클래스임을 나타냅니다. 따라서 이 클래스에서는 스프링 빈(bean)을 정의하고 구성할 수 있습니다.
@Bean
: 이 어노테이션은 해당 메서드가 빈을 생성하고 스프링 컨테이너에 등록하는 역할을 합니다.
ReactiveRedisOperations<String, Coffee> redisOperations(ReactiveRedisConnectionFactory factory)
: 이 메서드는 Reactive Redis 연산을 수행하기 위한 ReactiveRedisOperations
빈을 생성합니다. 이 빈은 Redis와 상호작용할 수 있는 메서드를 제공합니다. 메서드는 ReactiveRedisConnectionFactory
를 매개변수로 받아와 Redis 연결 팩토리를 주입받습니다.
Jackson2JsonRedisSerializer
: 이 클래스는 Jackson을 사용하여 Java 객체를 JSON 형식으로 직렬화/역직렬화할 수 있는 Redis Serializer입니다. 여기서는 Coffee
객체를 JSON으로 직렬화하는데 사용됩니다.
RedisSerializationContext
: Redis 직렬화 컨텍스트를 구성합니다. StringRedisSerializer
를 키(key)에 사용하고, Jackson2JsonRedisSerializer
를 값(value)에 사용하여 Redis에 저장될 데이터의 직렬화 형식을 설정합니다.
new ReactiveRedisTemplate<>(factory, context)
: ReactiveRedisTemplate
을 생성하고, 위에서 구성한 연결 팩토리와 직렬화 컨텍스트를 사용하여 Redis와 상호작용할 수 있는 Reactive Redis 템플릿을 생성합니다.
이 코드는 Reactive 스타일의 Redis 작업을 위해 Redis 연결을 설정하고, Coffee
객체를 JSON으로 직렬화하여 Redis에 저장하고 검색할 수 있는 기능을 스프링 빈으로 구성하고 있어요.
애플리케이션을 시작할 때 애플리케이션에 대한 샘플 데이터를 로드하기 위해 Spring Bean을 생성합니다.
애플리케이션을 여러 번 (다시) 시작할 수 있으므로 먼저 이전 실행에서 여전히 존재할 수 있는 데이터를 제거해야 합니다. 이는
flashAll()
(Redis) 서버 명령을 사용하여 수행합니다. 기존 데이터를 플러시한 후에는 작은Flux
를 만들고 각 커피 이름을Coffee
객체에 매핑한 다음 반응형 Redis 저장소에 저장합니다. 그런 다음 저장소에서 모든 값을 쿼리하고 표시합니다.
src/main/java/com/example/demo/CoffeeLoader.java
import jakarta.annotation.PostConstruct;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.util.UUID;
@Component
public class CoffeeLoader {
private final ReactiveRedisConnectionFactory factory;
private final ReactiveRedisOperations<String, Coffee> coffeOps;
public CoffeeLoader(ReactiveRedisConnectionFactory factory, ReactiveRedisOperations<String, Coffee> coffeeOps) {
this.factory = factory;
this.coffeOps = coffeeOps;
}
@PostConstruct
public void loadData() {
factory.getReactiveConnection().serverCommands().flushAll().thenMany(
Flux.just("Jet Black Redis", "Darth Redis", "Black Alert Redis")
.map(name -> new Coffee(UUID.randomUUID().toString(), name))
.flatMap(coffee -> coffeOps.opsForValue().set(coffee.getId(), coffee)))
.thenMany(coffeOps.keys("*")
.flatMap(coffeOps.opsForValue()::get))
.subscribe(System.out::println);
}
}
opsFor
메서드opsForValue()
opsForValue()
메서드는 Redis의 Key-Value 구조의 Value에 대한 작업을 수행합니다.ValueOperations
반환set(K key, V value)
: 주어진 키에 값을 설정합니다.get(K key)
: 주어진 키의 값을 가져옵니다.increment(K key)
: 숫자 값을 증가시킵니다.delete(K key)
: 주어진 키와 해당 값을 삭제합니다.opsForList()
opsForList()
메서드는 Redis의 리스트 구조에 대한 작업을 수행합니다.ListOperations
반환leftPush(K key, V value)
: 리스트 왼쪽에 값을 추가합니다.rightPop(K key)
: 리스트 오른쪽에서 값을 꺼냅니다.size(K key)
: 리스트의 크기를 가져옵니다.trim(K key, long start, long end)
: 리스트를 잘라서 원하는 부분만 남깁니다.opsForSet()
opsForSet()
메서드는 Redis의 집합 구조에 대한 작업을 수행합니다.SetOperations
반환add(K key, V... values)
: 집합에 값을 추가합니다.members(K key)
: 집합의 모든 멤버를 가져옵니다.size(K key)
: 집합의 크기를 가져옵니다.remove(K key, V... values)
: 집합에서 값을 제거합니다.opsForHash()
opsForHash()
메서드는 Redis의 해시 구조에 대한 작업을 수행합니다.HashOperations
반환put(K key, HK hashKey, HV value)
: 해시에 값을 추가합니다.get(K key, Object hashKey)
: 해시에서 특정 키의 값을 가져옵니다.delete(K key, Object... hashKeys)
: 해시에서 키와 해당 값을 제거합니다.Flux.just
:
Flux.just
는 주어진 값들을 이용하여 Flux를 생성하는 메서드입니다.Flux.just(1, 2, 3)
은 1, 2, 3의 값을 차례대로 내보내는 Flux를 생성합니다. Flux<Integer> numbers = Flux.just(1, 2, 3, 4, 5);
thenMany
:
thenMany
는 이전 작업이 완료된 후에 다른 Flux나 Mono를 실행하는데 사용됩니다.thenMany
는 Mono나 Flux에서 사용할 수 있으며, 다른 Flux나 Mono를 반환하거나 연결하는데 사용됩니다.Flux<Integer> flux1 = Flux.just(1, 2, 3);
Flux<Integer> flux2 = Flux.just(4, 5, 6);
flux1.thenMany(flux2).subscribe(System.out::println);
// flux1이 완료된 후 flux2의 값을 차례로 출력합니다.
map과 flatMap의 차이:
System.out::println: 이것은 메서드 레퍼런스(Method Reference)입니다. Java 8부터 도입된 기능으로, 람다식으로 표현된 메서드를 축약해서 표현할 수 있는 방법 중 하나입니다. System.out.println()
을 사용하여 콘솔에 출력하는 메서드를 가리킵니다. ::
은 메서드 레퍼런스를 사용할 때의 구분자입니다. System.out::println
은 메서드 레퍼런스로, println()
메서드를 가리킵니다. 이것을 subscribe 시 사용하면 각 요소를 println
메서드에 전달하여 출력합니다.
이 코드는 CoffeeLoader
라는 Spring @Component
입니다. 클래스가 생성되고, 모든 의존성이 주입된 후에 @PostConstruct
가 붙은 메서드가 실행됩니다. 따라서 CoffeeLoader
클래스가 생성된 후에 @PostConstruct
가 달린 loadData()
메서드가 실행되어 Redis 데이터를 초기화하고 작업을 수행하게 됩니다
ReactiveRedisConnectionFactory
와 ReactiveRedisOperations
를 필드로 주입받습니다. 이는 Reactive Redis에 연결하고 데이터를 처리하기 위한 연결과 작업을 수행할 수 있게 합니다.
loadData()
메서드는 애플리케이션이 시작될 때 실행됩니다. 이 메서드는 Redis의 데이터를 모두 지우고(flushAll()
) 새로운 데이터를 저장합니다.
Flux.just("Jet Black Redis", "Darth Redis", "Black Alert Redis")
는 세 가지 커피 종류를 Flux로 생성합니다.
map(name -> new Coffee(UUID.randomUUID().toString(), name))
는 각 커피 종류마다 UUID와 이름을 가진 Coffee 객체를 생성합니다.
flatMap(coffee -> coffeOps.opsForValue().set(coffee.getId(), coffee))
는 생성된 Coffee 객체를 Redis에 저장합니다. opsForValue().set()
을 사용하여 각 Coffee 객체를 Redis의 Key-Value 형태로 저장합니다.
coffeOps.keys("*").flatMap(coffeOps.opsForValue()::get)
는 Redis에 저장된 모든 데이터를 가져옵니다. keys("*")
로 모든 키를 가져온 뒤, 각 키에 대해 opsForValue().get()
을 통해 해당 값을 가져옵니다.
subscribe(System.out::println)
은 가져온 데이터를 출력합니다. 즉, Redis에 저장된 모든 Coffee 객체를 가져와서 출력합니다.
애플리케이션에 외부 인터페이스를 제공하기 위해 RestController
를 만듭니다.
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class CoffeeController {
private final ReactiveRedisOperations<String, Coffee> coffeeOps;
CoffeeController(ReactiveRedisOperations<String, Coffee> coffeeOps) {
this.coffeeOps = coffeeOps;
}
@GetMapping("/coffees")
public Flux<Coffee> all() {
return coffeeOps.keys("*").flatMap(coffeeOps.opsForValue()::get)
}
}
flatMap
에서 메서드 레퍼런스 대신 람다 표현식을 사용하여 같은 코드를 표현하려면 아래와 같이 할 수 있습니다.
@GetMapping("/coffees")
public Flux<Coffee> all() {
return coffeeOps.keys("*").flatMap(key -> coffeeOps.opsForValue().get(key));
}
외부 애플리케이션 서버에 배포하기 위해 이 서비스를 기존 WAR 파일로 패키징할 수 있지만 여기에 표시된 더 간단한 접근 방식을 사용하면 독립 실행형(single executable) 애플리케이션을 만들 수 있습니다. 좋은 오래된 Java main()
메소드에 의해 구동되는 단일 실행 가능한 JAR
파일에 모든 것을 패키지합니다. 그 과정에서 외부 인스턴스에 배포하는 대신 Netty를 HTTP 런타임으로 비동기 "컨테이너(container)"에 내장(embedding)하기 위한 Spring의 지원을 사용합니다.
src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Netty:
HTTP 런타임으로 내장:
비동기 '컨테이너(container)':
요약하면, "Netty를 HTTP 런타임으로 비동기 '컨테이너(container)'에 내장"이라는 말은 Netty가 HTTP 프로토콜을 처리하는 비동기적인 환경(컨테이너)에서 동작한다는 것을 의미합니다. Netty를 사용하면 비동기적이고 효율적인 방식으로 HTTP 요청을 처리할 수 있는 환경을 제공할 수 있습니다.
런타임 환경(Runtime Environment):
컨테이너(Container):
스프링 컨테이너(Spring Container):
스프링 컨텍스트(Spring Context):
3-way handshake는 TCP 연결을 설정하는 과정으로, 일반적으로 동기적이고 블로킹되는 작업입니다.
3-way handshake는 TCP 연결을 초기화하기 위해 클라이언트가 서버에게 SYN 패킷을 보내고, 서버가 이에 응답하여 SYN-ACK 패킷을 보내고, 마지막으로 클라이언트가 ACK 패킷을 보내는 과정입니다. 이러한 과정은 일반적으로 동기적이며, 연결이 설정될 때까지 기다리며 블로킹될 수 있습니다.
이 과정은 TCP/IP 프로토콜 스택에서 일어나며, 동기/블로킹 방식으로 작동합니다. 이와 관련하여 비동기적이거나 논블로킹인 HTTP와는 직접적인 연관성은 없습니다. HTTP는 TCP/IP 위에서 동작하며, TCP 연결 설정을 위한 3-way handshake는 HTTP 요청/응답과는 별개로 TCP/IP 수준에서 처리됩니다.
:: Spring Boot :: (v3.2.1)
2024-01-04T13:45:09.770+09:00 INFO 11958 --- [ main] g.s.SpringDataReactiveRedisApplication : Starting SpringDataReactiveRedisApplication using Java 17.0.9 with PID 11958 (/home/dev-hammy/IdeaProjects/SpringBoot_Guides/spring-data-reactive-redis/build/classes/java/main started by dev-hammy in /home/dev-hammy/IdeaProjects/SpringBoot_Guides/spring-data-reactive-redis)
2024-01-04T13:45:09.773+09:00 INFO 11958 --- [ main] g.s.SpringDataReactiveRedisApplication : No active profile set, falling back to 1 default profile: "default"
2024-01-04T13:45:10.532+09:00 INFO 11958 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
2024-01-04T13:45:10.534+09:00 INFO 11958 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
2024-01-04T13:45:10.560+09:00 INFO 11958 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 11 ms. Found 0 Redis repository interfaces.
Coffee(id=d2b4ea7f-b872-4c31-b602-0771e2d86cb5, name=Black Alert Redis)
Coffee(id=4439dd73-919c-4f56-8aee-c6df817d5282, name=Jet Black Redis)
Coffee(id=66ddf2e9-f094-4931-861b-64214fba032d, name=Darth Redis)
2024-01-04T13:45:12.182+09:00 INFO 11958 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080
2024-01-04T13:45:12.195+09:00 INFO 11958 --- [ main] g.s.SpringDataReactiveRedisApplication : Started SpringDataReactiveRedisApplication in 3.028 seconds (process running for 4.32)