스프링부트

Redis를 이용한 캐싱과 캐싱전략

Choony 2024. 7. 1. 21:23

똑같은 데이터를 매번 데이터베이스에서부터 가져오면 사용자와 서버입장에서 모두 비효율적이다. 

그렇기에 한번 가져온 데이터의 경우엔 캐싱처리를 해서 두 번째 요청부턴 효율적으로 가져오고자 한다.

 

현재 프로젝트의 경우에 RefreshToken과 로그아웃 관리를 위해 Redis를 사용하고 있다. 그렇기에 캐싱 저장소로 Redis를 사용해 보기로 했다.

 

❗️❗️ 주의 ❗️❗️
과정상의 코드는 별로 없다.

바로 캐싱을 적용하기 전에 간단하게 공부를 해보니 캐싱에도 여러가지 전략이 있다고 한다.

 

캐싱 전략을 세울 때 유의해야 할 것은 무엇일까?

  • 캐싱 데이터는 주로 메모리에 저장되기 때문에 용량 제한이 있다. (주로 16 ~ 32 기가)
    • 따라서 캐싱 데이터의 유효기간 설정이 중요하다.
    • 따라서 어떤 데이터를 캐싱해 둘지 선택하는 것이 중요하다.
  • 캐싱 데이터와 DB의 데이터의 정합성이 중요하다.
    • 따라서 C U D 작업에서 캐싱 데이터를 수정할지 R 할 때 어떻게 할 것이지에 대한 전략을 세워야 한다.
    • 나의 프로젝트는 정상적으로 종료된 채팅만 조회가 가능하게 되어있다. 따라서 채팅 시작과 채팅 종료에서 캐시데이터 업데이트가 필요할 것으로 보인다.
    • 따라서 Look Aside와 Write Around 조합을 사용해서 캐싱을 적용하도록 하겠다.
더보기

Look Aside 란

데이터를 찾을 때 우선 캐시에 저장된 데이터가 있는지 우선적으로 확인하는 전략.

나의 프로젝트의 경우 만일 캐시에 데이터가 없다면 Mongo DB에서 조회해 온다.

더보기

Write Around 란

모든 데이터를 DB에만 저장하는 기법 (캐시를 갱신하지 않음)

Cache miss가 발생할 때만 DB와 캐시에 데이터를 저장한다.

Cache와 DB의 데이터가 일치하지 않을 수 있음.

 

따라서 DB에 저장된 데이터가 수정, 삭제될 때마다, Cache 또한 수정하거나 삭제해야 하며, Cache의 expire를 짧게 유지해야 한다. 우리 코드에서는 채팅 종료 및 채팅 삭제에서 CacheEvict를 적용한 게 캐시데이터를 변경하는 거다.

캐싱 전략에 대해 더 자세히 보고 싶다면 이 글이 아주 자세히 정리되어 있다.. 👍

 

https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EC%BA%90%EC%8B%9CCache-%EC%84%A4%EA%B3%84-%EC%A0%84%EB%9E%B5-%EC%A7%80%EC%B9%A8-%EC%B4%9D%EC%A0%95%EB%A6%AC#write_around_%ED%8C%A8%ED%84%B4


[ 첫 번째 조회 ] 

[ 새로운 채팅 추가 ]

[ 두 번째 조회 ]

 

여전히 데이터가 두 개이다.

기존 450ms -> 14ms

캐싱처리는 됐지만 추가된 데이터가 반영되지 않아 데이터 정합성이 깨지는 걸 볼 수 있다.

@CachePut(value = "SelfIntros", key = "'selfAll'", unless = "#result == null", cacheManager = "cacheManager")

캐시 데이터 갱신을 위해 위의 코드를 채팅 종료 메서드에 적용해 주었지만 여전히 적용이 되지 않음

 

@CachePut메서드의 반환값으로 캐시갱신이 되기 때문에 현재 반환타입이 void인 채팅종료에서 적용이 되지 않는 것은 당연하다.

어떻게 해결할까?

@CacheEvict(value = "SelfIntros", key = "'selfAll'", cacheManager = "cacheManager")

 

@CacheEvict를 적용해 보자.

이는 메서드가 실행될 때 캐시 데이터를 모두 삭제하게 된다. 따라서 채팅 종료 후 첫 조회 요청이 들어오면 캐시 데이터가 없기 때문에 데이터베이스에 갔다 오게 된다.

 

[ 첫 번째 조회 ]

"selfIntros": [
            {
                "chatRoomId": "665c121e3c4d4b0bc61925a1",
            },
            {
                "chatRoomId": "665c12413c4d4b0bc61925a2",
            },
            {
                "chatRoomId": "665d13927098ba30b99c3e25",
            },
            {
                "chatRoomId": "665d14aa76777e49157346f1",
            }
        ]

현재 테스트해 보느라 2개가 더 추가된 4개가 현재 조회가 된다.

 

[ 새로운 채팅 생성 ]

 

[ 두 번째 조회 ]

390 ms -> 29 ms

"selfIntros": [
            {
                "chatRoomId": "665c121e3c4d4b0bc61925a1",
            },
            {
                "chatRoomId": "665c12413c4d4b0bc61925a2",
            },
            {
                "chatRoomId": "665d13927098ba30b99c3e25",
            },
            {
                "chatRoomId": "665d14aa76777e49157346f1",
            },
            {
                "chatRoomId": "665d17dec584f95771a36554",
            }
        ]

 

소요시간과 응답 데이터를 보니 캐싱이 잘 적용되었고 데이터 정합성 또한 잘 지켜지는 것을 볼 수 있다.

 

 

최종 코드

// 조회
@Cacheable(value = "CSChats", key = "'csAllTopic:' + #memberId", unless = "#result == null", cacheManager = "cacheManager")
public CSChatHistoryList findAllCSChat(String memberId) {
		... 코드 생략
}

 

 

// 채팅 종료 -> 새로운 채팅이 추가됨
@Caching(evict = {
		@CacheEvict(value = "CSChats", key = "'csAll:' + #memberId + ':' + #csChatInfo.topic()", cacheManager = "cacheManager"),
		@CacheEvict(value = "CSChats", key = "'csAllTopic:' + #memberId", cacheManager = "cacheManager"),
		@CacheEvict(value = "CSChat", key = "#csChatInfo.chatRoomId()", cacheManager = "cacheManager")
})
public CSChatHistory terminateCSChat(String memberId, CSChatInfo csChatInfo) {
		... 코드 생략
}
// 채팅 삭제
@Caching(evict = {
		@CacheEvict(value = "CSChats", key = "'csAll:' + #memberId + ':' + #csChatInfo.topic()", cacheManager = "cacheManager"),
		@CacheEvict(value = "CSChats", key = "'csAllTopic:' + #memberId", cacheManager = "cacheManager"),
		@CacheEvict(value = "CSChat", key = "#csChatInfo.chatRoomId()", cacheManager = "cacheManager")
})
public void deleteCSChat(String memberId, CSChatInfo csChatInfo) {
		... 코드 생략
}