Search

Spring Boot에 캐싱 적용해보기

Status
UPLOADING
Date
2024/06/28
Tags
Cache

1. 개요

오늘은 성능 개선 방법 중 하나인 Caching을 적용하는 과정에 대해 알아보도록 하겠습니다.

2. 캐싱이란

캐시는 쉽게 말해 데이터베이스 까지 쿼리를 날리지 않도록 하는 것입니다.
현재 서비스 중인 EAT-SSU를 예로 들어보겠습니다. EAT-SSU는 숭실대학교 구내 식당 정보를 제공하는 앱 어플리케이션 서비스로 사용자들은 식단과 관련해서 거의 동일한 정보를 제공 받습니다.
동일한 정보를 제공한다고 가정했을 때, 사용자가 식단을 조회할 때 마다 매번 쿼리를 날리는 것이 비효율적입니다.
해당 데이터를 캐시하고 사용자가 조회할 때 마다 캐시된 데이터를 넘겨주면 서버는 성능을 큰 폭으로 향상 시킬 수 있습니다.

3. Ehcache 알아보기

Ehcache는 Spring에서 주로 사용되는 로컬 캐시로서 Java 기반의 오픈 소스 캐시 라이브러리
별도의 데몬을 가지지 않고 Spring 내부적으로 동작합니다. 이는 애플리케이션과 라이프 사이클을 같이 가져가며 애플리케이션이 죽으면 Ehcache도 죽습니다.

3-1. Ehcache의 구조

Ehcache는 세 가지 공간에 데이터를 저장합니다.
Memory : 가장 빠르게 데이터를 접근할 수 있는 공간으로, 자바 애플리케이션의 메모리(Heap)에 데이터를 저장합니다. 단, GC에 의해 관리됩니다.
Offheap : 자바 힙 메모리 외부에 데이터를 저장합니다. 자바 힙 메모리가 아닌 시스템 메모리를 사용하며, GC의 영향을 받지 않습니다.
Disk : 데이터를 파일 형태로 디스크에 저장합니다. 디스크 저장소를 구성하여 많은 데이터를 저장할 수 있습니다.

3-2. 다른 Cache Engine과의 비교

Redis
별도의 서버가 필요하기 때문에 네트워크 지연 혹은 단절 등의 이슈로 부터 자유롭지 않습니다.
풍부한 자료구조를 가지고 있습니다
PUB/SUB
싱글 쓰레드를 통한 데이터 일관성을 유지합니다.
Memcached
같은 로컬 캐시입니다.
Spring 어플리케이션과 별도로 실행 해야 합니다. 즉, 라이프 사이클이 다릅니다.
어떤 캐시를 사용할지는 여러 환경에 따라 달라집니다.
한 대의 서버에서 데이터에 접근하거나 여러 대의 서버에서 접근하더라도 데이터의 변동성이 적다면 로컬 캐시를 이용하는 것이 좋습니다.
Ehcache를 사용하는 이유는 다음과 같습니다.
메소드에 캐싱을 적용함으로써 캐시에 보관된 정보로 메소드의 실행 횟수를 줄일 수 있습니다.
대상 메소드가 실행될 때 마다 해당 메소드가 같은 인자로 이미 실행되었는지 확인하는 캐싱 동작을 적용합니다.
해당 데이터가 존재한다면 실제 메소드를 실행하지 않고 결과를 반환하고 존재한다면 메소드를 실행하고 그 결과를 캐싱한 후, 사용자에게 반환해서 다음 번 호출 시에 사용할 수 있게 합니다.
대표적인 어노테이션으로는 다음이 있습니다.
@Cacheable : 캐시할 수 있는 메소드를 지정합니다
@CachePut : 메소드 실행에 영향을 주지 않고 캐시를 갱신합니다
@CacheEvict : 캐시에서 오래되거나 사용하지 않는 데이터를 제거하는 메소드를 지정, void 반환형에서만 사용 가능합니다. @Caching 어노테이션을 여러 개 사용할 때 사용합니다

4. Spring Boot에 적용하기

build.gradle
// Cache implementation "org.springframework.boot:spring-boot-starter-cache" implementation "org.ehcache:ehcache" implementation "javax.cache:cache-api" // JAXB implementation 'com.sun.xml.bind:jaxb-core:2.3.0.1' implementation 'com.sun.xml.bind:jaxb-impl:2.3.1' implementation 'javax.xml.bind:jaxb-api:2.3.1'
Shell
복사
CacheConfig.kt
@Configuration @EnableCaching class CacheConfig { }
Kotlin
복사
resources/ehcache.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.ehcache.org/v3" xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd"> <!-- @Cacheable의 value 값으로 사용됨 --> <cache alias="menus"> <!-- 캐시에 사용하는 키의 타입 --> <key-type>java.lang.String</key-type> <!-- 캐시에 사용하는 값의 타입이며 역직렬화를 통해 해당 DTO로 변환 --> <value-type>com.ssu.eatssu.domain.menu.presentation.dto.MenuCategoryListResponse</value-type> <expiry> <!-- 캐시 만료 시간 --> <ttl unit="hours">24</ttl> </expiry> <resources> <!-- 캐시에 사용할 메모리와 디스크의 크기 --> <heap unit="entries">100</heap> </resources> </cache> </config>
Kotlin
복사
MenuQueryService.kt
@Cacheable(value = ["menus"], key = "#restaurant.name") fun getMenusGroupedByCategory(restaurant: Restaurant): MenuCategoryListResponse { return menuQuerydslRepository.findAllMenusGroupedByCategory(restaurant.name) }
Kotlin
복사

5. 비교하기

// when menuQueryService.getMenusGroupedByCategory(restaurant = Restaurant.FOOD_COURT) menuQueryService.getMenusGroupedByCategory(restaurant = Restaurant.FOOD_COURT) menuQueryService.getMenusGroupedByCategory(restaurant = Restaurant.FOOD_COURT)
Kotlin
복사
메뉴 리스트를 조회하는 메소드를 세 번 연속으로 호출해보았습니다.
캐싱을 적용하기 전에는 다음과 같은 쿼리가 날라갑니다.
Hibernate: select [생략...] from menu m1_0 where m1_0.restaurant=? Hibernate: select [생략...] from menu m1_0 where m1_0.restaurant=? Hibernate: select [생략...] from menu m1_0 where m1_0.restaurant=?
Kotlin
복사
적용 후 select 쿼리가 한 번만 발생한 모습