-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat/CK-235] Redis 캐시를 적용한다 #197
Changes from all commits
c389b29
bd0a086
78d6ff1
f609e23
933fb35
828041b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,52 @@ | ||
package co.kirikiri.common.config; | ||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||
import java.time.Duration; | ||
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.context.annotation.Profile; | ||
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.core.RedisTemplate; | ||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; | ||
import org.springframework.data.redis.serializer.RedisSerializationContext; | ||
import org.springframework.data.redis.serializer.RedisSerializer; | ||
import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
|
||
@Configuration | ||
@EnableCaching | ||
@Profile(value = {"prod", "dev", "local"}) | ||
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 RedisTemplate<String, String> redisTemplate() { | ||
public RedisTemplate<String, String> redisTemplate(final RedisConnectionFactory redisConnectionFactory) { | ||
final RedisTemplate<String, String> redisTemplate = new RedisTemplate<>(); | ||
redisTemplate.setConnectionFactory(redisConnectionFactory()); | ||
redisTemplate.setConnectionFactory(redisConnectionFactory); | ||
redisTemplate.setKeySerializer(new StringRedisSerializer()); | ||
redisTemplate.setValueSerializer(new StringRedisSerializer()); | ||
return redisTemplate; | ||
} | ||
|
||
@Bean | ||
public CacheManager redisCacheManager(final RedisConnectionFactory redisConnectionFactory) { | ||
final ObjectMapper objectMapper = new ObjectMapper(); | ||
objectMapper.registerModule(new JavaTimeModule()); | ||
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.EVERYTHING); | ||
|
||
final RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(objectMapper); | ||
|
||
final RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() | ||
.entryTtl(Duration.ofMinutes(30L)) | ||
.disableCachingNullValues() | ||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer)); | ||
|
||
return RedisCacheManager.RedisCacheManagerBuilder | ||
.fromConnectionFactory(redisConnectionFactory) | ||
.cacheDefaults(redisCacheConfiguration) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,10 @@ | ||
package co.kirikiri.persistence.auth; | ||
|
||
import java.util.Optional; | ||
import java.util.concurrent.TimeUnit; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
public class RefreshTokenRepository { | ||
public interface RefreshTokenRepository { | ||
|
||
private final RedisTemplate<String, String> redisTemplate; | ||
private final Long refreshTokenValidityInSeconds; | ||
void save(final String refreshToken, final String memberIdentifier); | ||
|
||
public RefreshTokenRepository(final RedisTemplate<String, String> redisTemplate, | ||
@Value("${jwt.refresh-token-validity-in-seconds}") final Long refreshTokenValidityInSeconds) { | ||
this.redisTemplate = redisTemplate; | ||
this.refreshTokenValidityInSeconds = refreshTokenValidityInSeconds; | ||
} | ||
|
||
public void save(final String refreshToken, final String memberIdentifier) { | ||
final long timeToLiveSeconds = refreshTokenValidityInSeconds / 1000; | ||
|
||
redisTemplate.opsForValue() | ||
.set(refreshToken, memberIdentifier, timeToLiveSeconds, TimeUnit.SECONDS); | ||
} | ||
|
||
public Optional<String> findMemberIdentifierByRefreshToken(final String refreshToken) { | ||
final String memberIdentifier = redisTemplate.opsForValue().get(refreshToken); | ||
if (memberIdentifier == null) { | ||
return Optional.empty(); | ||
} | ||
return Optional.of(memberIdentifier); | ||
} | ||
Optional<String> findMemberIdentifierByRefreshToken(final String refreshToken); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package co.kirikiri.persistence.auth; | ||
|
||
import java.util.Optional; | ||
import java.util.concurrent.TimeUnit; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
public class RefreshTokenRepositoryImpl implements RefreshTokenRepository { | ||
|
||
private final RedisTemplate<String, String> redisTemplate; | ||
private final Long refreshTokenValidityInSeconds; | ||
|
||
public RefreshTokenRepositoryImpl(final RedisTemplate<String, String> redisTemplate, | ||
@Value("${jwt.refresh-token-validity-in-seconds}") final Long refreshTokenValidityInSeconds) { | ||
this.redisTemplate = redisTemplate; | ||
this.refreshTokenValidityInSeconds = refreshTokenValidityInSeconds; | ||
} | ||
|
||
@Override | ||
public void save(final String refreshToken, final String memberIdentifier) { | ||
final long timeToLiveSeconds = refreshTokenValidityInSeconds / 1000; | ||
|
||
redisTemplate.opsForValue() | ||
.set(refreshToken, memberIdentifier, timeToLiveSeconds, TimeUnit.SECONDS); | ||
} | ||
|
||
@Override | ||
public Optional<String> findMemberIdentifierByRefreshToken(final String refreshToken) { | ||
final String memberIdentifier = redisTemplate.opsForValue().get(refreshToken); | ||
if (memberIdentifier == null) { | ||
return Optional.empty(); | ||
} | ||
return Optional.of(memberIdentifier); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package co.kirikiri.service; | ||
|
||
import java.lang.reflect.Method; | ||
import java.util.Arrays; | ||
import java.util.stream.Collectors; | ||
import org.springframework.cache.interceptor.KeyGenerator; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class CacheKeyGenerator implements KeyGenerator { | ||
|
||
private static final String DELIMITER = "_"; | ||
|
||
@Override | ||
public Object generate(final Object target, final Method method, final Object... params) { | ||
return String.format("%s-%s-%s", | ||
target.getClass().getSimpleName(), | ||
method.getName(), | ||
arrayToDelimitedString(params)); | ||
} | ||
|
||
private Object arrayToDelimitedString(final Object... params) { | ||
return Arrays.stream(params) | ||
.map(o -> o == null ? "" : String.valueOf(o.hashCode())) | ||
.collect(Collectors.joining(DELIMITER)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ | |
import java.net.URL; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.cache.annotation.Cacheable; | ||
import org.springframework.http.HttpMethod; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
@@ -68,6 +69,7 @@ public class RoadmapReadService { | |
private final MemberRepository memberRepository; | ||
private final FileService fileService; | ||
|
||
@Cacheable(value = "roadmap", keyGenerator = "cacheKeyGenerator", cacheManager = "redisCacheManager") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로드맵 조회와 로드맵 목록 조회 등등 각각의 API 마다 레디스에 따로 저장하게 되는 방식인거죠? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 썬샷이 남겨주신 것 처럼 어느 기준치를 넘었을 때는 삭제되거나 어떤 조치가 취해질 것 같은데, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그런 경우를 대비해서 ttl을 30분으로 잡았는데 더 복잡한 방법은 직접 AOP 메소드를 구현해서 할 수 있을 거 같아요! 다같이 전략을 정해보면 어떨까요 |
||
public RoadmapResponse findRoadmap(final Long id) { | ||
final Roadmap roadmap = findRoadmapById(id); | ||
final RoadmapContent recentRoadmapContent = findRecentContent(roadmap); | ||
|
@@ -128,6 +130,7 @@ private RoadmapContent findRecentContent(final Roadmap roadmap) { | |
.orElseThrow(() -> new NotFoundException("로드맵에 컨텐츠가 존재하지 않습니다.")); | ||
} | ||
|
||
@Cacheable(value = "roadmapList", keyGenerator = "cacheKeyGenerator", cacheManager = "redisCacheManager") | ||
public RoadmapForListResponses findRoadmapsByOrderType(final Long categoryId, | ||
final RoadmapOrderTypeRequest orderTypeRequest, | ||
final CustomScrollRequest scrollRequest) { | ||
|
@@ -148,7 +151,7 @@ private RoadmapCategory findCategoryById(final Long categoryId) { | |
.orElseThrow(() -> new NotFoundException("존재하지 않는 카테고리입니다. categoryId = " + categoryId)); | ||
} | ||
|
||
public RoadmapForListScrollDto makeRoadmapForListScrollDto(final List<Roadmap> roadmaps, final int requestSize) { | ||
private RoadmapForListScrollDto makeRoadmapForListScrollDto(final List<Roadmap> roadmaps, final int requestSize) { | ||
final List<RoadmapForListDto> roadmapForListDtos = roadmaps.stream() | ||
.map(this::makeRoadmapForListDto) | ||
.toList(); | ||
|
@@ -200,6 +203,7 @@ public RoadmapForListResponses search(final RoadmapOrderTypeRequest orderTypeReq | |
return RoadmapMapper.convertRoadmapResponses(roadmapForListScrollDto); | ||
} | ||
|
||
@Cacheable(value = "categoryList", keyGenerator = "cacheKeyGenerator", cacheManager = "redisCacheManager") | ||
public List<RoadmapCategoryResponse> findAllRoadmapCategories() { | ||
final List<RoadmapCategory> roadmapCategories = roadmapCategoryRepository.findAll(); | ||
return RoadmapMapper.convertRoadmapCategoryResponses(roadmapCategories); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package co.kirikiri.integration.helper; | ||
|
||
import static co.kirikiri.integration.fixture.MemberAPIFixture.DEFAULT_IDENTIFIER; | ||
|
||
import co.kirikiri.persistence.auth.RefreshTokenRepository; | ||
import java.util.Optional; | ||
|
||
public class TestRefreshTokenRepository implements RefreshTokenRepository { | ||
younghoondoodoom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@Override | ||
public void save(final String refreshToken, final String memberIdentifier) { | ||
} | ||
|
||
@Override | ||
public Optional<String> findMemberIdentifierByRefreshToken(final String refreshToken) { | ||
return Optional.of(DEFAULT_IDENTIFIER); | ||
} | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
왜 파라미터까지 하나씩 다 받고 key에 추가해줄까 잘 몰랐는데 생각해보니 파라미터까지 동일한 요청인지 판별해야 해서 그런거더라구요. 좋은데요? 😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제 머리로는 이게 일단 최선이라고 생각했는데 캐시 key를 생성하는 전략에 대해서 좀 고민해봐야할거 같아요. 다 같이 한번 얘기해봤으면 좋겠어요!