
땅땅땅 서비스의 유저테스트 피드백을 살펴본 결과,
메인 페이지에서 경매글 목록을 조회하는 로직이 가장 느리다는 의견이 존재했습니다.
저희팀은 EHCache와 RedisCache를 적용해보고 나온 결과와 여러가지 트레이드 오프에 대해 이번글에서 서술해보겠습니다.

public Slice<AuctionListResponseDto> getAuctions(
...
) {
User user = userService.findUserOrElseThrow(userId);
List<Long> townList = user.getTown().getNeighborIdList();
return auctionRepository
.findAllByFilters(townList, status, title, pageable)
.map(auction -> new AuctionListResponseDto(...));
}
// ehcache
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.ehcache:ehcache'
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<cache alias="trashcanList"> <!-- @Cacheable의 value 값으로 사용됨 -->
<!-- key 타입 지정 -->
<key-type>java.lang.String</key-type>
<!-- value 타입 지정 -->
<value-type>java.util.List</value-type>
<expiry>
<ttl unit="seconds">600</ttl> <!-- ttl 설정 -->
</expiry>
<listeners>
<listener>
<!-- 리스너 클래스 위치 -->
<class>com.twoez.zupzup.config.cache.CacheEventLogger</class>
<!-- 비동기 방식 사용, 캐시 동작을 블로킹하지 않고 이벤트를 처리, SYNCHRONOUS와 반대 -->
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<!-- 이벤트 처리 순서 설정 X, ORDERED와 반대 -->
<event-ordering-mode>UNORDERED</event-ordering-mode>
<!-- 리스너가 감지할 이벤트 설정(EVICTED, EXPIRED, REMOVED, CREATED, UPDATED) -->
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>UPDATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
</listener>
</listeners>
<resources>
<!-- JVM 힙 메모리에 캐시 저장 -->
<heap unit="entries">100</heap>
<!-- off-heap(외부 메모리)에 캐시 저장 -->
<offheap unit="MB">10</offheap>
</resources>
</cache>
<cache alias="googlePublicKeys"> <!-- @Cacheable의 value 값으로 사용됨 -->
<expiry>
<ttl unit="seconds">600</ttl> <!-- ttl 설정 -->
</expiry>
<listeners>
<listener>
<!-- 리스너 클래스 위치 -->
<class>com.twoez.zupzup.config.cache.CacheEventLogger</class>
<!-- 비동기 방식 사용, 캐시 동작을 블로킹하지 않고 이벤트를 처리, SYNCHRONOUS와 반대 -->
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<!-- 이벤트 처리 순서 설정 X, ORDERED와 반대 -->
<event-ordering-mode>UNORDERED</event-ordering-mode>
<!-- 리스너가 감지할 이벤트 설정(EVICTED, EXPIRED, REMOVED, CREATED, UPDATED) -->
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
</listener>
</listeners>
<resources>
<!-- JVM 힙 메모리에 캐시 저장 -->
<heap unit="entries">100</heap>
<!-- off-heap(외부 메모리)에 캐시 저장 -->
<offheap unit="MB">10</offheap>
</resources>
</cache>
<cache alias="kakaoPublicKeys"> <!-- @Cacheable의 value 값으로 사용됨 -->
<expiry>
<ttl unit="seconds">600</ttl> <!-- ttl 설정 -->
</expiry>
<listeners>
<listener>
<!-- 리스너 클래스 위치 -->
<class>com.twoez.zupzup.config.cache.CacheEventLogger</class>
<!-- 비동기 방식 사용, 캐시 동작을 블로킹하지 않고 이벤트를 처리, SYNCHRONOUS와 반대 -->
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<!-- 이벤트 처리 순서 설정 X, ORDERED와 반대 -->
<event-ordering-mode>UNORDERED</event-ordering-mode>
<!-- 리스너가 감지할 이벤트 설정(EVICTED, EXPIRED, REMOVED, CREATED, UPDATED) -->
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
</listener>
</listeners>
<resources>
<!-- JVM 힙 메모리에 캐시 저장 -->
<heap unit="entries">100</heap>
<!-- off-heap(외부 메모리)에 캐시 저장 -->
<offheap unit="MB">10</offheap>
</resources>
</cache>
</config>
@Configuration
@EnableCaching
public class CacheConfig {
// default로 사용되는 cachemanager
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(“…”);
}
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofMinutes(4L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)
.cacheDefaults(redisCacheConfiguration).build();
}
@Cacheable(value = "auctions", cacheManager = "cacheManager")
public List<AuctionListResponseDto> getAuctions(
Long userId,
StatusEnum status,
String title
) {
User user = userService.findUserById(userId);
List<Long> townList = user.getTown().getNeighborIdList();
return auctionRepository.findAllByFilters(townList, status, title);
}
이런한 과정을 통해 캐싱을 적용하였고, 처리량은 54.9TPS으로 기존 대비 약 4.3배 향상한 결과를 볼 수 있었습니다.

이런 결과를 얻을 수 있었고, 가장 빠른 것은 Ehcache였지만 데이터 부정합 문제를 해결하기 위해 Redis Cache를 적용하게 되었습니다.