Redis

Zerodin·2022년 6월 16일
0

Redis

목록 보기
1/2
post-thumbnail

Redis?

흔히 클라우드 환경에서 캐쉬 서비스를 사용한다고 하면 Redis를 제일 먼저 떠올리게 된다.
그렇다면 대체 redis란 정확하게 어떤것을 의미하는 것일까?

AWS 의 캐쉬서비스인 elasticache의 document page에서는 이를 아래와 같이 정의하고 있다.

'Redis는 Remote Dictionary Server의 약어로 빠른 오픈 소스 인 메모리 키 값 데이터 구조 스토어입니다. 다양한 인 메모리 데이터 구조 집합을 제공하므로 다양한 사용자 정의 애플리케이션을 손쉽게 생성할 수 있습니다.

주요 Redis 사용 사례로는 캐싱, 세션 관리, pub/sub 및 순위표를 들 수 있습니다.

Redis는 현재 가장 인기 있는 키 값 스토어로서, BSD 라이선스가 있고, 최적화된 C 코드로 작성되었으며, 다양한 개발 언어를 지원합니다. Redis는 REmote DIctionary Server의 약어입니다.

Redis는 속도가 빠르고 사용이 간편하여 최고의 성능이 필요한 웹, 모바일, 게임, 광고 기술 및 IoT 애플리케이션에서 널리 사용되고 있습니다. AWS는 Redis용 Amazon ElastiCache라는 최적화된 완전관리형 데이터베이스 서비스를 통해 Redis를 지원하며, 고객은 원하는 경우 AWS EC2에서 자체 관리형 Redis를 실행할 수도 있습니다.'

조금더 설명을 하자면,
Memcached와 비슷한 캐시 시스템으로서 동일한 기능을 제공하면서 영속성, 다양한 데이터 구조와 같은 부가적인 기능을 지원하는 캐쉬이다.

특징

Redis에는 어떤 특징은 아래와 같이 요약할 수 있다.

  • NoSQL로서 Key-Value 타입의 저장소.
  • 영속성을 지원하는 in memory 데이터 저장소.
  • 읽기 성능 증대를 위한 서버 측 복제(replication)를 지원.
  • 쓰기 성능 증대를 위한 클라이언트 측 샤딩(Sharding) 지원.
  • 문자열, 리스트, 해시, 셋, 정렬된 셋과 같은 다양한 데이터형을 지원.

Sample 구현

Redis 설정관련 코드

우선 Redis 서버에 사용할 port와 host 명을 yml로 간단하게 작성한다.
application.yml

spring:
  redis:
	host: localhost
	port: 6379

Redis를 사용하기 위해서 connection을 만들기 위한 factory 와 @Cacheable에 할당할 cacheManager 에 대한 bean 생성을 하도록 하자.

@Configuration
@EnableRedisRepositories
class RedisConfig(
    @Value("\${spring.redis.host}")
    private val host: String,
    @Value("\${spring.redis.port}")
    private val port: Int
) {
    @Bean
    fun redisConnectionFactory(): LettuceConnectionFactory {
        val lettuceConnectionFactory = LettuceConnectionFactory(RedisStandaloneConfiguration(host, port))
        lettuceConnectionFactory.afterPropertiesSet()
        return lettuceConnectionFactory
    }

    /**
     * Redis Cache를 관리
     * @return RedisCacheManager
     */
    @Primary
    @Bean(name = ["memberManager"])
    fun memberManager(): RedisCacheManager {
        val configuration = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    GenericJackson2JsonRedisSerializer()
                )
            )
            .computePrefixWith(CacheKeyPrefix.simple())
            .disableCachingNullValues()                           
            .entryTtl(Duration.ofHours(8))                     // 캐싱 유지 시간 설정

        return RedisCacheManager.RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory())
            .cacheDefaults(configuration)
            .build()
    }
}

도메인

간단한 도메인을 하나 만들것이다.
흔히 사용되는 Member라는 도메인을 만들고 @RedisHash를 설정해주면, 특정 해쉬값을 키, 해당 클래스의 인스턴스를 값으로 Redis에 적재한다.

@RedisHash(value = "member")
data class Member(
    @Id
    var id: String? = null,
    var name: String? = null,
    var age: Int? = null
)

Service & Repository

