개발 메모장

[Redis] STS > Redis 적용 예제 본문

DBMS

[Redis] STS > Redis 적용 예제

yyyyMMdd 2024. 4. 22. 13:19
728x90

#. Redis 란?

 

  • Redis는 NoSQL DB의 한 유형으로 분류되며 데이터베이스, 캐시 및 메시지 브로커로 사용할 수 있는 오픈 소스, 인 메모리 데이터 구조 저장소입니다.

  • 속도, 다양성, 풍부한 데이터 구조 세트로 잘 알려져 있어 최신 애플리케이션에서 광범위하게 사용됩니다.

#. 주요 기능

1. 인메모리 데이터 저장소

    - Redis는 데이터를 인메모리에 저장하는 캐싱작업을 하기에 매우 빠른 읽기 및 쓰기 작업이 가능합니다. 

    - 지속성을 위해 디스크 스토리지를 사용하지만 기본 데이터 액세스는 메모리에서 발생하므로 데이터에 대한 짧은 대기 시간 액세스가 필요할 때 사용합니다.

2. 데이터 구조

    - Redis는 String, List, Set,  Sorted Sets, Hash와 같은 다양한 데이터 구조를 지원합니다. 

    - 이러한 데이터 구조를 통해 개발자는 복잡한 데이터를 모델링하고 데이터의 증가/감소, 교집합, 합집합 처리 등과 같은 작업을 Redis에서 수행할 수 있습니다.


3. 지속성

    - Redis는 스냅샷 및 AOF(추가 전용 파일) 지속성을 포함하여 데이터 지속성을 위한 다양한 옵션을 제공합니다. 

    - 스냅샷은 주기적으로 데이터 세트를 디스크에 저장합니다.

    - AOF 지속성은 모든 쓰기 작업을 로그 파일에 기록하므로 시스템 오류가 발생할 경우 데이터를 복구할 수 있습니다.


4. 복제 및 고가용성

    - Redis는 마스터-슬레이브 복제를 지원하므로 내결함성과 고가용성을 위해 데이터를 여러 Redis 인스턴스에 복제할 수 있습니다. 

    - 복제 외에도 Redis는 Redis 인스턴스의 자동 장애 조치 및 모니터링을 위한 내장 솔루션인 Sentinel도 제공합니다.


5. Pub/Sub 메시징

    - Redis에는 게시(Public)/구독(Subscribe) 메시징 지원이 내장되어 있습니다. 

    - 클라이언트는 채널에 메시지를 게시할 수 있으며, 다른 클라이언트는 이러한 채널을 구독하여 실시간으로 메시지를 받을 수 있습니다. 

    - 이 기능을 통해 Redis는 실시간 메시징 시스템, 채팅 애플리케이션 등을 구축할 수 있습니다.

 

    - 지속성이 없기 때문에 메시지를 전송한 후 삭제되며 저장되지 않고, 보장성 또한 없어 클라이언트가 메시지를 받았는지에 대한 신뢰성을 보장하지 않기에 별도 로직을 구현 해야 합니다.


6. LUA 스크립팅

    - Redis는 Lua 스크립팅을 지원하므로 개발자는 서버 측에서 실행할 수 있는 사용자 지정 스크립트를 작성할 수 있어 효율적으로 처리가 가능합니다.


7. GeoSpatial Indexing

    - Redis는 지리적 공간 데이터 저장 및 쿼리를 지원하므로 개발자는 특정 좌표값에 대한 주변 쿼리의 검색과 같은 작업을 수행할 수 있습니다.


8. 확장성

    - Redis는 확장성이 뛰어나며 다양한 프로그래밍 언어에 대한 다양한 클라이언트 라이브러리를 지원합니다. 또한 플러그인, 모듈 및 타사 도구를 지원하는 활발한 커뮤니티와 생태계도 있습니다.


