1. 개요
프로젝트를 진행하며 티켓조회 성능 개선을 위해 캐싱 기능을 도입하기로 했습니다.
티켓 데이터는 일단 생성되면 잘 수정되지는 않지만 사용자 요청이 집중될 때 자주 조회되는 특성이 있습니다. 이러한 특성을 고려해 티켓(공연) 정보를 조회 성능을 높이기 위해 캐싱을 도입하기로 결정했습니다.
2. 캐싱이란?
캐싱은 데이터나 연산 결과를 미리 저장해두고 필요할 때 빠르게 접근할 수 있는 기술입니다.
일반적으로 반복적으로 조회되는 데이터나 연산속도가 느린 데이터를 저장하는 역할을 하며 캐시에 저장된 데이터는 원본 데이터에 비해 훨신 빠르게 접근할 수 있다는 장점이 있습니다. 이로 인해 시스템 응답속도를 개선하고, 서버의 부하를 줄일 수 있습니다.
- 응답 속도 개선 : 캐시는 메모리같은 저장소를 활용하기 때문에 빠르게 접근이 가능하다.
- 서버 부하 감소 : 반복적인 데이터베이스 쿼리나 외부 API 호출을 줄여 서버 부하를 완화할 수 있습니다.
- 사용자 경험 개선 : 빠른 응답속도로 사용자 경험 향상에 도움을 줄 수 있습니다.
해당 프로젝트에서 적용할 캐싱의 흐름은 아래와 같습니다.
3. Global 캐시 vs Local 캐시
캐시는 서버가 위치한 로컬에 저장하는 로컬 캐시과 Redis같은 외부 저장소를 활용한 글로벌 캐시로 분류할 수 있습니다.
Local Cache
- 애플리케이션 내부 메모리에 데이터를 저장하고 속도가 빠르다.
- 인스턴스별로 캐시 데이터를 관리하기 때문에 확장성이 낮고 서버가 여러 대일 경우 일관성을 유지하기 힘들다.
Global Cache
- 별도의 캐시 서버를 활용해 여러 서버에서 접근해 일관성을 유지할 수 있다.
- 캐시 서버와의 네트워크 비용이 존재하기 때문에 로컬캐시에 비해 속도는 느리다.
- 캐시 데이터가 변경되어도 추가적인 작업이 필요하지 않다.
현재 프로젝트에서는 1개의 WAS 서버가 동작 중이지만 향 후 2개로 늘릴 계획에 있어 Global 캐시 전략을 사용하기로 결정했고, 마침 Redis를 다른 기능을 위해 사용중에 있어서, Redis를 캐시 저장소로 결정하게 되었습니다.
4. RedisCache 적용하기
지금부터 본격적으로 Redis를 이용한 캐싱을 진행해보도록 하겠습니다.
4-1. Spring Data Redis 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
build.bradle에 해당 의존성을 추가해줍니다.
4-2. yml 설정
spring:
redis:
port: 6379
host: localhost
Redis와 연결을 위해 application.yml 파일에 설정을 추가해줍니다.
4-3. RedisConfig 작성
@EnableCaching
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@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(30)); // 캐시 수명 30분
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
}
- CacheManager : Spring에서 캐시를 관리하는 기본 인터페이스 Spring의 캐시 추상화를 통해 Redis에 데이터를 캐싱한다.
- serializeKeyWith : 캐시 키를 StringRedisSerializer를 사용해 직렬화하도록 설정한다.
- serializeValuesWith : 캐시 값의 직렬화 방식을 GenericJackson2JsonRedisSerializer를 사용해 값이 JSON 형식으로 직렬화되도록 설정한다. (GenericJackson2JsonRedisSerializer은 객체를 JSON형식으로 변환해 저장하므로 복잡한 자바 객체를 Redis에 저장하고 조회 시 객체로 역직렬화할 수 있다.)
- entryTtl : 캐시 데이터의 유효 시간을 설정한다. (TTL이 만료되면 캐시된 데이터는 자동으로 삭제되어 메모리를 효율적으로 관리할 수 있다 비즈니스 로직에 맞게 적절한 유효기간 설정이 필요하다.)
4-4. @Cacheable 적용
@Cacheable(value = "ticket", key = "#ticketId")
@Transactional(readOnly = true)
public TicketInfoResponse ticketInfo(final Long ticketId) {
final Ticket ticket = ticketRepository.findById(ticketId).orElseThrow(() -> {
throw new TicketNotFoundException(); });
TicketInfoResponse response = TicketInfoResponse.of(ticket);
return response;
}
Spring은 캐시 적용을 쉽게할 수 있도록 AOP 기반의 어노테이션을 제공하는데, 캐싱을 적용할 메서드에 @Cacheable을 선언하면 해당 메서드에 요청이 오면 데이터를 Redis에 캐싱하게 됩니다.
(key를 따로 명시하지 않을 경우 넘어오는 파라미터로 설정된다.)
4-5. LocalDataTime 사용시 문제점 및 해결방법
캐시를 적용후 테스트를 진행했더니 아래와 같은 Exception이 발생했습니다.
해당 오류는 Jackson이 날짜, 시간 타입을 직렬화하지 못해서 발생한 문제였습니다.
해당 문제를 해결하기 위해 Gradle 설정을 추가해줍니다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.3'
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime openDate;
그리고 해당 날짜타입 필드에 위와같은 어노테이션을 붙여주면 해당 문제를 해결할 수 있습니다.
4-6. Redis 캐시 테스트
이제 모든 설정을 마치고 캐시가 정상적으로 동작하는지 실행해봅시다.
처음 1회 요청을 보낸 후 레디스에서 keys * 명령어를 입력하면 스프링에서 설정해준대로 key가 저장된 것을 볼 수 있습니다.
get {해당 키 값} 명령어를 입력하면 티켓 조회 결과가 직렬화되어서 저장된 것을 볼 수 있습니다.
초기 DB 데이터 조회 이후 Redis에 캐시 데이터가 저장된 후에는
조회 요청을 연속적으로 보내도 조회를 위한 쿼리가 나가지 않는 모습을 볼 수 있습니다.
참고
'Backend > 프로젝트' 카테고리의 다른 글
무중단 배포 적용하기 (0) | 2025.01.01 |
---|---|
[Spring] 커버링 인덱스를 통한 페이징 성능 개선하기 (0) | 2024.12.12 |
nGrinder Docker 설치 및 사용방법 (0) | 2024.10.25 |
[스프링] 토스 간편결제 기능 구현하기 (0) | 2024.09.10 |
Redis 분산 락(Distribution Lock)으로 동시성 문제 해결하기(+ AOP) (0) | 2024.08.18 |