분명 전글에 Redis에 관한 포스팅이었는데, 왜 이 사람은 또 Redis에 대해서 글을 쓰는 걸까? 🤔 라고 궁금해하신다면 대답해드리는게 인지상정! 이유는 간단하다. ✨ 비로소 Redis에 대해서 더 깊이 이해하게 됐고 어떻게 구현해야 되는지 이해가 되어서 그 동안 프로젝트에 Redis를 이용한 코드의 동작 과정을 설명할 겸 Redis에 대한 심화편 포스팅을 작성하게 되었다.
전 글에서 Redis를 캐싱을 위한 용도로 쓴다고 적었다.
사전적인 정의를 인용하여 나중에 요청 할 결과를 미리 저장해 둔 후 빠르게 서비스 해주는 것을 의미한다.
좀 더 짦게 말하자면 캐싱이란 " 접근하는 시간을 단축시키기 위해 더욱 빠른 메모리에 데이터를 저장해두는 것이다." 🚀
그러면 주로 어떤 데이터를 캐싱하는 걸까? " 자주 조회되는 데이터 / 연산에 비용이 많이 드는 데이터 " 를 주로 캐싱한다. 🧐
이에 관해서는 전글에서 사진 첨부를 통해 짦게 언급하였다. 그래서 이번 글에서는 어떤 자료구조를 지원하는지 좀 더 자세하게 설명하겠다.🔍
트위터의 경우 타임라인에 보일 게시글 캐싱 용도로 List를 사용한다.
자주 사용하는 사용자의 타임라인을 캐싱하는 방법에 대한 간단한 과정이다.
다음과 같은 단계로 이루어진다.
마지막 로그인 시간이 N일 이내인 사용자의 Id를 key로 설정합니다.
사용자가 글을 작성하면, 그 사용자의 팔로워들의 Id를 key로 하여, 작성한 트윗의 Id를 추가한다.
Step 1: 마지막 로그인 시간이 N일 이내인 사용자 팔로워 확인
----------------------------------------------------
사용자 A -> 사용자 B (A 팔로워)
마지막 로그인 시간 마지막 로그인 시간 N일 이내
Step 2: 사용자 A가 글 작성
----------------------------------------------------
사용자 A 사용자 B (A 팔로워)
글 작성 (트윗 X) 트윗 X Id 추가 대기
Step 3: 팔로워 타임라인에 트윗 추가
----------------------------------------------------
사용자 A 사용자 B (A 팔로워)
글 작성 (트윗 X) 타임라인에 트윗 X Id 추가
좀 더 이해하기 쉽게 그림을 그려왔다.
사용자가 글을 작성한다. 새글은 ID = 1111 로 저장된다.
그리고나서 DB에 저장이되고, REDIS의 타임라인 캐시에 트윗 데이터가 저장된다.
팔로워들의 ID를 KEY로 갖는 타임라인 캐시에 새로 작성된 트윗 ID가 추가된다.
팔로우한 유저가 트위터에 접속하게 되면, 미리 캐시해둔 타임라인의 트윗을 불러온다.
그렇다면 만약에 팔로우가 많은 제니나 손흥민의 경우에도 타임라인 리스트에 모두 추가할까?
답은 x다.
팔로워가 매우 많은 사용자의 경우, 사용자의 타임라인에 따로 셀럽의 게시글 타임라인을 합친다.
팔로워한 포트폴리오 조회 및 포트폴리오 저장 시 , 과정은 위에 설명했던 것처럼 비슷하게 구현했다.
코드를 보며 구현한 과정을 설명하겠다.
먼저 사용자가 글을 작성한다.
@Transactional
public void savePortfolio(String content, List<MultipartFile> imageFiles) {
UserEntity user = userService.getMyUserWithAuthorities();
PortfolioEntity portfolioEntity = PortfolioEntity.builder()
.user(user)
.content(content)
.build();
PortfolioEntity savedPortfolioEntity = portfolioRepository.save(portfolioEntity);
Images uploadedImages = portfolioImageService.uploadImage(imageFiles);
List<Image> imageList = uploadedImages.getImages();
for (Image image : imageList) {
PortfolioImageEntity portfolioImageEntity = PortfolioImageEntity.builder()
.image(image.toImageEntity())
.portfolio(savedPortfolioEntity)
.build();
savedPortfolioEntity.addImage(portfolioImageEntity);
}
cacheLatestPortfolioForUser(user.getId().toString(), savedPortfolioEntity.getId());
List<Long> followerIds = user.getFollowerIds();
cacheNewPortfolioForFollowers(savedPortfolioEntity.getId().toString(), followerIds, savedPortfolioEntity.getCreatedAt().getTime()); }
private void cacheLatestPortfolioForUser(String userId, Long portfolioId) {
String redisKey = "user:" + userId+ ":portfolios";
double timestamp = System.currentTimeMillis();
stringRedisTemplate.opsForZSet().add(redisKey, portfolioId.toString(), timestamp);
}
private void cacheNewPortfolioForFollowers(String newPortfolioId, List<Long> followerIds, double timestamp) {
followerIds.forEach(followerId -> {
String redisKey = "user:" + followerId + ":portfolios";
stringRedisTemplate.opsForZSet().add(redisKey, newPortfolioId, timestamp);
});
}
코드의 길이가 꽤 길어 복잡해 보인다. 하나하나 자세히 살펴보자
UserEntity user = userService.getMyUserWithAuthorities();
PortfolioEntity portfolioEntity = PortfolioEntity.builder()
.user(user)
.content(content)
.build();
PortfolioEntity savedPortfolioEntity = portfolioRepository.save(portfolioEntity);
Images uploadedImages = portfolioImageService.uploadImage(imageFiles);
List<Image> imageList = uploadedImages.getImages();
for (Image image : imageList) {
PortfolioImageEntity portfolioImageEntity = PortfolioImageEntity.builder()
.image(image.toImageEntity())
.portfolio(savedPortfolioEntity)
.build();
savedPortfolioEntity.addImage(portfolioImageEntity);
}
이부분은 작성한 포트폴리오가 DB에 저장되는 코드이다.
이후
cacheLatestPortfolioForUser(user.getId().toString(), savedPortfolioEntity.getId());
redis에 작성한 포트폴리오를 저장하는 함수를 호출한다.
private void cacheLatestPortfolioForUser(String userId, Long portfolioId) {
String redisKey = "user:" + userId+ ":portfolios";
double timestamp = System.currentTimeMillis();
stringRedisTemplate.opsForZSet().add(redisKey, portfolioId.toString(), timestamp);
}
이 떄 list로도 사용할 수 있겠지만, 나는 Sorted Sets를 이용했다.
그 이유는 다음과 같다.
List<Long> followerIds = user.getFollowerIds();
private void cacheNewPortfolioForFollowers(String newPortfolioId, List<Long> followerIds, double timestamp) {
followerIds.forEach(followerId -> {
String redisKey = "user:" + followerId + ":portfolios";
stringRedisTemplate.opsForZSet().add(redisKey, newPortfolioId, timestamp);
});
}
받은 팔로워들의 ID 목록인 followerIds를 forEach를 사용해 순회하며 해당 사용자의 Redis 캐시에 새로운 포트폴리오 정보를 저장한다.
실행시키게 되면
올바르게 저장이 된다.
구현과정이 순탄치 만은 않았다.
redis를 이용해 redis/ db에 저장하는 로직은 금방구현했으나, 자바 직렬화, 역직렳화
그니깐 내부적으로 바이트스트림으로 저장돼서, 자바 어플리케이션 -> redis서버, redis->자바
이 사이에 직렬화 , 역직렬화가 필요한데 설정을 이상하게 해줘서 자꾸 오류가 났다.
RedisConfig에서 <String,Object> redistemplate 이것만 빈 주입해줘서
<String, String> 의 형식을 이용하는 경우에는 value값이 직렬화되지않고 저장되었다.
그래서 <String,String> stringRedisTemplate를 작성하여 빈 주입해서
직렬화 , 역직렬화에 성공하였다.
RedisConfig.java
@Configuration
public class RedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(redisHost, redisPort);
redisConfig.setPassword(redisPassword);
return new LettuceConnectionFactory(redisConfig);
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)))
.entryTtl(Duration.ofMinutes(3L)); //임시 설정값
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.deactivateDefaultTyping(); // 타입 정보 비활성화
return objectMapper;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// Key Serializer configuration
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Value Serializer configuration
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return redisTemplate;
}
@Bean
public RedisTemplate<String, String> StringredisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> StringredisTemplate = new RedisTemplate<>();
StringredisTemplate.setConnectionFactory(redisConnectionFactory);
// Key Serializer configuration
StringredisTemplate.setKeySerializer(new StringRedisSerializer());
StringredisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Value Serializer configuration
StringredisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
StringredisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return StringredisTemplate;
}
}
PortfolioService.java
@Service
@RequiredArgsConstructor
public class PortfolioService {
private final PortfolioRepository portfolioRepository;
private final UserService userService;
private final PortfolioImageService portfolioImageService;
private final RedisTemplate redisTemplate;
private final StringRedisTemplate stringRedisTemplate;
@Transactional
public void savePortfolio(String content, List<MultipartFile> imageFiles) {
UserEntity user = userService.getMyUserWithAuthorities();
PortfolioEntity portfolioEntity = PortfolioEntity.builder()
.user(user)
.content(content)
.build();
PortfolioEntity savedPortfolioEntity = portfolioRepository.save(portfolioEntity);
Images uploadedImages = portfolioImageService.uploadImage(imageFiles);
List<Image> imageList = uploadedImages.getImages();
for (Image image : imageList) {
PortfolioImageEntity portfolioImageEntity = PortfolioImageEntity.builder()
.image(image.toImageEntity())
.portfolio(savedPortfolioEntity)
.build();
savedPortfolioEntity.addImage(portfolioImageEntity);
}
cacheLatestPortfolioForUser(user.getId().toString(), savedPortfolioEntity.getId());
List<Long> followerIds = user.getFollowerIds();
cacheNewPortfolioForFollowers(savedPortfolioEntity.getId().toString(), followerIds, savedPortfolioEntity.getCreatedAt().getTime()); }
private void cacheLatestPortfolioForUser(String userId, Long portfolioId) {
String redisKey = "user:" + userId+ ":portfolios";
double timestamp = System.currentTimeMillis();
stringRedisTemplate.opsForZSet().add(redisKey, portfolioId.toString(), timestamp);
}
private void cacheNewPortfolioForFollowers(String newPortfolioId, List<Long> followerIds, double timestamp) {
followerIds.forEach(followerId -> {
String redisKey = "user:" + followerId + ":portfolios";
stringRedisTemplate.opsForZSet().add(redisKey, newPortfolioId, timestamp);
});
}
이렇게 Redis의 심화편이 끝이났다.