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

refactor: 봉사 모집글 목록 조회 캐시를 개선한다. #461

Merged
merged 5 commits into from
Apr 19, 2024
Merged
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 @@ -11,12 +11,6 @@ public record FindRecruitmentsResponse(
List<FindRecruitmentResponse> recruitments,
PageInfo pageInfo) {

public static FindRecruitmentsResponse fromCached(
Slice<FindRecruitmentResponse> cachedRecruitments, Long count) {
PageInfo pageInfo = PageInfo.of(count, cachedRecruitments.hasNext());
return new FindRecruitmentsResponse(cachedRecruitments.getContent(), pageInfo);
}

public record FindRecruitmentResponse(
Long recruitmentId,
String recruitmentTitle,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
package com.clova.anifriends.domain.recruitment.repository;

import com.clova.anifriends.domain.recruitment.Recruitment;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsResponse.FindRecruitmentResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsResponse;

public interface RecruitmentCacheRepository {

void saveRecruitment(Recruitment recruitment);
long getTotalNumberOfRecruitments();

Slice<FindRecruitmentResponse> findRecruitments(Pageable pageable);
void saveRecruitment(Recruitment recruitment);

void updateRecruitment(Recruitment recruitment);
long deleteRecruitment(Recruitment recruitment);

void deleteRecruitment(Recruitment recruitment);
FindRecruitmentsResponse findRecruitments(int size);

void closeRecruitmentsIfNeedToBe();

Long getRecruitmentCount();

void saveRecruitmentCount(Long count);

void increaseRecruitmentCount();

void decreaseToRecruitmentCount();
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package com.clova.anifriends.domain.recruitment.repository;

import com.clova.anifriends.domain.common.PageInfo;
import com.clova.anifriends.domain.recruitment.Recruitment;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsResponse;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsResponse.FindRecruitmentResponse;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
Expand All @@ -24,10 +24,7 @@ public class RecruitmentRedisRepository implements RecruitmentCacheRepository {
private static final String RECRUITMENT_KEY = "recruitment";
private static final String RECRUITMENT_COUNT_KEY = "recruitment:count";
private static final int UNTIL_LAST_ELEMENT = -1;
private static final Long RECRUITMENT_COUNT_NO_CACHE = -1L;
private static final Long COUNT_ONE = 1L;
private static final int MAX_CACHED_SIZE = 30;
private static final int PAGE_SIZE = 20;
private static final int ZERO = 0;
public static final ZoneOffset CREATED_AT_SCORE_TIME_ZONE = ZoneOffset.UTC;

Expand All @@ -43,89 +40,82 @@ public RecruitmentRedisRepository(
this.recruitmentRepository = recruitmentRepository;
}

/**
* 봉사 모집글을 캐시에 저장하고 캐싱된 카운트를 증가시킵니다.
*
* @param recruitment
*/
@Override
public void saveRecruitment(final Recruitment recruitment) {
FindRecruitmentResponse recruitmentResponse = FindRecruitmentResponse.from(recruitment);
long createdAtScore = getCreatedAtScore(recruitment);
cachedRecruitments.add(RECRUITMENT_KEY, recruitmentResponse, createdAtScore);
cachedRecruitmentsCount.increment(RECRUITMENT_COUNT_KEY);
trimCache();
}

popUntilCachedSize();
private long getCreatedAtScore(Recruitment recruitment) {
return recruitment.getCreatedAt().toEpochSecond(CREATED_AT_SCORE_TIME_ZONE);
}

private void popUntilCachedSize() {
Set<FindRecruitmentResponse> recruitments
= cachedRecruitments.range(RECRUITMENT_KEY, ZERO, UNTIL_LAST_ELEMENT);
if (Objects.nonNull(recruitments)) {
int needToRemoveSize = recruitments.size() - MAX_CACHED_SIZE;
needToRemoveSize = Math.max(needToRemoveSize, ZERO);
cachedRecruitments.popMin(RECRUITMENT_KEY, needToRemoveSize);
}
private void trimCache() {
cachedRecruitments.removeRange(RECRUITMENT_KEY, ZERO, -MAX_CACHED_SIZE - 1L);
}

/**
* 캐시된 Recruitment dto 리스트를 size만큼 조회합니다. size가 지정된 최대 size를 초과하는 경우 지정된 최대 size만큼 조회해옵니다.
* 캐시된 Recruitment dto 리스트를 첫번째 요소부터 size만큼 조회합니다. size가 최대 캐싱 사이즈를 초과하는 경우 db에서 조회합니다.
*
* @param pageable 조회해 올 리스트 pageable. 최대 사이즈 20
* @return 캐시된 Recruitment dto 리스트
* @param size 조회할 사이즈
* @return FindRecruitmentsResponse 캐시 혹은 db에서 조회한 결과
*/
@Override
public Slice<FindRecruitmentResponse> findRecruitments(Pageable pageable) {
long size = pageable.getPageSize();
if (size > PAGE_SIZE) {
size = PAGE_SIZE;
public FindRecruitmentsResponse findRecruitments(int size) {
Set<FindRecruitmentResponse> recruitments = Objects.requireNonNull(
cachedRecruitments.reverseRange(RECRUITMENT_KEY, ZERO, size - 1L));
long count = getTotalNumberOfRecruitments();
PageInfo pageInfo = PageInfo.of(count, count > size);
if (recruitments.size() >= size) {
List<FindRecruitmentResponse> content = recruitments.stream()
.limit(size)
.toList();
return new FindRecruitmentsResponse(content, pageInfo);
}
Set<FindRecruitmentResponse> recruitments
= cachedRecruitments.reverseRange(RECRUITMENT_KEY, ZERO, size);
if (Objects.isNull(recruitments)) {
return new SliceImpl<>(List.of());
}
List<FindRecruitmentResponse> content = recruitments.stream()
.limit(size)
.toList();
boolean hasNext = recruitments.size() > size;
return new SliceImpl<>(content, pageable, hasNext);
}

@Override
public void updateRecruitment(final Recruitment recruitment) {
long createdAtScore = getCreatedAtScore(recruitment);
Set<FindRecruitmentResponse> recruitments = cachedRecruitments.rangeByScore(
RECRUITMENT_KEY, createdAtScore, createdAtScore);
if (Objects.nonNull(recruitments)) {
Optional<FindRecruitmentResponse> oldCachedRecruitment = recruitments.stream()
.filter(findRecruitmentResponse -> isEqualsId(recruitment, findRecruitmentResponse))
.findFirst();
oldCachedRecruitment.ifPresent(oldRecruitment -> {
cachedRecruitments.remove(RECRUITMENT_KEY, oldRecruitment);
FindRecruitmentResponse updatedRecruitment
= FindRecruitmentResponse.from(recruitment);
cachedRecruitments.add(RECRUITMENT_KEY, updatedRecruitment, createdAtScore);
});
}
}

private long getCreatedAtScore(Recruitment recruitment) {
return recruitment.getCreatedAt().toEpochSecond(CREATED_AT_SCORE_TIME_ZONE);
PageRequest pageRequest = PageRequest.of(ZERO, size);
List<FindRecruitmentResponse> content = getRecruitmentsV2(pageRequest)
.map(FindRecruitmentResponse::from)
.toList();
return new FindRecruitmentsResponse(content, pageInfo);
}

private boolean isEqualsId(
Recruitment recruitment,
FindRecruitmentResponse findRecruitmentResponse) {
return findRecruitmentResponse.recruitmentId().equals(recruitment.getRecruitmentId());
private Slice<Recruitment> getRecruitmentsV2(PageRequest pageRequest) {
return recruitmentRepository.findRecruitmentsV2(null, null,
null, null, null, null, null, pageRequest);
}

/**
* 캐시된 봉사 모집글을 제거하고 카운트를 감소시킵니다.
*
* @param recruitment
* @return
*/
@Override
public void deleteRecruitment(final Recruitment recruitment) {
public long deleteRecruitment(final Recruitment recruitment) {
FindRecruitmentResponse recruitmentResponse = FindRecruitmentResponse.from(recruitment);
cachedRecruitments.remove(RECRUITMENT_KEY, recruitmentResponse);
Long number = cachedRecruitments.remove(RECRUITMENT_KEY, recruitmentResponse);
cachedRecruitmentsCount.decrement(RECRUITMENT_COUNT_KEY);
return Objects.isNull(number) ? 0 : number;
}

/**
* 캐시된 봉사 모집글 중 모집 기간이 종료된 요소를 업데이트합니다.
*/
@Override
public void closeRecruitmentsIfNeedToBe() {
LocalDateTime now = LocalDateTime.now();
Set<FindRecruitmentResponse> findRecruitments = cachedRecruitments.range(RECRUITMENT_KEY,
ZERO, UNTIL_LAST_ELEMENT);
if(Objects.nonNull(findRecruitments)) {
if (Objects.nonNull(findRecruitments)) {
Map<FindRecruitmentResponse, FindRecruitmentResponse> cachedKeyAndUpdatedValue
= new HashMap<>();
findRecruitments.stream()
Expand Down Expand Up @@ -169,50 +159,13 @@ private FindRecruitmentResponse closeCachedRecruitment(FindRecruitmentResponse r
}

@Override
public Long getRecruitmentCount() {
public long getTotalNumberOfRecruitments() {
Object cachedCount = cachedRecruitmentsCount.get(RECRUITMENT_COUNT_KEY);

if (Objects.isNull(cachedCount)) {
return RECRUITMENT_COUNT_NO_CACHE;
}

if (cachedCount instanceof Long) {
return (Long) cachedCount;
} else {
return ((Integer) cachedCount).longValue();
}
}

@Override
public void saveRecruitmentCount(
Long count
) {
cachedRecruitmentsCount.set(RECRUITMENT_COUNT_KEY, count);
}

@Override
public void increaseRecruitmentCount() {
Object cachedCount = cachedRecruitmentsCount.get(RECRUITMENT_COUNT_KEY);

if (Objects.nonNull(cachedCount)) {
saveRecruitmentCount((Integer) cachedCount + COUNT_ONE);
}

long dbRecruitmentCount = recruitmentRepository.count();

saveRecruitmentCount(dbRecruitmentCount);
}

@Override
public void decreaseToRecruitmentCount() {
Object cachedCount = cachedRecruitmentsCount.get(RECRUITMENT_COUNT_KEY);

if (Objects.nonNull(cachedCount)) {
saveRecruitmentCount((Integer) cachedCount - COUNT_ONE);
return ((Integer) cachedCount).longValue();
}

long dbRecruitmentCount = recruitmentRepository.count();

saveRecruitmentCount(dbRecruitmentCount);
long dbCount = recruitmentRepository.count();
cachedRecruitmentsCount.set(RECRUITMENT_COUNT_KEY, dbCount);
return dbCount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentDetailResponse;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsByShelterResponse;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsResponse;
import com.clova.anifriends.domain.recruitment.dto.response.FindRecruitmentsResponse.FindRecruitmentResponse;
import com.clova.anifriends.domain.recruitment.dto.response.FindShelterRecruitmentsResponse;
import com.clova.anifriends.domain.recruitment.dto.response.RegisterRecruitmentResponse;
import com.clova.anifriends.domain.recruitment.exception.RecruitmentNotFoundException;
Expand All @@ -31,8 +30,6 @@
@RequiredArgsConstructor
public class RecruitmentService {

private static final Long RECRUITMENT_COUNT_NO_CACHE = -1L;

private final ShelterRepository shelterRepository;
private final RecruitmentRepository recruitmentRepository;
private final ApplicationEventPublisher applicationEventPublisher;
Expand Down Expand Up @@ -61,7 +58,6 @@ public RegisterRecruitmentResponse registerRecruitment(

recruitmentRepository.save(recruitment);
recruitmentCacheRepository.saveRecruitment(recruitment);
recruitmentCacheRepository.increaseRecruitmentCount();

return RegisterRecruitmentResponse.from(recruitment);
}
Expand Down Expand Up @@ -144,37 +140,17 @@ public FindRecruitmentsResponse findRecruitmentsV2(
Long recruitmentId,
Pageable pageable
) {
Long count = recruitmentCacheRepository.getRecruitmentCount();
if (findRecruitmentsWithoutCondition(keyword, startDate, endDate, isClosed,
keywordCondition, recruitmentId)) {
if (Objects.equals(count, RECRUITMENT_COUNT_NO_CACHE)) {
count = recruitmentRepository.countFindRecruitmentsV2(
keyword,
startDate,
endDate,
isClosed,
keywordCondition
);
recruitmentCacheRepository.saveRecruitmentCount(count);
}
} else {
count = recruitmentRepository.countFindRecruitmentsV2(
keyword,
startDate,
endDate,
isClosed,
keywordCondition
);
if (isFirstPage(keyword, startDate, endDate, isClosed, keywordCondition, recruitmentId)) {
return recruitmentCacheRepository.findRecruitments(pageable.getPageSize());
}

if (findRecruitmentsWithoutCondition(keyword, startDate, endDate, isClosed,
keywordCondition, recruitmentId)) {
Slice<FindRecruitmentResponse> cachedRecruitments
= recruitmentCacheRepository.findRecruitments(pageable);
if (canTrustCached(cachedRecruitments)) {
return FindRecruitmentsResponse.fromCached(cachedRecruitments, count);
}
}
long count = recruitmentRepository.countFindRecruitmentsV2(
keyword,
startDate,
endDate,
isClosed,
keywordCondition
);
Slice<Recruitment> recruitments = recruitmentRepository.findRecruitmentsV2(
keyword,
startDate,
Expand All @@ -187,11 +163,7 @@ public FindRecruitmentsResponse findRecruitmentsV2(
return FindRecruitmentsResponse.fromV2(recruitments, count);
}

private boolean canTrustCached(Slice<FindRecruitmentResponse> cachedRecruitments) {
return cachedRecruitments.hasNext();
}

private boolean findRecruitmentsWithoutCondition(String keyword, LocalDate startDate,
private boolean isFirstPage(String keyword, LocalDate startDate,
LocalDate endDate, Boolean isClosed, KeywordCondition keywordCondition, Long recruitmentId) {
return Objects.isNull(keyword) && Objects.isNull(keywordCondition)
&& Objects.isNull(startDate) && Objects.isNull(endDate) && Objects.isNull(isClosed)
Expand All @@ -201,8 +173,11 @@ private boolean findRecruitmentsWithoutCondition(String keyword, LocalDate start
@Transactional
public void closeRecruitment(Long shelterId, Long recruitmentId) {
Recruitment recruitment = getRecruitmentByShelter(shelterId, recruitmentId);
recruitmentCacheRepository.updateRecruitment(recruitment);
long deleted = recruitmentCacheRepository.deleteRecruitment(recruitment);
recruitment.closeRecruitment();
if(deleted > 0) {
recruitmentCacheRepository.saveRecruitment(recruitment);
}
}

@Transactional
Expand All @@ -218,6 +193,7 @@ public void updateRecruitment(
List<String> imageUrls
) {
Recruitment recruitment = getRecruitmentByShelterWithImages(shelterId, recruitmentId);
long deleted = recruitmentCacheRepository.deleteRecruitment(recruitment);

List<String> imagesToDelete = recruitment.findImagesToDelete(imageUrls);
applicationEventPublisher.publishEvent(new ImageDeletionEvent(imagesToDelete));
Expand All @@ -231,7 +207,9 @@ public void updateRecruitment(
content,
imageUrls
);
recruitmentCacheRepository.updateRecruitment(recruitment);
if(deleted > 0) {
recruitmentCacheRepository.saveRecruitment(recruitment);
}
}

@Transactional
Expand All @@ -244,7 +222,6 @@ public void deleteRecruitment(Long shelterId, Long recruitmentId) {

recruitmentRepository.delete(recruitment);
recruitmentCacheRepository.deleteRecruitment(recruitment);
recruitmentCacheRepository.decreaseToRecruitmentCount();
}

private Recruitment getRecruitmentByShelterWithImages(Long shelterId, Long recruitmentId) {
Expand Down
Loading
Loading