#. 캐싱 처리는 언제 사용해야 할까?

  • 클라이언트에게 전달하는 값이 동일한 경우

  • 특정 데이터가 자주 사용되지만 자주 수정되지는 않는 경우

  • 사용될 때 서버 자원을 많이 사용하는 경우

  • 분산 환경을 사용하는 경우

#. 언제 사용하지 않아야 할까?

  • 데이터가 자주 바뀌고 최신 상태의 데이터를 제공해야 하는 경우

  • 메모리를 많이 소비하는 대규모 데이터 세트를 사용하는 경우
    (캐싱 시 메모리가 고갈될 수 있습니다.)

  • 민감 데이터를 사용하는 경우

#. Redis 사용 예제

#. Redis 서버 실행

 

1. 콘솔을 이용해 Redis를 설치한 폴더로 이동하여 redis-server를 입력하면 클라이언트 연결을 수신하는 서버 인스턴스가 시작됩니다.

  • 만약 위와 같은 화면이 나오지 않는다면 Redis 폴더의 redis.windows.conf를 실행해 보시고 정상적으로 열리지 않으면 아래의 명령어를 입력해 보시길 바랍니다.
  • 6380 포트에서 서버 인스턴스를 시작하고 이를 6379 포트에서 실행 중인 Redis 서버의 슬레이브로 구성할 수 있게 복제 설정하는 방법입니다.
redis-server --port 6380 --slaveof 127.0.0.1 6379

2. 다른 콘솔을 이용해 Redis 서버와 상호작용할 수 있도록 Redis 폴더에서 redis-cli를 입력해 보시면 아래와 같이 입력이 가능한 콘솔로 변경되며 여기서 각종 명령어를 입력해 Redis를 사용할 수 있습니다.

 


#. build.gradle

 

  • Spring boot에서 지원하는 Redis 라이브러리를 사용합니다.

  • SQL은 JPA를 이용하며, DBMS는 별도 연결 없이 사용 가능한 내장 DBMS H2를 사용하였습니다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'

#. application.yml

spring:
  datasource:
    driver-class-name: org.h2.Driver.
    url: jdbc:h2:~/test
    username: sa
    password:  
  h2:
    console:
      enabled: true  
      path: /h2-console  
  jpa:
    hibernate:
      ddl-auto: update 
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect  
        show_sql: true  
        format_sql: true  
        use_sql_comments: true  
  cache:
    type: redis
  data:
    redis:
      host: 127.0.0.1
      port: 6379

#. Configuration

  • 어노테이션 및 사용 메서드에 대한 설명은 아래와 같습니다.
어노테이션 및 메서드명 설명
@EnableCaching 캐시 기능을 사용하려할 때 붙여줍니다.
LettuceConnectionFactory 
( JedisConnectionFactory )
Redis 서버에 대한 연결 설정
RedisCacheConfiguration Redis 캐시의 동작을 구성하는 데 사용
defaultCacheConfig() Redis 기본 구성을 생성하기 위한 메서드
serializeValuesWith() 캐시 값을 직렬화하기 위한 메서드
RedisSerializationContext.SerializationPair 키와 값에 대한 직렬화 및 역직렬화 쌍을 정의하는데 사용되는 클래스
GenericJackson2JsonRedisSerializer Jackson을 사용하여 캐시 값을 JSON으로 직렬화 함을 명시
disableCachingNullValues() null 값 캐싱을 비활성화
entryTtl(Duration.ofMinutes(30L)) 캐시 항목의 TTL(Time-To-Live)을 설정(30분)
cacheDefaults(configuration) 이 캐시 관리자가 생성한 Redis 캐시의 기본 구성임을 설정

import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

@Configuration
@EnableCaching
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String host;

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager.RedisCacheManagerBuilder builder =
                RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());

        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
				.disableCachingNullValues()
                .entryTtl(Duration.ofMinutes(30L));

        builder.cacheDefaults(configuration);

        return builder.build();
    }
}

#. Entity

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Member {

	@Id
	@GeneratedValue
	@Column(name="ID")
	private Long id;
	
	@Column(name="NAME")
	private String name;
}

