Introduction
최근 데이터 게시판 조회수 에 관련하여 최적화 방법을 찾아보던 중 Spring 에도 내장된 캐싱 방법이 있다는 것을 발견하고 알아보기로 하였다.
해당 문서에서는 spring--boot-starter-cache 라이브러리에 대한 간단한 설명 및 실습을 진행한다.
Dependencies
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-cache'
Conception
Spring Cache 는 메서드 단위로 적용이 가능하며, 캐싱이 적용된 특정 메서드에 어떤 값이 인수로 전달되었을 때, 해당 인수와 리턴 값을 key-value 형태로 저장해 놓았다가 후에 동일한 key 값이 인수로 전달되면 메서드를 실행하지 않고 캐싱된 데이터를 바로 리턴해 준다.
예를들어 이러한 메서드가 존재한다고 해 보자.
public int plus(int n) {
return n + 10;
}
해당 메서드에 캐싱을 적용시키면, 처음 실행 시 인수와 리턴 값을 캐싱한다.
plus(10);
// 임의의 캐시 저장공간 Map (10:20)
후에 plus(10)
이 다시 호출된다면 Map 에 10이 존재하므로 plus 메서드를 실행하지 않고 Map 에서 value를 꺼내 바로 리턴한다.
해당 로직은 메서드 전후로 실행되므로 AOP가 적용된 기술임을 예상할 수 있다.
위의 결과로 알 수 있듯이 캐싱이 적용될 메서드는 특정 인수를 전달하면 항상 동일한 리턴 값을 전달하는 메서드에만 적용이 가능하며 임의의 값을 리턴하는 메서드에는 적용이 불가능하다. (정확히는 해서는 안 된다.)
CacheManager
캐시 저장공간은 Spring 에서 CacheManager 를 Bean 으로 등록함으로서 구체화 시킬 수 있다. 만약 Spring Application 실행 시, CacheManager Bean 이 등록되어있지 않다면, 해당 순서대로 저장 공간이 구현되어 있는지 탐색을 시도한다.
Generic
JCache
EnCache 2.x
Hazelcast
Infinispan
Couchbase
Redis
Caffeine
Simple (In-memory)
만약 8번까지 탐색에 실패하면 9번을 구현하는데 In-memory 에 ConcurrentHashMap 을 구현하는 방식으로 캐싱 공간을 마련하게 된다.
Implementaion
@EnableCaching
Spring Boot 에서는 Caching 적용에 어노테이션 방법을 사용하므로 @Configuration 할 코드에 @EnableCaching 어노테이션을 추가함으로써 캐싱 사용이 준비가 완료된다.
나는 In-memory 캐싱을 사용하기 때문에 별도의 CacheManager 파일을 만들어 주지 않았다. 따라서 @SpringBootApplication 에 추가해 주었다. (해당 어노테이션 또한 @Conriguration 이 포함된다.)
@SpringBootApplication
@EnableCaching
public class PostRedisViewsApplication {
public static void main(String[] args) {
SpringApplication.run(PostRedisViewsApplication.class, args);
}
}
}
@Cacheable
캐싱을 적용할 메서드를 지정한다.
Spring Boot 같은 WebApp 에서는 보통 DB와 커넥션 횟수를 줄이기 위해 캐싱을 사용한다. 따라서 Service 단에서 DB 에 Select 하는 메서드에 캐싱을 적용한다.
// PostService
@Cacheable(value = "post", key = "#postId")
public PostDto getPost(long postId) {
log.info("Get id: {}", postId);
Post post = postRepository.findById(postId)
.orElseThrow(EntityNotFoundException::new);
return modelMapper.map(post, PostDto.class);
}
@Cacheable 어노테이션을 통해 해당 메서드를 캐싱하겠다고 선언하였다.
value 는 캐싱공간의 이름으로, String 을 통해 참조할 캐싱 공간을 구분하게 된다. 다른 메서드에서 동일하게 @Cacheable("post")
어노테이션을 사용한다면 위의 getPost()
메서드와 동일한 캐싱 공간을 사용하게 된다.
key 는 캐싱 데이터를 구분할 key 를 의미한다. 위의 코드에서는 postId를 가지고 캐싱 데이터를 구분한다.
만약 현재 캐싱 데이터가 (1:10) 이라고 가정하면 후에 postId 가 1로 다시 호출되면 캐싱된 데이터를 꺼내 반환한다.
만약 객체를 키로 사용할 경우 hashcode 가 제대로 정의되어 있어야 한다. HashMap은 hashcode로 키를 구분하기 때문이다.
key 는 SpEL 로 작성되므로 특정 객체의 필드 하나를 키로 지정할 수 있다. 즉,
@Cacheable(value = "person", key = "#p.id") //Person 의 id 필드를 키로 사용
public void getPerson(Person p) {
...
}
public class Person {
int id;
int age;
String name;
}
위와 같이 작성할 수 있다.
@CachePut
캐싱된 데이터를 수정할 때 사용된다.
만약, DB 내의 데이터가 업데이트 되었으나 캐싱된 데이터를 업데이트 하지 않을 경우, 업데이트 되지 않은 캐싱 데이터를 리턴함으로써 데이터의 무결성이 깨질 수 있다. 따라서 DB 업데이트를 하는 Service 메서드에 적용함으로써 캐싱공간과 DB의 데이터를 일관되게 관리할 수 있다.
// PostService
public PostDto updatePost(UpdatePost updatePost) {
Post post = postRepository.findById(updatePost.getId())
.orElseThrow(EntityNotFoundException::new);
post.setTitle(updatePost.getTitle());
Post resultPost = postRepository.save(post);
return modelMapper.map(resultPost, PostDto.class);
}
@CacheEvict
캐싱된 데이터를 삭제할 때 사용된다.
만약 DB 내의 데이터를 삭제하였으나 캐싱된 데이터를 삭제하지 않을 경우 실제로는 존재하지 않는 캐싱된 데이터를 리턴하는 상황이 벌어질 수 있다. 따라서 DB 데이터를 삭제하는 Service 메서드에 적용한다.
// PostService
@CacheEvict(value = "post", key = "#postId")
public void deletePost(long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(EntityNotFoundException::new);
postRepository.delete(post);
}
References
'개발 > Spring Boot' 카테고리의 다른 글
[Spring Boot] Spring Boot Application 을 재시작해도 Spring cache가 초기화 되지 않는 문제 해결 (0) | 2024.02.07 |
---|---|
[ModelMapper] 필드 타입이 다른 두 객체간 매핑 커스텀하기 (0) | 2023.12.02 |
[SpringBoot] 발급받은 타행 SSL인증서를 SpringBoot에 적용시키기 (0) | 2022.12.10 |