Spring Boot로 Redis client 구현해보자

Karim·2025년 7월 30일
2

SpringBoot

목록 보기
17/17
post-thumbnail

1. Version

💬

  • jdk : 21
  • Spring boot : 3.5.4

2. build.gradle

📓 build.gredle dependencies

// Redis에 HTTP 세션 데이터를 저장하여 세션 클러스터링 및 영속화 지원
implementation 'org.springframework.session:spring-session-data-redis'
// Spring Data Redis를 통해 Redis 데이터베이스와 상호작용하는 기본 기능 제공
implementation 'org.springframework.boot:spring-boot-starter-data-redis' 

3. application.yml

✒️

spring:
  data:
    redis:
      host: redis_server
      port: redis_port
      password: # 비밀번호가 있다면 여기에 입력
      timeout: 5000ms
      lettuce:
        pool:
            max-active: 8    # 풀에서 유지할 최대 활성(사용 중이거나 사용 가능한) 연결 수
            max-idle: 8      # 풀에서 유지할 최대 유휴(사용 가능) 연결 수
            min-idle: 0      # 풀에서 유지할 최소 유휴(사용 가능) 연결 수
            max-wait: -1ms   # 풀에서 연결을 얻기 위해 대기할 최대 시간 (음수 값은 무한대 대기를 의미)

4. project 구조 및 설명

🧩

📦 main
 ┗ 📂 java
    ┗ 📂 com.demo.redis_spring
       ┣ 📂 config
       ┃  ┗ ⚙️ RedisConfig
       ┃     - Redis 연결 설정을 담당하는 클래스입니다. Redis 서버의 호스트, 포트, 비밀번호, 연결 풀 설정 등을 정의하며, RedisTemplate 빈을 생성합니다.
       ┣ 📂 controller
       ┃  ┗ ⚙️ RedisCommonController
       ┃     - 모든 Redis 자료구조(String, List, Set, ZSet, Hash)에 대한 공통적인 CRUD(생성, 조회, 삭제, 존재 확인) 작업을 처리하는 REST API 엔드포인트를 제공합니다.
       ┣ 📂 exception
       ┃  ┗ 🚫 GlobalExceptionHandler
       ┃     - 애플리케이션 전반에서 발생하는 예외를 중앙에서 처리하는 클래스입니다. 특히 Redis 관련 예외(WRONGTYPE 등)를 포함하여 클라이언트에게 친화적인 에러 응답을 제공합니다.
       ┣ 📂 model
       ┃  ┣ 🎛️ RedisDataType
       ┃  ┃  - Redis 데이터 타입(STRING, LIST, SET, ZSET, HASH)을 정의하는 Enum 클래스입니다. 각 타입에 해당하는 서비스 빈 이름을 포함하여 디스패처 서비스에서 활용됩니다.
       ┃  ┗ 🧩 RedisRequest
       ┃     - 클라이언트로부터 Redis 작업 요청을 받을 때 사용되는 DTO(Data Transfer Object)입니다. 키,, 만료 시간, 멤버, 스코어, 필드 등 다양한 Redis 작업에 필요한 파라미터를 캡슐화합니다.
       ┣ 📂 service
       ┃  ┣ 🗄️ RedisDispatcherService
       ┃  ┃  - 클라이언트 요청의 Redis 데이터 타입에 따라 적절한 RedisOperationService 구현체로 작업을 위임하는 핵심 디스패처 서비스입니다. 각 자료구조별 서비스의 고유 메서드에 대한 직접 접근도 제공합니다.
       ┃  ┗ 🗃️ RedisOperationService
       ┃     - Redis String, List, Set, ZSet, Hash 등 모든 Redis 자료구조 서비스 구현체들이 공통적으로 구현해야 할 기본 Redis 작업(setData, getData, deleteData, hasKey)을 정의하는 인터페이스입니다.
       ┗ 📂 type
          ┣ 📂 hash
          ┃  ┣ 📂 controller
          ┃  ┃  ┗ ⚙️ RedisHashController
          ┃  ┃     - Redis Hash 타입 데이터에 특화된 웹 요청을 처리하는 REST API 엔드포인트를 제공합니다. Hash의 특정 필드 저장/조회, 모든 필드 조회, 필드 삭제 등의 고유 작업을 담당합니다.
          ┃  ┗ 📂 service
          ┃     ┗ 🗄️ RedisHashServiceImpl
          ┃        - Redis Hash 자료구조에 대한 실제 데이터베이스 연산(필드 저장, 조회, 삭제 등)을 구현하는 서비스 클래스입니다. RedisOperationService 인터페이스를 구현합니다.
          ┣ 📂 list
          ┃  ┣ 📂 controller
          ┃  ┃  ┗ ⚙️ RedisListController
          ┃  ┃     - Redis List 타입 데이터에 특화된 웹 요청을 처리하는 REST API 엔드포인트를 제공합니다. 리스트의 양 끝에 요소 추가/제거, 범위 조회 등의 고유 작업을 담당합니다.
          ┃  ┗ 📂 service
          ┃     ┗ 🗄️ RedisListServiceImpl
          ┃        - Redis List 자료구조에 대한 실제 데이터베이스 연산(요소 추가, 조회, 제거 등)을 구현하는 서비스 클래스입니다. RedisOperationService 인터페이스를 구현합니다.
          ┣ 📂 set
          ┃  ┣ 📂 controller
          ┃  ┃  ┗ ⚙️ RedisSetController
          ┃  ┃     - Redis Set 타입 데이터에 특화된 웹 요청을 처리하는 REST API 엔드포인트를 제공합니다. 멤버 추가/제거, 모든 멤버 조회, 멤버 존재 여부 확인 등의 고유 작업을 담당합니다.
          ┃  ┗ 📂 service
          ┃     ┗ 🗄️ RedisSetServiceImpl
          ┃        - Redis Set 자료구조에 대한 실제 데이터베이스 연산(멤버 추가, 조회, 제거 등)을 구현하는 서비스 클래스입니다. RedisOperationService 인터페이스를 구현합니다.
          ┣ 📂 string
          ┃  ┣ 📂 controller
          ┃  ┃  ┗ ⚙️ RedisStringController
          ┃  ┃     - Redis String 타입 데이터에 특화된 웹 요청을 처리하는 REST API 엔드포인트를 제공합니다. (예: 값 증가/감소 등). 공통 CRUD는 RedisCommonController에서 처리합니다.
          ┃  ┗ 📂 service
          ┃     ┗ 🗄️ RedisStringServiceImpl
          ┃        - Redis String 자료구조에 대한 실제 데이터베이스 연산(값 설정, 조회 등)을 구현하는 서비스 클래스입니다. RedisOperationService 인터페이스를 구현합니다.
          ┗ 📂 zset
             ┣ 📂 controller
             ┃  ┗ ⚙️ RedisZSetController
             ┃     - Redis Sorted Set (ZSet) 타입 데이터에 특화된 웹 요청을 처리하는 REST API 엔드포인트를 제공합니다. 멤버와 스코어 추가/조회, 스코어 범위 조회 등의 고유 작업을 담당합니다.
             ┗ 📂 service
                ┗ 🗄️ RedisZSetServiceImpl
                   - Redis Sorted Set (ZSet) 자료구조에 대한 실제 데이터베이스 연산(멤버와 스코어 추가, 조회, 제거 등)을 구현하는 서비스 클래스입니다. RedisOperationService 인터페이스를 구현합니다.