#. Repository & Impl

  • 조회, 저장, 삭제에 대한 기능에 캐싱처리를 할 예정입니다.
public interface RedisTestRepository {
	Member save(Member member);
	Member findOne(Long id);
	void delete(Member member);
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.redis_Test.entity.Member;

import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class RedisTestRepositoryImpl implements RedisTestRepository {
	
	@Autowired
	private EntityManager em;
	
	@Override
	public Member save(Member member) {
		if(member.getId() != null && !"".equals(member.getId())) {
			Member findMember = em.find(Member.class, member.getId());
			findMember.setName(member.getName());
		} else {
			em.persist(member);
		}
		
		return member;
	}

	@Override
	public Member findOne(Long id) {
		return em.find(Member.class, id);
	}

	@Override
	public void delete(Member member) {
		em.remove(member);
	}
}

#. Service & Impl

import com.redis_Test.entity.Member;

public interface RedisTestService {
	void joinMember(Member member);
	Member updateMember(Member member, Long id);
	Member getMemberInfo(Long id);
	void deleteMember(Long id);
}

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.redis_Test.entity.Member;
import com.redis_Test.repository.RedisTestRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly=true)
public class RedisTestServiceImpl implements RedisTestService {

	@Autowired
	private RedisTestRepository redisTestRepository;
	
	@Override
	@Transactional
	public void joinMember(Member member) {
		redisTestRepository.save(member);
	}
	
	@Override
    @Cacheable(value = "Member", key = "'_'+#id", cacheManager = "cacheManager", unless = "#result == null", condition = "#id != null")
	public Member getMemberInfo(Long id) {
		return redisTestRepository.findOne(id);
	}
	
	@Override
	@CachePut(value="Member", key="'_'+#id", cacheManager="cacheManager")
	@Transactional
	public Member updateMember(Member member, Long id) {
		return redisTestRepository.save(member);
	}
	
	@Override
	@CacheEvict(value="Member", key="'_'+#id", cacheManager="cacheManager")
	@Transactional
	public void deleteMember(Long id) {
		Member member = redisTestRepository.findOne(id);
		redisTestRepository.delete(member);
	}
}
어노테이션 및 속성 설명
@Cacheable - 메서드 호출 결과가 캐시 되어야 할 때 사용합니다. 

(메서드를 실행하기 전 메서드 호출을 가로채며, 캐시에 결과가 없으면 메서드가 호출되고 결과가 있다면 캐시에서 결과를 리턴합니다.)
value - 사용할 캐시 이름을 지정할 수 있으며 redis에 저장되는 캐시명입니다.

- 최초 해당 캐시명이 없으면 spring에서 생성하도록 합니다.
key - 캐시 된 결과가 저장될 키를 정의합니다.

- #id와 같이 #기호를 넣어 SpEL표현식으로 나타냅니다.

- #p0은 메서드의 첫 번째 매개변수를 나타냅니다.

- '_' +#id와 같이 key 앞에 문자를 입력할 시 key가 null이어도 캐시값을 생성합니다.

