-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Elasticache for Redis를 도입하여 캐싱 적용 #86
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
The head ref may contain hidden characters: "MOCACONG-258-\uBC31-Elasticache"
Changes from all commits
5a52215
fc6dead
e1fa65a
8001de3
b18a3ad
cafa18a
b957b81
421278f
9bdc28d
2325654
759e9d2
fb8a4ea
8e52714
1c9102d
d48648e
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 |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package mocacong.server.config; | ||
|
||
import java.time.Duration; | ||
import org.springframework.beans.factory.annotation.Value; | ||
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.Primary; | ||
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.serializer.GenericJackson2JsonRedisSerializer; | ||
import org.springframework.data.redis.serializer.RedisSerializationContext; | ||
import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
|
||
@EnableCaching | ||
@Configuration | ||
public class RedisCacheConfig { | ||
|
||
private static final long DELTA_TO_AVOID_CONCURRENCY_TIME = 30 * 60 * 1000L; | ||
|
||
@Value("${security.jwt.token.expire-length}") | ||
private long accessTokenValidityInMilliseconds; | ||
|
||
@Bean | ||
@Primary | ||
public CacheManager cafeCacheManager(RedisConnectionFactory redisConnectionFactory) { | ||
/* | ||
* 카페 관련 캐시는 충분히 많이 쌓일 수 있으므로 OOM 방지 차 ttl 12시간으로 설정 | ||
*/ | ||
RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration() | ||
.entryTtl(Duration.ofHours(12L)); | ||
|
||
return RedisCacheManager.RedisCacheManagerBuilder | ||
.fromConnectionFactory(redisConnectionFactory) | ||
.cacheDefaults(redisCacheConfiguration) | ||
.build(); | ||
} | ||
|
||
@Bean | ||
public CacheManager oauthPublicKeyCacheManager(RedisConnectionFactory redisConnectionFactory) { | ||
/* | ||
* public key 갱신은 1년에 몇 번 안되므로 ttl 3일로 설정 | ||
* 유저가 하루 1번 로그인한다고 가정, 최소 1일은 넘기는 것이 좋다고 판단 | ||
*/ | ||
RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration() | ||
.entryTtl(Duration.ofDays(3L)); | ||
return RedisCacheManager.RedisCacheManagerBuilder | ||
.fromConnectionFactory(redisConnectionFactory) | ||
.cacheDefaults(redisCacheConfiguration) | ||
.build(); | ||
} | ||
|
||
@Bean | ||
public CacheManager accessTokenCacheManager(RedisConnectionFactory redisConnectionFactory) { | ||
/* | ||
* accessToken 시간만큼 ttl 설정하되, | ||
* 만료 직전 캐시 조회하여 로그인 안되는 동시성 이슈 방지를 위해 accessToken ttl 보다 30분 일찍 만료 | ||
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. accessToken 관련 캐시 ttl을 12시간으로 하려다가, 모카콩 접속 시간이 30분 이상일 듯하지 않아서 30분으로 설정했습니다. 괜찮은지 모르겠네요 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. 사용자가 모카콩을 오래 접속하진 않을 거 같진 않아서 30분 혹은 1시간으로 설정해도 될 거 같아요 |
||
*/ | ||
RedisCacheConfiguration redisCacheConfiguration = generateCacheConfiguration() | ||
.entryTtl(Duration.ofMillis(accessTokenValidityInMilliseconds - DELTA_TO_AVOID_CONCURRENCY_TIME)); | ||
|
||
return RedisCacheManager.RedisCacheManagerBuilder | ||
.fromConnectionFactory(redisConnectionFactory) | ||
.cacheDefaults(redisCacheConfiguration) | ||
.build(); | ||
} | ||
|
||
private RedisCacheConfiguration generateCacheConfiguration() { | ||
return RedisCacheConfiguration.defaultCacheConfig() | ||
.serializeKeysWith( | ||
RedisSerializationContext.SerializationPair.fromSerializer( | ||
new StringRedisSerializer())) | ||
.serializeValuesWith( | ||
RedisSerializationContext.SerializationPair.fromSerializer( | ||
new GenericJackson2JsonRedisSerializer())); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package mocacong.server.config; | ||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration; | ||
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.StringRedisSerializer; | ||
|
||
@Configuration | ||
public class RedisConfig { | ||
|
||
@Value("${spring.redis.host}") | ||
private String redisHost; | ||
|
||
@Value("${spring.redis.port}") | ||
private int redisPort; | ||
|
||
@Bean | ||
public RedisConnectionFactory redisConnectionFactory() { | ||
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort)); | ||
} | ||
|
||
@Bean | ||
public RedisTemplate<String, Object> redisTemplate() { | ||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); | ||
redisTemplate.setConnectionFactory(redisConnectionFactory()); | ||
redisTemplate.setKeySerializer(new StringRedisSerializer()); | ||
|
||
/* Java 기본 직렬화가 아닌 JSON 직렬화 설정 */ | ||
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); | ||
|
||
return redisTemplate; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
package mocacong.server.security.auth.apple; | ||
|
||
import org.springframework.cache.annotation.Cacheable; | ||
import org.springframework.cloud.openfeign.FeignClient; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
|
||
@FeignClient(name = "apple-public-key-client", url = "https://appleid.apple.com/auth") | ||
public interface AppleClient { | ||
|
||
@Cacheable(value = "oauthPublicKeyCache", cacheManager = "oauthPublicKeyCacheManager") | ||
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.
|
||
@GetMapping("/keys") | ||
ApplePublicKeys getApplePublicKeys(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
import mocacong.server.security.auth.OAuthPlatformMemberResponse; | ||
import mocacong.server.security.auth.apple.AppleOAuthUserProvider; | ||
import mocacong.server.security.auth.kakao.KakaoOAuthUserProvider; | ||
import org.springframework.cache.annotation.Cacheable; | ||
import org.springframework.security.crypto.password.PasswordEncoder; | ||
import org.springframework.stereotype.Service; | ||
|
||
|
@@ -28,6 +29,7 @@ public class AuthService { | |
private final AppleOAuthUserProvider appleOAuthUserProvider; | ||
private final KakaoOAuthUserProvider kakaoOAuthUserProvider; | ||
|
||
@Cacheable(key = "#request.email", value = "accessTokenCache", cacheManager = "accessTokenCacheManager") | ||
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.
|
||
public TokenResponse login(AuthLoginRequest request) { | ||
Member findMember = memberRepository.findByEmail(request.getEmail()) | ||
.orElseThrow(NotFoundMemberException::new); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ | |
import mocacong.server.service.event.DeleteNotUsedImagesEvent; | ||
import mocacong.server.service.event.MemberEvent; | ||
import mocacong.server.support.AwsS3Uploader; | ||
import org.springframework.cache.annotation.CacheEvict; | ||
import org.springframework.cache.annotation.Cacheable; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.context.event.EventListener; | ||
import org.springframework.data.domain.PageRequest; | ||
|
@@ -91,6 +93,7 @@ public FindCafeResponse findCafeByMapId(String email, String mapId) { | |
); | ||
} | ||
|
||
@Cacheable(key = "#mapId", value = "cafePreviewCache", cacheManager = "cafeCacheManager") | ||
@Transactional(readOnly = true) | ||
public PreviewCafeResponse previewCafeByMapId(String email, String mapId) { | ||
Cafe cafe = cafeRepository.findByMapId(mapId) | ||
|
@@ -177,6 +180,7 @@ public MyCommentCafesResponse findMyCommentCafes(String email, int page, int cou | |
return new MyCommentCafesResponse(comments.isLast(), responses); | ||
} | ||
|
||
@CacheEvict(key = "#mapId", value = "cafePreviewCache") | ||
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. 리뷰 작성, 즐겨찾기 작성, 즐겨찾기 삭제 작업이 일어나면 카페 관련 캐시가 삭제되도록 했습니다. |
||
@Transactional | ||
public CafeReviewResponse saveCafeReview(String email, String mapId, CafeReviewRequest request) { | ||
Cafe cafe = cafeRepository.findByMapId(mapId) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
package mocacong.server.service; | ||
|
||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.util.List; | ||
import mocacong.server.domain.*; | ||
import mocacong.server.dto.request.CafeFilterRequest; | ||
import mocacong.server.dto.request.CafeRegisterRequest; | ||
|
@@ -13,21 +16,16 @@ | |
import mocacong.server.repository.*; | ||
import mocacong.server.service.event.DeleteNotUsedImagesEvent; | ||
import mocacong.server.support.AwsS3Uploader; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.mock.mockito.MockBean; | ||
import org.springframework.mock.web.MockMultipartFile; | ||
|
||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
import static org.junit.jupiter.api.Assertions.assertAll; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import static org.mockito.Mockito.doNothing; | ||
import static org.mockito.Mockito.when; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.mock.mockito.MockBean; | ||
import org.springframework.mock.web.MockMultipartFile; | ||
|
||
@ServiceTest | ||
class CafeServiceTest { | ||
|
@@ -196,15 +194,13 @@ void previewCafeWithScore() { | |
scoreRepository.save(new Score(5, member2, cafe)); | ||
favoriteRepository.save(new Favorite(member1, cafe)); | ||
|
||
PreviewCafeResponse actual1 = cafeService.previewCafeByMapId(member1.getEmail(), cafe.getMapId()); | ||
PreviewCafeResponse actual2 = cafeService.previewCafeByMapId(member2.getEmail(), cafe.getMapId()); | ||
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. 캐싱이 일어나서 actual1이 그대로 반환됩니다. 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. 변수명을 actual로 변경하는 거 어떠세요? |
||
PreviewCafeResponse actual = cafeService.previewCafeByMapId(member1.getEmail(), cafe.getMapId()); | ||
|
||
assertAll( | ||
() -> assertThat(actual1.getFavorite()).isTrue(), | ||
() -> assertThat(actual2.getFavorite()).isFalse(), | ||
() -> assertThat(actual1.getScore()).isEqualTo(4.5), | ||
() -> assertThat(actual1.getStudyType()).isNull(), | ||
() -> assertThat(actual1.getReviewsCount()).isEqualTo(0) | ||
() -> assertThat(actual.getFavorite()).isTrue(), | ||
() -> assertThat(actual.getScore()).isEqualTo(4.5), | ||
() -> assertThat(actual.getStudyType()).isNull(), | ||
() -> assertThat(actual.getReviewsCount()).isEqualTo(0) | ||
); | ||
} | ||
|
||
|
@@ -225,15 +221,13 @@ void previewCafeWithScoreAndReview() { | |
"깨끗해요", "없어요", null, "보통이에요")); | ||
favoriteRepository.save(new Favorite(member1, cafe)); | ||
|
||
PreviewCafeResponse actual1 = cafeService.previewCafeByMapId(member1.getEmail(), cafe.getMapId()); | ||
PreviewCafeResponse actual2 = cafeService.previewCafeByMapId(member2.getEmail(), cafe.getMapId()); | ||
PreviewCafeResponse actual = cafeService.previewCafeByMapId(member1.getEmail(), cafe.getMapId()); | ||
|
||
assertAll( | ||
() -> assertThat(actual1.getFavorite()).isTrue(), | ||
() -> assertThat(actual2.getFavorite()).isFalse(), | ||
() -> assertThat(actual1.getScore()).isEqualTo(2.5), | ||
() -> assertThat(actual1.getStudyType()).isEqualTo("group"), | ||
() -> assertThat(actual1.getReviewsCount()).isEqualTo(2) | ||
() -> assertThat(actual.getFavorite()).isTrue(), | ||
() -> assertThat(actual.getScore()).isEqualTo(2.5), | ||
() -> assertThat(actual.getStudyType()).isEqualTo("group"), | ||
() -> assertThat(actual.getReviewsCount()).isEqualTo(2) | ||
); | ||
} | ||
|
||
|
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.
테스트 환경에서는 실제 redis를 쓰는 것을 지양해야될 뿐더러, elasticache를 사용할 수도 없습니다. 그렇기 때문에 테스트용 embedded-redis 를 추가했습니다.
최신 버전
0.7.3
은 Slf4j를 사용하여 logback과 충돌 이슈가 발생합니다. 따라서 0.7.2로 설정했습니다.ozimov/embedded-redis#18 (comment)
+) 저 깃허브 이슈 링크 남기니까, 해당 이슈에 제 닉네임이 박제됐네요..ㅋㅋㅋ