Home 기존 페이징 + shuffle 구현 방식 문제 with 캐싱, 성능개선, 여전히 남은 문제
Post
Cancel

기존 페이징 + shuffle 구현 방식 문제 with 캐싱, 성능개선, 여전히 남은 문제

페이징과 무한스크롤 구현은 제대로 되었지만 페이징 + shuffle을 고려해 구현했던 로직이, 테스트를 해보니 문제가 잘 해결되지 않았다.

여전히 매 페이지 요청 시 웹툰 목록을 섞어 0페이지에 있던 항목이 2페이지에서 보이는 등의 문제가 발생한다.

이 문제는 사용자 경험과 제대로 된 평가 데이터 수집을 위해 꼭 수정해야 하는 부분이기 때문에 타협하지 않고 다시 해결 방법을 찾아야 한다.

해결 방법

보통 성능 개선을 위해 고려하는 방법인 캐싱을 이용하기로 했다.

결론부터 말하자면 기존 문제의 해결과 함께 덤으로 성능 개선 효과도 얻을 수 있었다.

기존 로직은 데이터를 조회 후 섞은 다음 페이징 처리를 한다. 문제는 매 요청 시 shuffle 로직이 계속 수행된다는 것이다.

스크롤을 내릴 때 페이지 요청이 발생하는데, 그 요청때마다 shuffle이 수행되면서 기존에 보였던 웹툰 데이터가 또 나타나게 되는 것이다.

그래서 데이터 전체를 조회 후 그 데이터를 셔플하고, 그 데이터를 캐싱해놓는다. 그리고 캐싱한 데이터를 페이징 처리하는 것이다.

캐싱을 이용한 시나리오는 아래와 같다.

  • 평가한 웹툰을 제외한 웹툰 리스트를 조회할 때 조회한 데이터를 캐싱한다.
  • 평가한 웹툰을 제외한 평균 약 500개의 웹툰이 캐싱된다.
    • 캐싱되지 않았다면 조회를 다시 하게되는 것이고, 캐싱된 데이터라면 캐싱된 데이터를 사용하게 된다.
  • 매 요청 시 500개의 웹툰 데이터를 매 페이지 요청 시 shuffle 하는 것이 아닌, 이미 한번 shuffle된 데이터를 캐싱한다.
  • shuffle, 캐싱된 데이터를 페이징 처리 하면 된다.

구현

우선 기존에 작성했떤 Querydsl을 사용한 평가 웹툰 필터링 메서드에서 셔플과 페이징 처리를 하지 않고, List를 반환하도록 수정한다. (꼭 Querydsl일 필요 없다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration  
@EnableCaching  
public class CacheConfig {  
  
  @Bean  
  public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {  
       RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()  
		    .entryTtl(Duration.ofMinutes(10))  
		    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));  
  
       return RedisCacheManager.builder(connectionFactory)  
		    .cacheDefaults(config)  
		    .build();  
    }  
}

우선 Redis 캐싱을 사용하기 위한 Config 클래스를 만들어 주었다.

  • entryTtl(…)
    • 캐싱 만료 시간은 10분이다.
  • serializeValuesWith(…)
    • 직렬화를 위한 설정을 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Cacheable(value = "notEvaluatedWebtoons", key = "#loginUsername", unless = "#result == null", cacheManager = "cacheManager")  
public List<RatingWebtoonDto> getCachedNotEvaluatedWebtoon(String loginUsername) {  
    Account loginUser = accountRepository.findByUsername(loginUsername)  
		    .orElseThrow(() -> new NotFoundException("계정 정보가 존재하지 않습니다."));  
  
    List<RatingWebtoonDto> contents = webtoonRepository.findWebtoonsNotEvaluated(loginUser.getId());  
    Collections.shuffle(contents);  
  
    return contents;  
}

public Page<RatingWebtoonDto> getNoEvaluateCards(String loginUsername, int page,  
                                                 int size) {  
    List<RatingWebtoonDto> allNotEvaluatedWebtoons = webtoonCacheService.getCachedNotEvaluatedWebtoon(loginUsername);  
  
    int fromIndex = page * size;  
    int toIndex = Math.min((page + 1) * size, allNotEvaluatedWebtoons.size());  
    List<RatingWebtoonDto> subList = allNotEvaluatedWebtoons.subList(fromIndex, toIndex);  
  
    return new PageImpl<>(subList, PageRequest.of(page, size), allNotEvaluatedWebtoons.size());  
}
  • 셔플한 데이터를 캐싱한다.
  • 캐싱된 (셔플된) 데이터를 가지고 페이징 처리를 한다.

주의사항

처음에는 저 두 메서드를 같은 WebtoonService 클래스에서 정의해놓고 사용했다.

1
@Cacheable self-invocation (in effect, a method within the target object calling another method of the target object). The cache annotation will be ignored at runtime

캐싱을 적용한 getCachedNotEvaluatedWebtoon()에 위와 같은 문구가 나타났다.

실행은 가능했지만, 캐싱이 되지 않았다.

  • 이는 Spring에서 캐시 애노테이션을 사용할 때 발생하는 self-invocation 문제이다.
  • 캐시나 트랜잭션과 같은 애노테이션이 붙은 메서드를 동일한 클래스 내에서 직접 호출할 때 발생하는 문제이다.
  • 동일 클래스 내 직접 호출시 Spring AOP 프록시를 거치지 않아 애노테이션 기반의 처리가 무시되는 현상이다.
    • AOP 프록시는 외부 호출을 가로채 애노테이션을 처리하지만, 클래스 내부에서의 메서드 호출은 프록시를 거치지 않기 때문.

이 문제를 해결하기 위해서는 자기 자신을 Autowiring 하는 방법도 있지만,

나는 WebtoonCacheService 클래스를 따로 두어 관리하기로 했다.

결과

결과적으로 이제 중복되는 웹툰이 발견되는 현상은 보이지 않았다.

덤으로 성능 개선의 효과까지 얻었다.

  • 기존 요청

  • 캐싱 구현 후 요청

17ms -> 7ms로 줄어든 것을 볼 수 있다.

캐시를 적용함으로써 약 59%의 성능 개선이 된 것이다.

그러나..

여전히 남은 문제가 존재한다.

사용자가 만약 어떤 웹툰에 대해 평가를 한 후 페이지를 빠져나온 후 다시 접근하게 되면

이전의 캐시를 이용하게 되어, 평가한 웹툰도 다시 보여지게 된다.

물론 해당 웹툰을 다시 평가한다고 해서 평가 데이터를 수집하는데 문제가 있지는 않다.

하지만 사용자 경험에 좋지 않기 때문에 @CacheEvict, @CachePut 등을 이용해 수정하려 했다.

하지만 만약 평가 시 @CacheEvict를 사용해 캐시를 무효화한다면 0페이지에서 무효화하고 3페이지를 호출 했을 때 또 중복된 웹툰 요소가 보여질 수 있다는 문제가 있다.

@CachePut을 이용하면 전체 데이터를 로드하고 평가된 웹툰을 제외한 후 다시 저장하는 과정을 거치기 때문에 성능상의 문제가 있다.

이 문제는 아직 해결하지 못했다. 어떻게 해결해야 될 지 고민을 많이 해야될 것 같다.

This post is licensed under CC BY 4.0 by the author.