(테스트 시 #id에 대한 값을 가져오지 못해 null key returned for cache operation이란 오류가 발생해  이렇게 처리 후 테스트했습니다.)
cacheManager - 사용할 캐시매니저 Bean을 지정합니다.

- RedisConfig에서 만든 Bean을 나타냅니다.
unless - 캐시가 되지 않는 조건을 정의합니다.

- 결과의 값이 null인 경우 캐시 되지 않도록 지정합니다.
condition - 캐시 저장 시 조건 지정을 할 수 있습니다.

- 객체, 필드에 대한 조건을 지정하며 SpEL문법으로 작성합니다.("Member.id.length() < 2")
keyGenerator - Spring은 메서드의 파라미터와 값을 기반해 기본적으로 키를 생성합니다.

- 자체적으로 키를 생성해야 할 때 인터페이스를 구현하고 keyGenerator에 입력하면 기본키 대신 자체 키를 생성합니다.
CacheResolver - 마찬가지로 사용자정의 캐시리졸버를 구현해 사용할 때 선언합니다.
sync - 캐시 작업을 동기/비동기적으로 처리할지에 대한 선택이 가능합니다.

- 기본 값을 true입니다.
   
@CachePut  - 항상 메서드 실행을 강제하고 해당 결과로 캐시를 업데이트합니다.

- 메서드가 데이터를 업데이트하거나 생성하고 이 결과가 캐시에 업데이트 돼야 할 때 사용

- 내부 선언은 Cacheable과 거의 동일하며 자세한 내용은 class를 확인해 보시길 바랍니다.
   
@CacheEvict - 메서드 호출 시 캐시에서 키에 해당하는 캐시를 제거하는 데 사용합니다.
allEntries = true - 캐시에 저장된 값을 모두 제거해야 할 때 사용합니다.
beforeInvocation = true - 메서드 실행 전 캐시를 제거해야 할 때 사용합니다.

#. Controller

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.redis_Test.entity.Member;
import com.redis_Test.service.RedisTestService;

import lombok.RequiredArgsConstructor;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;


@RestController
@RequestMapping("api/redis")
@RequiredArgsConstructor
public class RedisTestController {

	@Autowired
	private RedisTestService redisTestService;
	
	@GetMapping("/{id}")
	public ResponseEntity<?> getMemberInfo(@PathVariable("id") Long id) {
		return ResponseEntity.ok(redisTestService.getMemberInfo(id));
	}
	
	@PostMapping("/join")
	public ResponseEntity<?> joinMember(@RequestBody Map<String, String> memberInfo) {
		Member member = new Member();
		member.setName(memberInfo.get("name").toString());
		redisTestService.joinMember(member);
		return ResponseEntity.ok("등록완료");
	}
	
	@PutMapping("/update")
	public ResponseEntity<?> updateMember(@RequestBody Map<String, String> memberInfo) {
		Member member = new Member();
		member.setId(Long.parseLong(memberInfo.get("id").toString()));
		member.setName(memberInfo.get("name").toString());
		redisTestService.updateMember(member, member.getId());
		return ResponseEntity.ok("수정완료");
	}
	
	@DeleteMapping("/{memberId}")
	public ResponseEntity<?> deleteMember(@PathVariable("id") Long id) {
		redisTestService.deleteMember(id);
		return ResponseEntity.ok("삭제완료");
	}
}

#. 캐싱처리 확인

 

1. 조회


- postMan을 이용해 getMemberInfo 메서드를 실행시켜 보겠습니다.


- 최초 실행 시 아래와 같이 SQL이 작동하는 모습을 확인할 수 있습니다.


- 또한 Redis에서 key를 조회해 보면 생성된 것도 확인할 수 있습니다.

 

- 다시 같은 내용을 조회해 보면 서비스 콘솔에선 아무 반응 없이 리턴값만 노출되게 됩니다.


2. 수정

 

- updateMember 메서드를 실행시켜 보겠습니다.


- @CachePut을 사용하기에 항상 메서드의 로직이 실행됩니다.


- 캐시 값을 확인해보면 동일 키에 대한 값이 변경된 것을 볼 수 있습니다.


- 이후 다시 조회를 해보면 로직을 타지 않고 변경된 캐시값을 호출해 옵니다.


3. 삭제

 

- 삭제 메서드를 실행하면 key값에 맞는 캐시가 정상적으로 삭제된 것을 볼 수 있습니다.


  • 조회, 수정, 삭제 시 캐시와 함께 자원을 소모하지 않고 위와 같은 방식으로 처리되기에 기존의 처리 속도보다 빠른 성능을 보여줍니다.

  • 처음 소개할 때에 언급했듯이 필요한 상황에 맞추어 적용한다면 더 나은 서비스를 제공할 수 있을 것입니다.

 

 

===========================================================
틀린 내용이 있거나 이견 있으시면 언제든 가감 없이 말씀 부탁드립니다!
===========================================================

728x90