본문으로 건너뛰기

SpringBoot-Redis 로 만든 디저트가게 예제

  • SpringBoot 와 Redis를 이용한 디저트가게 예제
  • Redis를 사용하기위해서 디저트가게 예제를 만들어보자
  • A dessert shop example using SpringBoot and Redis
  • Let's create a dessert shop example to learn how to use Redis

목차

Table of Contents

스택

  • SpringBoot
    • Lombok
    • Spring Reactive Web
    • Spring Data Reactive Redis
    • Validation
  • Gradle
  • Java 11 +
  • Redis

Tech Stack

  • SpringBoot
    • Lombok
    • Spring Reactive Web
    • Spring Data Reactive Redis
    • Validation
  • Gradle
  • Java 11 +
  • Redis

레디스서버 띄우기

  • 레디스 서버를 실행시켜야 함
  • 로컬에서 임베드로 생성하거나, 로컬에 Docker로 생성하거나, 클라우드에 설치 된 redis 사용
  • docker-compose를 사용해서 설치 함
    • docker-compose.redis.yml
      version: "3"
      services:
      redis-docker:
      image: redis:latest
      command: redis-server --requirepass qwerqwer123 --port 6379
      container_name: "docker-redis"
      labels:
      - "name=redis"
      - "mode=standalone"
      volumes:
      - /Users/wool/Database-docker/data/redis:/data
      ports:
      - 6379:6379
    • redis에 비밀번호를 적용하지 않는다면 --requirepass 옵션을 빼고 작성
    • redis서버 실행
      $ docker-compose -f docker-compose.redis.yml up -d

Starting the Redis Server

  • You need to run a Redis server
  • You can create an embedded instance locally, create one with Docker locally, or use a Redis instance installed in the cloud
  • We'll install it using docker-compose
    • docker-compose.redis.yml
      version: "3"
      services:
      redis-docker:
      image: redis:latest
      command: redis-server --requirepass qwerqwer123 --port 6379
      container_name: "docker-redis"
      labels:
      - "name=redis"
      - "mode=standalone"
      volumes:
      - /Users/wool/Database-docker/data/redis:/data
      ports:
      - 6379:6379
    • If you don't want to set a password for Redis, remove the --requirepass option
    • Run the Redis server
      $ docker-compose -f docker-compose.redis.yml up -d

Configuration 작성하기

  • ReactiveRedisConfiguration.java
  • Redis와 Spring을 연결하기 위해 설정 파일을 작성
  • reactRedisConnectionFactory 빈을 생성하여 레디스 연결 팩토리를 생성
    @Bean
    public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
    return new LettuceConnectionFactory(
    Objects.requireNonNull(env.getProperty("spring.redis.host")),
    Integer.parseInt(Objects.requireNonNull((env.getProperty("spring.redis.port"))))
    );
    }
    혹은 @Value어노테이션을 사용해서 아래와 같이 데이터를 깔끔하게 정리 할 수 있음
        @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Bean
    public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
    return new LettuceConnectionFactory(redisHost, redisPort);
    }
  • Redis의 데이터를 주고받아야 하기 때문에 JSON형식을 지원하는 Jackson 라이브러리를 사용한 Operation Bean을 세팅한다.
    @Bean
    public ReactiveRedisOperations<String, Object> redisOperations(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
    Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);

    RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder =
    RedisSerializationContext.newSerializationContext(new StringRedisSerializer());

    RedisSerializationContext<String, Object> context = builder.value(serializer).hashValue(serializer)
    .hashKey(serializer).build();

    return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, context);
    }

Writing the Configuration

  • ReactiveRedisConfiguration.java
  • Write a configuration file to connect Redis and Spring
  • Create the reactRedisConnectionFactory bean to establish the Redis connection factory
    @Bean
    public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
    return new LettuceConnectionFactory(
    Objects.requireNonNull(env.getProperty("spring.redis.host")),
    Integer.parseInt(Objects.requireNonNull((env.getProperty("spring.redis.port"))))
    );
    }
    Alternatively, you can use the @Value annotation to organize the data more cleanly as shown below
        @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private int redisPort;

    @Bean
    public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
    return new LettuceConnectionFactory(redisHost, redisPort);
    }
  • Since we need to send and receive data from Redis, we set up an Operation Bean using the Jackson library which supports JSON format.
    @Bean
    public ReactiveRedisOperations<String, Object> redisOperations(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
    Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);

    RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder =
    RedisSerializationContext.newSerializationContext(new StringRedisSerializer());

    RedisSerializationContext<String, Object> context = builder.value(serializer).hashValue(serializer)
    .hashKey(serializer).build();

    return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, context);
    }

CRUD API 작성하기

  • CRUD를 작성하기 위해 우선 모델 클래스가 필요

Domain 모델 작성

  • 도메인 객체 생성하기
    • pk, 제품명, 카테고리, 설명, 가격, 출시일 을 가진 Dessert 도메인객체 생성

Writing the CRUD API

  • To write CRUD operations, we first need a model class

Writing the Domain Model

  • Creating the domain object
    • Create the Dessert domain object with pk, product name, category, description, price, and release date

