강의를 듣던 도중 Redis를 접하게 되었고 캐시에 대한 내용과 Redis를 사용하는 방법에 대해서 정리해본다. 총 2번의 리뷰를 통해 진행한다. 이번 리뷰에서는 Cache의 기본 개념 및 SpringBoot를 통한 예제를 살펴보자
캐시란(Cache)?
캐시는 자주 사용하는 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 동일한 요청이 들어오면 작업을 수행해서 결과를 만드는 대신 이미 보관된 결과를 돌려주는 방식을 말한다.
캐시는 저장공간이 작고 비용이 비싸지만 빠른 성능을 제공한다. 하지만 저장공간이 작고 비용이 비싼만큼 모든 상황에서 쓸 수 있는 것은 아니다. 아래와 같은 경우에 사용을 고려하면 좋다.
- 반복적으로 동일한 결과를 돌려주는 작업(이미지, 썸네일, 실시간 검색어 등...)
- 잘 바뀌지 않는 정보를 외부에서 반복적으로 읽어올때 (DB 데이터 호출 및 api 호출)
캐시는 Memory에 데이터를 저장하였다가 불러 사용한다. Enterprise급 Application에서 DBMS의 부하를 줄이고, 시간이 오래 걸리는 작업들에 대해 성능을 높일 수 있다. 캐시의 데이터가 변하는 시점(노출될 광고가 변하는 시점, 사람의 나이가 변하는 시점 등...)에 맞춰 전략적으로 캐시를 사용하면 된다.
Spring Cache Abstraction(캐시 추상화)
예제를 보기전에 먼저 Spring Cache Abstraction(캐시 추상화)을 먼저 간단하게 알아보고 넘어가자. 캐시 추상화는 애플리케이션에 "투명하게" 캐시를 넣어주는 기능이다. 캐시가 시스템, 애플리케이션에 투명하게 자리잡는다는 말은 무엇일까?
- 데이터가 통신하는 시스템 쌍방이 캐시의 존재를 모른다는 의미
- 캐시가 있건 없건, 시스템의 기대 동작은 동일해야 한다.
- 캐시의 목표는 오로지 성능
캐시는 오로지 성능을 위해서 자리를 잡고있는 것이기 때문에 다른 것에 영향을 미쳐서는 안 된다. 다른 데이터가 바뀌어 버린다면 또 다른 비즈니스 로직이 되어버린다. 오로지 성능만을 위하기 때문에 다른 것에 영향을 미치지 않는다는 의미로 "투명하다"라는 말을 쓴다고 한다.
예제
예제 환경
- SpringBoot 2.7.2
- Gradle
- Java11
- lombok
파일 구성
- Controller
- Service
- Entity
- Repository
- RepositoryImpl
실제 DB와 연동은 하지않으며 Dto는 구현하지 않았습니다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.7.2'
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
캐시관련 설정을 편리하게 지원해주는 패키지 spring-boot-cache는 spring-boot-starter에 기본 내장되어있다고 한다. spring-boot-start를 사용한다면 spring-boot-cache는 내장되어있기에 패키지를 주입하지 않아도 된다. redis를 사용하기 전에 기본 내장되어있는 캐시를 사용해보자.
User.java
@AllArgsConstructor(staticName = "of")
public class User {
private Long id;
private String name;
private String email;
private Integer age;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
staticName이라는 옵션을 사용하여 static factory 메소드를 사용한 것 외에는 별 기능이 없는 Domain 클래스이다.
UserRepository.java
public interface UserRepository {
User findByIdCache(Long id);
User findByIdNoCache(Long id);
void refresh(Long id);
}
UserRepositoryImpl.java
@Slf4j
@RequiredArgsConstructor
@Repository
public class UserRepositoryImpl implements UserRepository {
// DB를 연결하지 않았기에 예제의 DB 역할을 해줄 Map
private final Map<Long, User> storage;
@Override
@Cacheable("User")
public User findByIdCache(Long id) {
slowQuery(3000);
return storage.get(id);
}
@Override
public User findByIdNoCache(Long id) {
slowQuery(3000);
return storage.get(id);
}
@Override
@CacheEvict(value = "User", key = "#id")
public void refresh(Long id){
log.info("Cache Clear: " + id);
}
// storage 에 유저정보를 넣어준다.
public User enroll(Long id, String name, String email, Integer age){
return storage.put(id, User.of(id, name, email, age));
}
// 빅 쿼리를 돌려 시간이 걸린다는 가정
private void slowQuery(long seconds) {
try{
Thread.sleep(seconds);
}catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
이번 예제의 핵심 코드이다. UserRepositoryImpl.java 를 살펴보자.
@Cacheable 은 캐시를 저장/조회하기 위한 어노테이션이다. 캐시를 적용할 메서드에 @Cacheable 어노테이션을 붙여주면 캐시에 데이터가 없을 경우에는 기존 로직을 실행하여 데이터를 캐시에 데이터를 넣어주고, 캐시에 데이터가 있을 시에는 캐시의 데이터를 반환해준다. 캐시는 기본적으로 이름 하위에 key-value 형태로 데이터가 저장되는데, @Cacheable("User") 의 "User" 이 캐시의 이름이다. User(id=0, name=jack, email=jack@naver.com, age=30)
캐시에 데이터가 없을 경우 왜 기존로직을 실행하여 데이터를 캐시에 넣어줄까?
간단한 이유이다. 캐시는 데이터를 잠깐 저장하는 곳이다. 그렇기에 조회할 데이터가 없다면 DB에서 데이터를 가져와 캐시에 데이터를 넣어 사용하는 것이다.
메서드 refresh는 저장된 캐시를 비워주기 위한 메서드이다. 이때에 @CacheEvict 어노테이션을 사용한다. @CacheEvict는 저장된 캐시를 비워주기 위한 어노테이션이다. 캐시에 저장된 정보가 업데이트되는 주기에 맞춰 캐시를 비우고 새로 데이터를 넣어줘야 한다. value로는 캐시의 이름을 전달하고 key로는 캐시에 저장되는 값의 key를 전달하여준다. 위의 예제 같은 경우에는 id 값을 전달하였다.
메서드 slowQuery는 아주 많은 양의 데이터가 존재하여 한번 조회할 때에 3초라는 시간이 걸린다 가정하기 위해 만들었다. 캐시를 사용했을 때와 하지 않았을 때의 속도 차이를 비교하기 위한 메서드이다.
메서드 enroll은 현재 예제에서는 데이터베이스와 연결하여 사용하고 있지 않기 때문에, 예제를 위해 애플리케이션 실행 시에 Map타입의 storege에 데이터를 넣어 데이터베이스의 역할을 만들어주기 위함이다.
User.controller.java
@Slf4j
@EnableCaching // 어노테이션을 이용하여 해당 클래스에서 캐시기능을 사용하겠다는 선언
@RequiredArgsConstructor
@RestController
public class UserController {
private final UserService userService;
@GetMapping("/user/nocache/{id}")
public User getNoCacheUser(
@PathVariable Long id
){
long start = System.currentTimeMillis(); // 시간측정
User user = userService.printUserNoCache(id); // 데이터베이스 조회
long end = System.currentTimeMillis();
log.info(user.getName() + "NoCache 수행시간 : " + (end-start));
return user;
}
@GetMapping("/user/cache/{id}")
public User getCacheUser(
@PathVariable Long id
){
long start = System.currentTimeMillis(); // 시간측정
User user = userService.printUser(id); // 데이터베이스 조회
long end = System.currentTimeMillis();
log.info(user.getName() + " Cache 수행시간 : " + (end-start));
return user;
}
// key 값을 이용하여 캐시삭제
@GetMapping("/user/refresh/{id}")
public void refreshCache(
@PathVariable Long id
){
userService.refresh(id);
}
}
Contoller는 만들어둔 로직을 url을 통해 호출할 수 있게 해 두었다. 캐시가 있을 때와 없을 때의 수행 시간을 측정해보며 비교해보자.
Nocache
http://localhost:8080/user/nocache/0
http://localhost:8080/user/nocache/1
http://localhost:8080/user/nocache/2
먼저 캐시를 적용하지 않은 url을 호출해서 시간이 얼마나 걸리는지 확인해보자. 각 url을 2번씩 호출하였다.
IDE의 콘솔이다. 캐시를 적용하지 않을 시에는 각 url을 호출할 때마다 3초의 시간이 걸리는 걸 볼 수 있다. 그렇다면 캐시를 적용했을 때에는 어떻게 되는지 살펴보자.
Cache
http://localhost:8080/user/cache/0
http://localhost:8080/user/cache/1
http://localhost:8080/user/cache/2
캐시를 적용한 url을 각 3번씩 호출하였다.
첫 호출 시에는 캐시에 데이터가 없기 때문에 3초라는 시간이 걸리지만 그 이후부터는 캐시에 저장된 데이터를 사용하여 0초라는 시간이 걸리는 걸 확인할 수 있다.
만약 이때에 데이터베이스에 저장된 유저의 정보가 변경된다면 어떻게 해야 할까? 기존 캐시에 저장되어있던 변경된 유저의 데이터를 지우고 변경된 데이터를 다시 캐시 데이터에 넣어주면 된다.
http://localhost:8080/user/cache/0
http://localhost:8080/user/refresh/0
http://localhost:8080/user/cache/0
id 값이 0번인 유저를 4번 호출했다. 유저의 정보가 변경됐다고 가정하고 id값이 0인 유저의 캐시를 refresh 한 후에 다시 호출했을 때의 모습이다. 캐시를 refresh 한 후에는 다시 3초라는 시간이 걸리고 그 뒤에는 캐시 데이터를 가져와 0초가 적용된 모습을 볼 수 있다.
마무리
강의를 보던 도중 캐시에 대한 내용이 나와 공부하게 되었다. 처음에는 springframework에 내장되어있는 캐시를 사용하는 예제를 통하여 공부하였고, Redis 까지 적용시키는 공부를 하게되었다. 캐시에 대해 깊게 공부를 한 것은 아니지만 대략적으로 캐시란 무엇인지 알게 되었다. 다음 리뷰에는 현재 글의 예제에 Redis를 적용시키는 리뷰를 작성하고 Docker을 이용하여 테스트 컨테이너를 띄우는 방법에 대해 리뷰할 예정이다.
Reference
FastCampus 한 번에 끝내는 Spring 완전판 초격차 패키지
관련 리뷰
'Experience' 카테고리의 다른 글
Instagram Api Refresh Token batch 리펙토링 (0) | 2023.06.24 |
---|---|
JRebel이 안될때 확인해 볼 사항들 (0) | 2023.06.21 |
특수 사례 패턴 (외부 API를 대하는 방법) (0) | 2022.12.19 |
MCMS 웹 프로젝트 후기 (2) | 2022.12.04 |
Required request body is missing, Argument(s) are different! Wanted (0) | 2022.06.11 |