Skip to content
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: 반려동물 식품 로컬 캐시 적용 #540

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public Long createPrimaryIngredient(PrimaryIngredientCreateRequest request) {
return primaryIngredientRepository.save(primaryIngredient).getId();
}

@CacheEvict(cacheNames = "petFoods", keyGenerator = "customKeyGenerator")
public Long createPetFood(PetFoodCreateRequest request, String imageUrl) {
Brand brand = brandRepository.getById(request.brandId());
PetFood petFood = request.toEntity(brand, imageUrl);
Expand Down
12 changes: 9 additions & 3 deletions backend/src/main/java/zipgo/common/cache/CacheType.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package zipgo.common.cache;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum CacheType {

BREEDS("breeds", 1),
BREEDS("breeds"),
PET_FOODS("petFoods")
;

CacheType(String name) {
this.name = name;
this.maxSize = 10000;
this.expireTime = 3000;
}

private final String name;
private final int maxSize;
private final long expireTime;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package zipgo.common.cache;

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

public class CustomKeyGenerator implements KeyGenerator {

@Override
public Object generate(Object target, Method method, Object... params) {
return StringUtils.arrayToDelimitedString(params, "_");
}
}
9 changes: 9 additions & 0 deletions backend/src/main/java/zipgo/common/config/CacheConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import zipgo.common.cache.CacheType;
import zipgo.common.cache.CustomKeyGenerator;

@EnableCaching
@Configuration
Expand All @@ -33,7 +36,13 @@ private List<CaffeineCache> caches() {
private Cache<Object, Object> cache(CacheType cacheType) {
return Caffeine.newBuilder()
.maximumSize(cacheType.getMaxSize())
.expireAfterWrite(cacheType.getExpireTime(), TimeUnit.SECONDS)
.build();
}


@Bean("customKeyGenerator")
public KeyGenerator keyGenerator() {
return new CustomKeyGenerator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datadog.trace.api.Trace;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import zipgo.brand.domain.Brand;
Expand Down Expand Up @@ -31,6 +32,7 @@ public class PetFoodQueryService {
private final FunctionalityRepository functionalityRepository;
private final PrimaryIngredientRepository primaryIngredientRepository;

@Cacheable(cacheNames = "petFoods", keyGenerator = "customKeyGenerator")
public GetPetFoodsResponse getPetFoodsByFilters(FilterRequest filterDto, Long lastPetFoodId, int size) {
return GetPetFoodsResponse.from(
getPetFoodsCount(filterDto),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import static zipgo.brand.domain.QBrand.brand;
import static zipgo.petfood.domain.QPetFood.petFood;
import static zipgo.petfood.domain.QPetFoodFunctionality.petFoodFunctionality;
import static zipgo.petfood.domain.QPetFoodPrimaryIngredient.petFoodPrimaryIngredient;
import static zipgo.petfood.domain.QPrimaryIngredient.primaryIngredient;

Expand Down Expand Up @@ -45,8 +44,6 @@ public List<GetPetFoodQueryResponse> findPagingPetFoods(
)
.from(petFood)
.innerJoin(petFood.brand, brand)
.innerJoin(petFood.petFoodPrimaryIngredients, petFoodPrimaryIngredient)
.innerJoin(petFood.petFoodFunctionalities, petFoodFunctionality)
.where(
isLessThan(lastPetFoodId),
isContainBrand(brandsName),
Expand All @@ -55,7 +52,6 @@ public List<GetPetFoodQueryResponse> findPagingPetFoods(
isContainFunctionalities(functionalityList)
)
.orderBy(petFood.id.desc())
.groupBy(petFood.id)
.limit(size)
.fetch();
}
Expand Down Expand Up @@ -117,8 +113,6 @@ public Long findPetFoodsCount(
.select(petFood.id.countDistinct())
.from(petFood)
.join(petFood.brand, brand)
.join(petFood.petFoodPrimaryIngredients, petFoodPrimaryIngredient)
.join(petFood.petFoodFunctionalities, petFoodFunctionality)
.where(
isContainBrand(brandsName),
isMeetStandardCondition(standards),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package zipgo.petfood.application;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import zipgo.brand.domain.Brand;
import zipgo.brand.domain.repository.BrandRepository;
import zipgo.common.cache.CacheType;
import zipgo.common.service.ServiceTest;
import zipgo.petfood.domain.Functionality;
import zipgo.petfood.domain.PetFood;
Expand All @@ -23,6 +26,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import static java.util.Collections.EMPTY_LIST;
import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -50,7 +54,14 @@

class PetFoodQueryServiceTest extends ServiceTest {

private static final int size = 20;
private static final int SIZE = 20;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

중요하지 않은 변경사항은 따로 커밋하면 좋은 것 같아요!
저도 이제 연습해보고 있는건데, 히스토리를 관리하는 센스가 중요한 것 같더라구요.
나중에 읽는 사람(높은 확률로 나 자신)한테 문서보다 훨씬 좋은 설명이기 때문에..
무민도 저와 함께 중요하고 관련있는 변경사항들이 히스토리에 같이 모여있을 수 있게 노력해봅시당

private static final String CACHE_KEY_DELIMITER = "_";
private static final FilterRequest EMPTY_FILTER_REQUEST = FilterRequest.of(
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST
);

@Autowired
private PetFoodQueryService petFoodQueryService;
Expand All @@ -67,6 +78,14 @@ class PetFoodQueryServiceTest extends ServiceTest {
@Autowired
private FunctionalityRepository functionalityRepository;

@Autowired
private CacheManager cacheManager;
Comment on lines +81 to +82
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저라면, 이 부분은 따로 테스트를 했을 것 같아요.
무민은 캐시 속을 검증하기 위해 customGenerator를 사용했지만 관점을 달리해서.. 캐시를 사용하는 서비스만 테스트 해볼 수 있지 않을까요?

그런 관점에서 mock test를 하면.. 캐시 매니저 구현에는 의존하지 않을 것 같아요 (사실 simpleGenerator 로직에 문제가 없었던 건 아니니까...! 바꿀 필요 없어짐)

그럼 reposiroty 구현체를 스터빙할 수 있고 (mockito 사용하면) 다음처럼 테스트할 수 있어요.

  • 캐시 히트 시 Repository 호출하지 않음을 테스트
  • 캐시 미스 시 Repository 호출 테스트
  • 업데이트 시 Repository 호출 테스트

이런 식으로, 서비스 행위 테스트가 되겠죵..

단점은 .. 시간이란 비용과.. 목테스트를 최소화하는 우리 프로젝트의 별종케이스가 되는 것.. 그리고 캐시 클래스 자체의 기능은 테스트 하지 않는다는거? -> 근데 이건 스프링이 제공하는 거라 학습테스트나 다름 없을 것 같아요.
아무튼 제 생각은 그렇슴니다

무민이 질문해줘서 재밌게 읽었어요 . 감사합니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나 왜 이렇게
.. <- 이거 많이 써요?
원래 그랬나


@BeforeEach
void setUp() {
Objects.requireNonNull(cacheManager.getCache(CacheType.PET_FOODS.getName())).clear();
}

@Nested
class 필터_조회 {

Expand Down Expand Up @@ -99,7 +118,7 @@ class 필터_조회 {
EMPTY_LIST
),
lastPetFoodId,
size
SIZE
);

//then
Expand Down Expand Up @@ -143,7 +162,7 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {
EMPTY_LIST
),
lastPetFoodId,
size
SIZE
);

// then
Expand Down Expand Up @@ -183,7 +202,7 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {
EMPTY_LIST
),
lastPetFoodId,
size
SIZE
);

// then
Expand Down Expand Up @@ -226,7 +245,7 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {
EMPTY_LIST
),
lastPetFoodId,
size
SIZE
);

// then
Expand Down Expand Up @@ -266,7 +285,7 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {
List.of("튼튼")
),
lastPetFoodId,
size
SIZE
);

// then
Expand Down Expand Up @@ -307,7 +326,7 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {
List.of("튼튼")
),
lastPetFoodId,
size
SIZE
);

// then
Expand Down Expand Up @@ -340,14 +359,9 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {

// when
GetPetFoodsResponse petFoodsResponse = petFoodQueryService.getPetFoodsByFilters(
FilterRequest.of(
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST
),
EMPTY_FILTER_REQUEST,
lastPetFoodId,
size
SIZE
);

// then
Expand All @@ -373,14 +387,9 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {

// when
GetPetFoodsResponse petFoodsResponse = petFoodQueryService.getPetFoodsByFilters(
FilterRequest.of(
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST
),
EMPTY_FILTER_REQUEST,
유럽_영양기준_만족_식품.getId() - 1,
size
SIZE
);

// then
Expand All @@ -401,14 +410,9 @@ private Long getLastPetFoodId(List<PetFood> allFoods) {

// when
GetPetFoodsResponse petFoodsResponse = petFoodQueryService.getPetFoodsByFilters(
FilterRequest.of(
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST,
EMPTY_LIST
),
EMPTY_FILTER_REQUEST,
null,
size
SIZE
);

// then
Expand All @@ -433,6 +437,44 @@ private boolean isLatest(long id, List<PetFoodResponse> petFoodResponses) {
return true;
}

@Test
void 로컬_캐시가_있으면_로컬_캐시에서_데이터를_조회한다() {
//given
PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
petFoodRepository.save(모든_영양기준_만족_식품);

// when
GetPetFoodsResponse petFoodsResponse = petFoodQueryService.getPetFoodsByFilters(
EMPTY_FILTER_REQUEST,
null,
SIZE
);

// then
assertThat(petFoodsResponse).isEqualTo(getCachedPetFoodsResponse());
}

private GetPetFoodsResponse getCachedPetFoodsResponse() {
String key = FilterRequest.of(EMPTY_LIST, EMPTY_LIST, EMPTY_LIST, EMPTY_LIST)
+ CACHE_KEY_DELIMITER + null + CACHE_KEY_DELIMITER + SIZE;
return Objects.requireNonNull(cacheManager.getCache(CacheType.PET_FOODS.getName()))
.get(key, GetPetFoodsResponse.class);
}

@Test
void 로컬_캐시가_없으면_로컬_캐시에서_데이터를_조회하지_못한다() {
//given
PetFood 모든_영양기준_만족_식품 = 모든_영양기준_만족_식품(brandRepository.save(아카나_식품_브랜드_생성()));
식품_기능성_추가(모든_영양기준_만족_식품, functionalityRepository.save(기능성_튼튼()));
식품_주원료_추가(모든_영양기준_만족_식품, primaryIngredientRepository.save(주원료_소고기()));
petFoodRepository.save(모든_영양기준_만족_식품);

// then
final GetPetFoodsResponse cachedPetFoodsResponse = getCachedPetFoodsResponse();
assertThat(cachedPetFoodsResponse).isNull();
}
}

@Nested
Expand Down