캐시란?
- 데이터나 값을 미리 복사해놓는 임시 저장소
- 시스템 성능을 향상시키기 위한 메커니즘
- 캐시에 데이터를 저장하고 엑세스하는 프로세스이다
캐시를 사용해야 하는 이유
① 데이터 접근이 빠르고 비용이 저렴
② 애플리케이션 성능이 향상됨
③ 응답이 빠름
④ 메모리에 데이터 접근하는게 DB에서 가져오는 것보다 항상 빠름
⑤ 비용이 많은 백엔드 요청이 줄어듦
캐시에 데이터를 미리 복사해 놓음으로써 처리/접근 시간(비용) 없이 빠른 속도로 데이터 접근할 수 있다
언제 사용
- 자주 변경되지 않는 데이터
- 원본 데이터에 접근/처리 시간이 오래 걸리는 경우
캐싱 종류
① 인메모리 캐싱 (ex. Redis)
② 데이터베이스 캐싱 (ex. hibernate 1차 캐시)
③ 웹 서버 캐싱
- HTTP Cache : 브라우저/프록시/웹 서버 캐시
- Application Cache : 인 메모리/글로벌 캐시
④ CDN 캐싱
Spring Caching
스프링에서 사용가능한 캐시는 아래와 같다
- JCache (JSR-107) : standard caching API for Java
- EhCache : 오픈 소스 Java 기반 캐시, EhCache3 부터 JCache 표준 정식 지원
- Hazelcast
- Infinispan
- Couchbase
- Redis : JCache 표준 지원x, Spring Data Redis 통해 쉽게 통합 가능
- Caffeine : 고성능 Java 기반 캐시, JCache 표준 지원
- Simple
- Simple 캐시의 경우 SimpleCacheManager 뜻함, Spring에서 기본 제공하고, 특별한 캐시 구현체 필요 없이 간단한 애플리케이션이나 테스트 용도로 사용됨
- Spring Boot에서는 특별한 캐시 구현체 없는 경우 ConcurrentMapCacheManager 빈을 기본 생성
- EhCache 추가할 경우 JCacheCacheManager 빈을 기본 생성
참고. 슈퍼마켓에서 우유를 사면서 캐싱을 알아보자
https://rinae.dev/posts/web-caching-explained-by-buying-milk-kr/
프로젝트 버전
- spring boot 3.2.6
- jdk 17
- EhCache 3.10.8 (2024.07.22 기준 최신)
Simple Cache
특별한 캐시 구현체를 사용하지 않을 경우 ConcurrentMapCacheManager 자동 생성하여 사용할 수 있다
참고. 전체 코드
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.6'
id 'io.spring.dependency-management' version '1.1.5'
}
group = 'caching'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-cache'
}
tasks.named('test') {
useJUnitPlatform()
}
@EnableCaching 애노테이션 추가시 기본적으로 설정된다
@EnableCaching
@SpringBootApplication
public class HelloCachingApplication {
public static void main(String[] args) {
SpringApplication.run(HelloCachingApplication.class, args);
}
}
애플리케이션 실행시 SimpleCacheConfiguration이 동작함을 확인할 수 있었다
해당 Bean이 주입되는지 확인해본다
@Slf4j
@Component
public class CacheManagerCheck implements CommandLineRunner {
private final CacheManager cacheManager;
public CacheManagerCheck(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void run(String... args) throws Exception {
log.info("================================\n Using cache manager :" + this.cacheManager.getClass().getName());
}
}
콘솔 출력
==================================
Using cache manager :org.springframework.cache.concurrent.ConcurrentMapCacheManager
SpringBoot + Ehcache 기본 예제 및 소개 - 기억보단 기록을 기술 블로그에 있는 동일한 예제 코드를 활용해서 캐시 테스트를 진행해본다 (전체 코드는 깃 허브 참고)
간단하게 RestController를 생성하고 MemberRepository를 호출하도록 기능 구현
① 캐시를 사용하지 않는 경우 : getNoCacheMember(..)
② 캐시를 사용하는 경우 : getCacheMember(..)
③ 캐시 만료 요청 : refrech(..)
@Slf4j
@RestController
public class MemberController {
private final MemberRepository memberRepository;
public MemberController(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@GetMapping("/member/nocache/{name}")
public ResponseEntity<Member> getNoCacheMember(@PathVariable("name") String name) {
long start = System.currentTimeMillis();
Member member = memberRepository.findByNameNoCache(name);
long end = System.currentTimeMillis();
log.info("{}의 NoCache 수행시간 : {} ", name, (end - start));
return ResponseEntity.ok(member);
}
@GetMapping("/member/cache/{name}")
public ResponseEntity<Member> getCacheMember(@PathVariable("name") String name) {
long start = System.currentTimeMillis();
Member member = memberRepository.findByNameCache(name);
long end = System.currentTimeMillis();
log.info("{}의 Cache 수행시간 : {} ", name, (end - start));
return ResponseEntity.ok(member);
}
@GetMapping("/member/refresh/{name}")
public ResponseEntity<String> refresh(@PathVariable("name") String name) {
memberRepository.refresh(name);
return ResponseEntity.ok("Cache Clear");
}
}
public interface MemberRepository {
Member findByNameNoCache(String name);
Member findByNameCache(String name);
void refresh(String name);
}
Thread.sleep(..)을 사용하여 2초 지연 시간을 부여한다
@Slf4j
@Repository
public class MemberRepositoryImpl implements MemberRepository {
@Override
public Member findByNameNoCache(String name) {
slowQuery(2000);
return new Member(1L, name + "@gmail.com", name);
}
@Override
@Cacheable(value = "findMemberCache", key = "#name", unless = "#result == null")
public Member findByNameCache(String name) {
slowQuery(2000);
return new Member(1L, name + "@gmail.com", name);
}
@Override
@CacheEvict(value = "findMemberCache", key = "#name")
public void refresh(String name) {
log.info("{}의 Cache Clear!", name);
}
private void slowQuery(long seconds) {
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
throw new IllegalArgumentException(e);
}
}
}
캐시를 사용하지 않는 API의 경우 매 호출시 2초가 걸린다
캐시를 사용하는 API의 경우 첫 호출시 2초 시간이 걸리고 이후에는 캐싱 데이터 반환하여 빠르게 응답함을 확인할 수 있었다
EhCache
- Java 기반 오픈 소스
- EhCache 3버전 부터 JSR-107 자바 표준 인터페이스를 지원한다
- 캐시에 대한 자바 표준 인터페이스를 JCache 라고 한다
- EhCache 설정은 Programmatic 방식과 xml 두 가지 방식으로 설정할 수 있다 (공식문서 참고)
참고. 전체 코드
1. 의존성 문제 해결
처음 EhCache v3.8.1 로 했는데 아래 의존성 문제가 발생했다
- java.lang.NoClassDefFoundError: javax/xml/bind/JAXBElement
- Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory
- Caused by: java.lang.ClassNotFoundException: javax.xml.bind.ValidationEventHandler
Mkyong.com 에서 com.sun.xml.internal.bind.v2.ContextFactory가 Java 9에서 Deprecated 되었고,
Java11부터 완전히 삭제되었다고 jakarata.* dependencies 수정할 것을 가이드하였지만,
패키지가 맞지 않아 원하는 해답이 되진 않았다
깃허브 이슈를 찾아보니 같은 이슈를 발견 (해결방안은 없다)
https://github.com/ehcache/ehcache3/issues/2797
해결
- JAXB 관련 의존성을 2.x버전으로 맞추니 정상 동작한다
참고. https://groups.google.com/g/ehcache-users/c/U1bO6QArswQ
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'javax.cache:cache-api:1.1.1'
implementation 'org.ehcache:ehcache:3.10.8'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.3'
애플리케이션 구동시 기본 CacheManager 빈으로 JCacheManager 주입 확인
Using cache manager :org.springframework.cache.jcache.JCacheCacheManager
2. 추가 설정 (xml, properties, ..)
/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">
<cache alias="findMemberCache">
<key-type>java.lang.String</key-type>
<value-type>hello.caching.domain.Member</value-type>
<expiry>
<!--class(직접 정책 구현), none, ttl(timeToLive), tti(timeToIdle) 정책 지정-->
<ttl unit="seconds">20</ttl>
</expiry>
<listeners>
<listener>
<class>hello.caching.CacheLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap unit="entries">2</heap> <!-- v3.10.8 deprecated -->
<offheap unit="MB">10</offheap>
</resources>
</cache>
</config>
참고. https://www.ehcache.org/documentation/2.8/configuration/data-life.html
이벤트 발생(CREATED, EXPIRED, ..)시 로그로 정보를 간단히 출력한다
@Slf4j
public class CacheLogger implements CacheEventListener<Object, Object> {
@Override
public void onEvent(CacheEvent<?, ?> event) {
log.info("key : {}, eventType : {}, old value : {}, new value : {}",
event.getKey(),
event.getType(),
event.getOldValue(),
event.getNewValue());
}
}
- CREATED의 경우 신규 생성시 CacheEventListener 출력 확인
- EXPIREd의 경우 20초 후 만료 된 후 캐시 API 호출시 EXPIRED와 CREATED 출력됨을 확인
application.properties 설정 추가
spring.cache.jcache.config=classpath:ehcache.xml
에러
Caused by: org.ehcache.spi.serialization.SerializerException: java.io.NotSerializableException: hello.caching.domain.Member
- EhCache에 사용되는 객체는 Serializable 구현해야 한다
Member 도메인 수정
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
//..
}
3. 호출 테스트
- 20초 이상 지났을 때 캐시가 만료되는지 확인
- httpie 사용하여 호출
http -v ":8080/member/cache/tester"
38 분 27초에 최초 호출 후 39분 00초에 호출시 캐시가 만료된 것을 확인가능했다
참고.
SpringBoot + Ehcache 기본 예제 및 소개 - 기억보단 기록을
니들이 Caffeine 맛을 알아! - 네이버 결제 백엔드 개발자
'공부 > Spring' 카테고리의 다른 글
Spring boot 3.x + Security 6.x + @WebMvcTest 회고 (0) | 2024.06.26 |
---|---|
[Spring] AOP 용어 정리 (0) | 2023.08.22 |
[JPA] Date 타입 포맷 맞춰주는 @Temporal (0) | 2022.06.20 |
[JUnit] org.junit.runners.model.InvalidTestClassError: Invalid test class (0) | 2022.06.19 |
[ERROR] org.springframework.oxm.UncategorizedMappingException: Unknown JAXB exception (0) | 2022.06.12 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!