5. 주요 class

✒️ RedisConfig

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        // Key 직렬화 (String)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // Value 직렬화 (JSON)
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // Hash Key 직렬화 (String)
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // Hash Value 직렬화 (JSON)
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet(); // 직렬화 설정을 적용

        return redisTemplate;
    }
}

✒️ RedisRequest

/**
 * Redis 작업 요청을 위한 DTO
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RedisRequest implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    private String type;             // Redis 데이터 타입 (예: "STRING", "LIST", "SET", "ZSET", "HASH")
    private String key;              // Redis 키
    private Object value;            // 저장할 값 (String, JSON 객체 등, 단일 값 또는 HASH/ZSET의 다중 값 Map)
    private int expirationSeconds;   // 만료 시간 (초, 0 이하는 만료 없음)
    private Object member;           // SET/ZSET 작업 시 단일 멤버
    private double score;            // ZSET 작업 시 스코어
    private double maxScore;         // ZSET 범위 조회 시 최대 스코어
    private double minScore;         // ZSET 범위 조회 시 최소 스코어
    private String field;            // HASH 작업 시 필드
}

✒️ RedisCommonController

/**
 * 모든 Redis 데이터 타입에 대한 공통 웹 요청(생성, 조회, 삭제, 존재 확인)을 처리하는 컨트롤러입니다.
 * RedisRequest DTO의 'type' 필드를 사용하여 적절한 서비스로 작업을 위임합니다.
 */
@RestController
@RequestMapping("/redis") // 최상위 /redis 경로를 사용하여 범용 엔드포인트를 제공
public class RedisCommonController {

    private final RedisDispatcherService redisDispatcherService;

    public RedisCommonController(RedisDispatcherService redisDispatcherService) {
        this.redisDispatcherService = redisDispatcherService;
    }