간단한 비지니스 로직을 만들도록 한다.
저장, 조회정도만 구현할 것이다.
이때 저장되는 장소는 RDB가 아닌 Redis가 될 것이다.
특히, 저장된 내용을 조회할 때 캐쉬값을 조회하도록 하는 @Caheable 선언을 설명하면,

  • value는 RedisHash로 선언된 value값을 의미한다.
  • key 는 생성할때의 입력받는 id 파라메터와 value를 사용해서 합성하도록 설정 할 수 있는데
    설정한 cacheManger에서 computePrefixWith 항목에서 CacheKeyPrefix.simple()로 할당해주면 key앞에 '::'가 들어가서 value::key 패턴으로 값이 생성된다.
  • 이를 담당하는 cacheManager를 할당 해야 하는데 RedisConfig에서 @Primary로 생성한
    memberManager를 할당했다.
@Service
class MemberService(
    private val memberRepository: MemberRepository
) {
    fun getMemberById(id : String): Member {
        return memberRepository.findById(id).get()
    }

    fun saveMember(member: Member){
        memberRepository.save(member)
    }

    @Cacheable(value = ["member"], key = "#id", cacheManager = "memberManager")
    fun getMember(id: String): Member {
        println("getMember!!")
        return memberRepository.findById(id).get()
    }
}

Repository는 간단하게 spring 프레임워크의 CrudRepository를 상속받아서 사용할 것이다.

interface MemberRepository : CrudRepository<Member, String>

TestCase 작성

이제 만들어진 코드를 확인하기 위해 테스트 케이스를 작성해야 한다.

문제는 단순히 값을 넣고 찾게 하는 식으로 코드를 짜면 Redis Server가 실행되지 않은 상태로 테스트코드가 실행되서 정상적인 결과가 반환되지 않는다.

Redis Server가 먼저 실행되도록 TestConfiguration를 만들어 줘야 하는데,
테스트에서 사용할 목적으로 embedded-redis를 사용할 것이다.

핵심은 테스트코드의 서비스가 실행되기전에 Redis Server가 start되고, 테스트 종료이후 stop이 되도록 하는 것이다.

@PostConstruct를 사용해서 의존성 주입이 이루어진 후 초기화를 수행하는 시점에서 Redis Server가 실행되도록 할 수 있다.

그리고 @PreDestroy를 사용하면 서비스 테스트코드 종료 이후 시점에서 Server stop을 할 수 있다.

@TestConfiguration
class TestRedisConfiguration(
    @Value("\${spring.redis.host}")
    private val host: String,
    @Value("\${spring.redis.port}")
    private val port: Int
) {

	private val redisServer: RedisServer = RedisServer(port)

    @PostConstruct
    fun postConstruct() {
        println("------------------Embed redis server start!------------------")
        redisServer.start()
    }

    @PreDestroy
    fun preDestroy() {
        println("------------------Embed redis server end!------------------")
        redisServer.stop()
    }
}

간단한 서비스영역 테스트 케이스를 작성해보았다.
좀전에 만든 redis 테스트 설정을 반드시 import해줘야 한다.

테스트클래스 주생성자에 Service와 Repository를 각각 주입할 것이다.
이때, @Autowired를 넣어주어야 한다는 점을 잊지말자.

@Autowired를 주지 않고 테스트코드를 실행해보니 보기 좋게 실패했다.

  • 왜 @Autowired를 설정해야 할까?
    이유는 테스트 프레임워크에서의 생성자 매개변수 관리는 스프링 컨테이너가 아닌 Jupiter가 담당하기 때문에 @Autowired를 명시적으로 선언해줘서 Jupiter가 Spring Container에게 빈 주입을 요청할 수 있도록 해야하기 때문이다. 이에 관련한 자세항 내용은 아래 링크를 통해서 확인할 수 있다.

참조 : https://minkukjo.github.io/framework/2020/06/28/JUnit-23/

@SpringBootTest
@Import(TestRedisConfiguration::class)
internal class MemberServiceTest(
    @Autowired
    private var memberService: MemberService,
    @Autowired
    private val memberRepository: MemberRepository
) {

    @BeforeEach
    private fun setUp(){
        memberService = MemberService(memberRepository)
    }

    @AfterEach
    @Throws(Exception::class)
    private fun tearDown() {
        memberRepository.deleteAll()
    }

    @Test
    fun getMemberInRedisTest() {
        val member1 = Member(
            "123",
            "tester",
            22
        )
        memberService.saveMember(member1)

        val cacheMember = memberService.getMember("123")

        assertEquals(member1,cacheMember)
    }
}

테스트코드 실행 결과

profile
멈추지 않는 사람이 되고 싶어요!

0개의 댓글

관련 채용 정보