[Redis] Redis 캐시 설정 수정 - DefaultTyping.EVERYTHING 삭제
지난번에 설정한 DefaultTyping.EVERYTHING에서 보안 문제가 생길 수 있다는 이슈를 들었다. 그래서 이번엔 그 부분을 제거하는 방향으로 수정하게 되었다.
1. DefaultTyping.EVERYTHING의 문제점
처음 설계는 Redis용 ObjectMapper에 DefaultTyping.EVERYTHING를 써서 모든 캐시 데이터에 강제로 자바 클래스 정보(@class)를 다 넣는 구조였다. 하지만 이러면 몇 가지 문제가 생기게 된다. 첫 번째, 내 설정 하나 때문에 다른 팀원이 만든 캐시 데이터 포맷까지 강제로 바뀔 수 있다. 두 번째, 모든 클래스를 다 받아 주는 구조라 외부에서 이상한 클래스 정보가 섞여 들어오면 역직렬화 과정에서 RCE 공격 위험이 생길 수 있다. 그래서 보안적으로도, 팀 단위 코드 기준으로도 사용하지 않는 게 좋다.
2. 해결 과정
수정 전 구조에서는 Redis에 Java 객체를 그대로 저장한 게 아니었다. 나는 그냥 객체로 들어갔다가 객체로 나오는 줄 알고 쓴 거고,
PostCacheDTO -> JSON -> Redis -> LinkedHashMap
실제로는 이 흐름인 것이다.
그래서 생각한 해결 방법은 캐시에서 꺼낸 Map을 우리가 직접 DTO로 변환해 주자였고,
LinkedHashMap -> PostCacheDTO
그 결과 이렇게 바꿔 주는 코드가 추가되었다.
그리고 인기글 캐시는 단순한 목록 캐시가 아니라 로그인/비로그인, 좋아요 여부, 이미지 URL 가공 로직까지 섞이기 때문에 결과를 무조건 캐시하게 되면 안 된다. 어디까지 캐시할지 결정하는 로직이 필요하다고 생각되어서
1. DB에서 가져온 공통 데이터만 캐시
2. 좋아요 여부, 이미지 URL 같은 건 요청 시점에 조립
이 구조를 만들기 위해 PopularPostCacheService 파일을 따로 생성하였다. 그리고 @Cacheable을 기존 Service나 Repository에 붙이지 않고 PopularPostCacheService로 분리했다. 이렇게 하니 캐시 정책이 한곳에 모이고, 요청자 기준 분기 로직도 훨씬 관리하기 쉬워졌다.
objectMapper 함수는 JacksonConfig 파일에 존재하기 때문에 중복 코드는 지우고 끌어와서 쓰게 되었다.
3. 해결 결과
Redis에서 캐시를 꺼낸다고 해서 항상 PostCacheDTO가 그대로 나오는 것은 아니었다. 스프링 캐시의 내부 동작 때문에 최초로 DB에서 조회하여 캐시에 저장하는 시점에는 메서드가 반환한 PostCacheDTO가 메모리에 남아 있어 그대로 반환한다. 하지만 캐시가 이미 존재하여 Redis에서 데이터를 읽어오는 시점에는 타입 정보(@class)가 없으므로 Jackson이 이를 LinkedHashMap으로 읽어오게 된다.
첫 번째 호출 (DB 조회) -> PostCacheDTO
두 번째 이후 호출 (캐시 히트) -> LinkedHashMap
최종적으로는 이런 흐름으로 진행하게 된다.
+. @Cacheable에 관하여
@Cacheable을 남발하면 좋지 않다는 얘기를 들었다. 그래서 조사해 보았더니 이 부분은 상황마다 다른 것 같다. 데이터가 자주 바뀌지 않거나, 조회가 매우 잦거나, DB 조인 쿼리가 무거울 때 쓰면 효과가 좋다고 한다. 내가 맡은 인기글도 조회가 잦은 홈페이지에서 사용되니 어노테이션을 사용해도 괜찮은 것이다. 반대로 주식처럼 실시간으로 계속 바뀌는 데이터나 어쩌다 한 번만 조회되는 데이터, DB 조회가 아주 단순하고 빠른 경우엔 오히려 손해라 안 쓰는 게 좋다고 한다.
오늘의 요약
- Redis에 대해 제대로 이해하지 않고 진행하다 보니 역직렬화에서 계속 부딪히게 됐다. 꼭 이해하고 넘어가자.