Database에 접근하는 Repository 작성

  • Redis 데이터베이스와 연결 할 Repository를 작성한다
  • Interface를 사용해서 CRUD 기능을 명시하고, 구현체를 사용해 각 내부 로직을 구현
  • JSON 파싱을 위해서 ObjectMapperUtils 클래스 생성
  • Redis와 연결을 위해서 ReactiveRedisComponent 클래스 생성
  • DessertRepository
    public interface DessertRepository {

    Mono<Dessert> save(Dessert dessert);

    Mono<Dessert> get(String key);

    Flux<Dessert> getAll();

    Mono<Long> delete(String id);
    }
  • RedisDessertRepository
    public class RedisDessertRepository implements DessertRepository {

    private ReactiveRedisOperations<String, Object> redisOperations;

    public RedisDessertRepository(ReactiveRedisOperations<String, Object> redisOperations) {
    this.redisOperations = redisOperations;
    }

    @Override
    public Mono<Dessert> save(Dessert dessert) {
    return redisOperations.opsForValue().set(dessert.getId(), dessert);
    }

    @Override
    public Mono<Dessert> get(String key) {
    return redisOperations.opsForValue().get(key);
    }

    @Override
    public Flux<Dessert> getAll() {
    return redisOperations.keys("*")
    .flatMap(redisOperations::opsForValue::get)
    .cast(Dessert.class);
    }

    @Override
    public Mono<Long> delete(String id) {
    return redisOperations.delete(id);
    }
    }

Writing the Repository for Database Access

  • Write a Repository to connect to the Redis database
  • Use an Interface to define the CRUD functions and implement the internal logic using an implementation class
  • Create the ObjectMapperUtils class for JSON parsing
  • Create the ReactiveRedisComponent class for connecting to Redis
  • DessertRepository
    public interface DessertRepository {

    Mono<Dessert> save(Dessert dessert);

    Mono<Dessert> get(String key);

    Flux<Dessert> getAll();

    Mono<Long> delete(String id);
    }
  • RedisDessertRepository
    public class RedisDessertRepository implements DessertRepository {

    private ReactiveRedisOperations<String, Object> redisOperations;

    public RedisDessertRepository(ReactiveRedisOperations<String, Object> redisOperations) {
    this.redisOperations = redisOperations;
    }

    @Override
    public Mono<Dessert> save(Dessert dessert) {
    return redisOperations.opsForValue().set(dessert.getId(), dessert);
    }

    @Override
    public Mono<Dessert> get(String key) {
    return redisOperations.opsForValue().get(key);
    }

    @Override
    public Flux<Dessert> getAll() {
    return redisOperations.keys("*")
    .flatMap(redisOperations::opsForValue::get)
    .cast(Dessert.class);
    }

    @Override
    public Mono<Long> delete(String id) {
    return redisOperations.delete(id);
    }
    }

Service 작성

  • Interface를 먼저 명시 해 준 후에 구현체로 구현
  • DessertService
    • DessertService
      public interface DessertService {

      Mono<Dessert> create(Dessert dessert);

      Flux<Dessert> getAll();

      Mono<Dessert> getOne(String id);

      Mono<Long> deleteById(String id);

      }
    • DessertServiceImpl
      @RequiredArgsConstructor
      @Service
      public class DessertServiceImpl implements DessertService {

      private final RedisDessertRepository redisDessertRepository;

      @Override
      public Mono<Dessert> create(Dessert dessert) {
      return redisDessertRepository.save(dessert);
      }

      @Override
      public Flux<Dessert> getAll() {
      return redisDessertRepository.getAll();
      }

      @Override
      public Mono<Dessert> getOne(String id) {
      return redisDessertRepository.get(id);
      }

      @Override
      public Mono<Long> deleteById(String id) {
      return redisDessertRepository.delete(id);
      }
      }

Writing the Service

  • First define the Interface, then implement it with a concrete class
  • DessertService
    • DessertService
      public interface DessertService {

      Mono<Dessert> create(Dessert dessert);

      Flux<Dessert> getAll();

      Mono<Dessert> getOne(String id);

      Mono<Long> deleteById(String id);

      }
    • DessertServiceImpl
      @RequiredArgsConstructor
      @Service
      public class DessertServiceImpl implements DessertService {

      private final RedisDessertRepository redisDessertRepository;

      @Override
      public Mono<Dessert> create(Dessert dessert) {
      return redisDessertRepository.save(dessert);
      }

      @Override
      public Flux<Dessert> getAll() {
      return redisDessertRepository.getAll();
      }

      @Override
      public Mono<Dessert> getOne(String id) {
      return redisDessertRepository.get(id);
      }

      @Override
      public Mono<Long> deleteById(String id) {
      return redisDessertRepository.delete(id);
      }
      }

Contoller 생성

  • DessertController를 생성하여 API Call을 Response 할 수 있도록 작성
    @RestController
    @RequestMapping("/v1")
    @RequiredArgsConstructor
    public class DessertController {

    private final DessertServiceImpl dessertService;

    @PostMapping("/dessert")
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Dessert> addDessert(@RequestBody @Valid Dessert dessert) {
    return dessertService.create(dessert);
    }

    @GetMapping("/dessert")
    public Flux<Dessert> getAllDessert() {
    return dessertService.getAll();
    }

    @GetMapping("/dessert/{id}")
    public Mono<Dessert> getDessert(@PathVariable String id) {
    return dessertService.getOne(id);
    }

    @DeleteMapping("/dessert/{id}")
    public Mono<Long> deleteDessert(@PathVariable String id) {
    return dessertService.deleteById(id);
    }

    }

Creating the Controller

  • Create the DessertController to handle API call responses
    @RestController
    @RequestMapping("/v1")
    @RequiredArgsConstructor
    public class DessertController {

    private final DessertServiceImpl dessertService;

    @PostMapping("/dessert")
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Dessert> addDessert(@RequestBody @Valid Dessert dessert) {
    return dessertService.create(dessert);
    }

    @GetMapping("/dessert")
    public Flux<Dessert> getAllDessert() {
    return dessertService.getAll();
    }

    @GetMapping("/dessert/{id}")
    public Mono<Dessert> getDessert(@PathVariable String id) {
    return dessertService.getOne(id);
    }

    @DeleteMapping("/dessert/{id}")
    public Mono<Long> deleteDessert(@PathVariable String id) {
    return dessertService.deleteById(id);
    }

    }