    /**
     * 지정된 타입에 따라 Redis에 데이터를 저장하고 만료를 처리합니다.
     * RedisRequest Body의 'type' 필드에 따라 STRING, LIST, SET, ZSET, HASH 서비스로 위임됩니다.
     * - STRING: 단일 값 저장
     * - LIST: 리스트의 오른쪽에 요소 추가 (RPUSH)
     * - SET: Set에 멤버 추가
     * - ZSET: Sorted Set에 멤버 (스코어 0.0) 또는 맵 형태로 여러 멤버/스코어 추가
     * - HASH: Hash에 맵 형태로 여러 필드/값 추가 (HMSET)
     *
     * @param request 타입, 키, 값, 만료 시간을 포함하는 RedisRequest 객체
     * @return 확인 메시지
     * @throws IllegalArgumentException 지원되지 않는 타입이거나 필수 필드가 누락된 경우
     */
    @PostMapping("/set")
    public String setData(@RequestBody RedisRequest request) {

        // 필수 필드 유효성 검사
        if (request.getType() == null || request.getKey() == null || request.getValue() == null) {
            throw new IllegalArgumentException("Error: Type, key, and value are required in the request body.");
        }

        Optional<RedisDataType> type = RedisDataType.fromString( request.getType() );
        String key = request.getKey();
        Object value = request.getValue();
        int expirationSeconds = request.getExpirationSeconds();

        if (expirationSeconds > 0) {
            redisDispatcherService.dispatchSetDataWithExpiration(type.orElseThrow().getServiceBeanName(), key, value, expirationSeconds);
            return "Data set with expiration (" + expirationSeconds + "s) successfully for type " + type.orElseThrow().name() + "!";
        } else {
            redisDispatcherService.dispatchSetData(type.orElseThrow().getServiceBeanName(), key, value);
            return "Data set successfully for type " + type.orElseThrow().name() + "!";
        }
    }

    /**
     * 지정된 타입에 따라 Redis에서 데이터를 가져옵니다.
     * URL 쿼리 파라미터로 타입과 키를 받습니다.
     * @param typeString Redis 데이터 타입 문자열 (예: "STRING", "LIST")
     * @param key Redis 키
     * @return 가져온 데이터 (String, List, Set, Map 등)
     * @throws IllegalArgumentException 지원되지 않는 타입인 경우
     */
    @GetMapping("/get")
    public Object getData(@RequestParam(value = "type") String typeString, @RequestParam(value = "key") String key) {
        RedisDataType type = RedisDataType.fromString(typeString)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Redis type: " + typeString));
        return redisDispatcherService.dispatchGetData(type.getServiceBeanName(), key);
    }

    /**
     * 지정된 타입에 따라 Redis에서 데이터를 삭제합니다.
     * URL 쿼리 파라미터로 타입과 키를 받습니다.
     * @param typeString Redis 데이터 타입 문자열
     * @param key Redis 키
     * @return 확인 메시지
     * @throws IllegalArgumentException 지원되지 않는 타입인 경우
     */
    @DeleteMapping("/delete")
    public String deleteData(@RequestParam(value = "type") String typeString, @RequestParam(value = "key") String key) {
        RedisDataType type = RedisDataType.fromString(typeString)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Redis type: " + typeString));

        if (redisDispatcherService.dispatchDeleteData(type.getServiceBeanName(), key)) {
            return "Data deleted successfully for type " + type.name() + "!";
        }
        return "Failed to delete data or key not found for type " + type.name() + ".";
    }

    /**
     * 지정된 타입에 따라 Redis에 키가 존재하는지 확인합니다.
     * URL 쿼리 파라미터로 타입과 키를 받습니다.
     * @param typeString Redis 데이터 타입 문자열
     * @param key Redis 키
     * @return 키 존재 여부
     * @throws IllegalArgumentException 지원되지 않는 타입인 경우
     */
    @GetMapping("/has")
    public boolean hasKey(@RequestParam(value = "type") String typeString, @RequestParam(value = "key") String key) {
        RedisDataType type = RedisDataType.fromString(typeString)
                .orElseThrow(() -> new IllegalArgumentException("Invalid Redis type: " + typeString));
        return redisDispatcherService.dispatchHasKey(type.getServiceBeanName(), key);
    }
}

✒️ RedisOperationService

/**
 * Redis 작업 공통 인터페이스. 각 구현체는 Redis 데이터 타입별(String, List 등) 작업을 처리합니다.
 */
public interface RedisOperationService {

    /**
     * Redis에 데이터를 저장합니다.
     * @param key 저장할 키
     * @param value 저장할 값
     */
    void setData(String key, Object value);

    /**
     * Redis에 데이터를 저장하고 만료 시간을 설정합니다.
     * @param key 저장할 키
     * @param value 저장할 값
     * @param timeoutSeconds 만료 시간 (초)
     */
    void setDataWithExpiration(String key, Object value, long timeoutSeconds);

    /**
     * Redis에서 데이터를 가져옵니다.
     * @param key 가져올 키
     * @return 저장된 값 또는 요소들
     */
    Object getData(String key);

    /**
     * Redis에서 데이터를 삭제합니다.
     * @param key 삭제할 키
     * @return 삭제 성공 여부
     */
    Boolean deleteData(String key);

    /**
     * 특정 키의 존재 여부를 확인합니다.
     * @param key 확인할 키
     * @return 존재 여부
     */
    Boolean hasKey(String key);
}

✒️ RedisHashServiceImpl

/**
 * Redis Hash 타입 작업 서비스 구현체입니다. RedisOperationService 인터페이스를 구현합니다.
 */
@Service("hashRedisService") // 디스패처에 주입하기 위한 특정 빈 이름
@Slf4j
public class RedisHashServiceImpl implements RedisOperationService {

    private final HashOperations<String, String, Object> hashOperations; // HashOperations 사용

    public RedisHashServiceImpl(RedisTemplate<String, Object> redisTemplate) {
        // HashOperations는 Hash Key를 String으로, Hash Value를 Object로 사용하도록 지정
        this.hashOperations = redisTemplate.opsForHash();
    }

    // --- RedisOperationService 인터페이스 Hash 타입 메서드 구현 ---

    /**
     * Hash 타입의 'setData'는 Map<String, Object> 형태의 value를 받아 여러 필드-값 쌍을 Hash에 저장합니다. (HMSET/HSET)
     * value가 Map이 아니면 지원하지 않습니다.
     * @param key Hash의 키
     * @param value 저장할 필드-값 쌍 (Map<String, Object> 형태)
     */
    @Override
    public void setData(String key, Object value) {

        if (value instanceof Map) {
            Map<String, Object> entries = (Map<String, Object>) value;
            hashOperations.putAll(key, entries); // 여러 필드-값 쌍 저장
            log.info("[HashService] Stored multiple entries in Hash via setData: {} -> {}", key, entries);

        } else {
            log.error("[HashService] setData for HASH type requires a Map<String, Object> value. Received: {}", value.getClass().getName());
            throw new IllegalArgumentException("For HASH type, 'value' must be a Map<String, Object>.");
        }
    }

    /**
     * Hash 타입의 'setDataWithExpiration'은 Map<String, Object> 형태의 value를 받아 Hash에 저장하고, 해당 키(Hash 전체)에 만료 시간을 설정합니다.
     * @param key Hash의 키
     * @param value 저장할 필드-값 쌍
     * @param timeoutSeconds 만료 시간 (초)
     */
    @Override
    public void setDataWithExpiration(String key, Object value, long timeoutSeconds) {
        setData(key, value); // 필드-값 쌍 추가는 setData 로직 재사용
        hashOperations.getOperations().expire(key, Duration.ofSeconds(timeoutSeconds)); // Hash 키에 TTL 설정
        log.info("[HashService] Stored entries in Hash via setDataWithExpiration: {} -> {}. TTL set for key: {}s", key, value, timeoutSeconds);
    }

    /**
     * Hash 타입의 'getData'는 Hash의 모든 필드-값 쌍을 가져오는 것으로 해석됩니다. (HGETALL)
     * @param key Hash의 키
     * @return Hash의 모든 필드-값 쌍 (Map<String, Object>)
     */
    @Override
    public Map<String, Object> getData(String key) {
        Map<String, Object> entries = hashOperations.entries(key);
        log.info("[HashService] Retrieved all Hash entries: {} -> {}", key, entries);
        return entries != null ? entries : Collections.emptyMap();
    }

    @Override
    public Boolean deleteData(String key) {
        Boolean deleted = hashOperations.getOperations().delete(key);
        log.info("[HashService] Delete data: {} -> {}", key, deleted);
        return deleted;
    }

    @Override
    public Boolean hasKey(String key) {
        Boolean exists = hashOperations.getOperations().hasKey(key);
        log.info("[HashService] Has key: {} -> {}", key, exists);
        return exists;
    }
  ...
}

6. runining log

💬 hash 데이터 저장

  • postmen

  • redis server

💬 다른 타입의 중복된 key로 데이터 저장 시 exception

  • postmen

📌

  • 전체 소스 코드는 github spring-basic-server 프로젝트 redis_spring에 있습니다.

📚 참고

profile
나도 보기 위해 정리해 놓은 벨로그

0개의 댓글