-
레디스 도입하기 [기초]백엔드 : 서버공부/Spring 2024. 5. 5. 15:45728x90
최근 개인 공부용으로 프로젝트를 진행 중인데요, https://github.com/Mouon/issuehub
이번에는 캐싱을 도입해보았습니다.
도입한 이유는 데이터베이스에 부하가 걸리는 것을 최소화해보기 위해서입니다.
이를 위해 레디스를 도입해보기로 하였구요 이를 통해 데이터를 캐시로 저장하고 빠르게 접근할 수 있도록 해보겠습니다.
캐시 도입시 간단한 흐름을 보면 아래와 같습니다.
- 클라이언트가 요청을 보내면, 서버에서 해당 요청에 대한 데이터를 레디스 캐시에서 검색합니다.
- 캐시에 데이터가 존재하면, 캐시된 데이터를 클라이언트에 반환합니다.
- 캐시에 데이터가 존재하지 않으면, 데이터베이스나 다른 소스에서 데이터를 가져와서 캐시에 저장한 후 클라이언트에 반환합니다.
- 데이터베이스나 다른 소스에서 가져온 데이터는 캐시에 저장되어 다음에 동일한 요청이 발생할 때 사용됩니다.
이제 프로젝트에 캐시를 도입해 보겠습니다. 먼저 프로젝트에 Redis관련 의존성을 추가해보겠습니다.
Redis 의존성을 추가하려면, buid.gradle 파일에 필요한 의존성을 명시해야 합니다. 다음 두 가지 주요 의존성을 포함해보겠습니다.
- Spring Boot Starter Data Redis: 스프링 부트와 Redis를 쉽게 통합할 수 있게 해주는 스타터 패키지입니다.
- Jedis: Redis 서버와 통신하기 위한 자바 클라이언트 라이브러리인 Jedis입니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'redis.clients:jedis'
이후 application.yml 파일에 Redis의 호스트 및 포트 정보를 설정합니다.
spring: redis: host: localhost port: 6379 jedis: pool: max-active: 10 max-idle: 5 min-idle: 1 max-wait: -1ms
그리고 @EnableCaching 어노테이션을 캐싱 설정 클래스에 추가하여 캐싱을 활성화했습니다.
import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Configuration; @Configuration @EnableCaching public class CacheConfig { //여러 설정들 }
그다음 이슈 상세조회 로직 부분에 캐싱을 적용해 보겠습니다. @Cacheable 어노테이션을 사용하여 캐싱할 메서드를 지정합니다.
import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class IssueService { @Cacheable(value = "issue", key = "#issueId") public IssueDTO findIssuesDetail(Long memberId, Long issueId) { //레포지에서 조회 //조회수 증가 return new IssueDTO(issue); } }
여기서 value는 캐시의 이름을 나타내고, key는 캐시 내에서 유니크한 데이터를 식별하는 데 사용됩니다. #issueId는 메서드 파라미터중 issueId 값을 key로 사용하겠다는 것을 의미합니다.
캐시 만료 및 관리
캐시 데이터는 항상 최신 상태를 유지해야 하기때문에, 이슈 리스트가 변경될 때 캐시도 업데이트 해야 합니다.
이를 위해 @CachePut 어노테이션을 데이터를 업데이트하는 로직에 추가합니다.
예를 들어, 이슈가 추가되거나 업데이트되는 경우 캐시를 업데이트하려면 @CachePut 어노테이션을 사용하면됩니다.
import org.springframework.cache.annotation.CachePut; @CachePut(value = "issues", key = "#issue.keyword") public IssueDTO updateIssue(IssueDTO issue) { return issueRepository.save(issue); }
그런데..
그리고 서버를 키니 문제가생겼습니다.
java.io.NotSerializableException 오류
IssueDTO 객체가 직렬화할 수 없다는 것을 의미합니다.
Redis는 객체를 캐시에 저장하기 전에 직렬화 과정을 거치는데,
해당 객체가 Serializable 인터페이스를 구현하지 않았기 때문에 이 오류가 발생한 것입니다.
이 오류를 해결하기 위해 DTO 클래스에 Serializable 인터페이스를 추가하였습니다.
IssueDTO 클래스와 그 내부에 있는 모든 객체들이 java.io.Serializable 인터페이스를 구현하도록 수정해야 합니다.
이렇게 하면 Java의 기본 직렬화 메커니즘을 사용할 수 있게 됩니다.
import java.io.Serializable; public class IssueListDTO implements Serializable { private static final long serialVersionUID = 1L; // 기존 코드 유지 }
그런데 또..
새로운 문제가 발생했습니다. java.lang.ClassCastException 오류가 발생했습니다.
이는 클래스 로더 간의 충돌로 인한 문제였습니다.
이 문제를 해결하기 위해 GenericJackson2JsonRedisSerializer를 도입하여
객체를 JSON 형식으로 직렬화하고 저장하였습니다. 아래는 캐시 사용한 설정 클래스 입니다.
import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .disableCachingNullValues() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); cacheManager.setTransactionAware(true); cacheManager.afterPropertiesSet(); return cacheManager; } }
이제 기본적인 캐싱구현은 끝났습니다. 이제 동일한 이슈를 여러번 상세 조회하면 최초 조회 이후에는
DB에 접근이 없이 상세조회를 할 수 있을것으로 기대됩니다.
테스트를 위해 서버를 키고 로그를 살펴 보겠습니다.
위 로그를 보면 GET "/issues/detail?id=2265520817" 요청이 여러 번 수행되었음에도 불구하고, 최초 호출을 제외하고는 각 요청 사이에 데이터베이스의 조회나 업데이트 SQL 쿼리 로그가 발생하지 않았다는 점을 확인 할 수 있습니다.
포스트맨을 통해 확인해보면 반환내용은 적절한 것으로 확인이 됩니다.
이는 요청마다 새로 데이터베이스를 조회하지 않고 캐시된 데이터를 사용했다는 것을 의미합니다. 캐시가 제대로 작동하고 있음을 알 수 있습니다.
GET "/issues/detail?id=1642352785" 요청을 여러 번 수행해보겠습니다.
마찬가지로 로그를 살펴보면 반복된 요청에대해서는 쿼리문이 발생하지않고 응답에 성공하는것을 확인 할 수 있습니다.
3번의 요청을 해본후 각각의 응답 시간을 비교해 보겠습니다.
첫번째 요청 두번째 요청 세번째 요청 첫번째 요청에선 21ms의 응답시간을 소요하였는데 그 이후에는 각각 8ms, 9ms로 응답시간이 대폭 감소한것을 확인하실 수 있습니다.
캐시를 통해 반복되는 데이터 요청에 빠르게 응답할 수 있으므로,
실제 운영 환경에서의 효율성이 크게 개선될 것으로 기대됩니다.
여담이지만 또한 자동적으로 이슈 조회수 로직문제도 해결되었습니다. 한 사용자가 반복적으로 이슈를 반복해서 보아도 조회수가 증가하는 문제가 있었는데 캐시를 사용하면 일정시간동안 DBMS에 접근하지않고 캐싱된 데이터를 사용하니, 자세히보기라는 기능은 수행하면서 조회수 조적(?)도 방지하게 되었습니다.
오늘은 그만 알아보고 레디스의 작동원리와 캐싱의 자세한 내용은 다음글에서 공부하겠습니다.
'백엔드 : 서버공부 > Spring' 카테고리의 다른 글
EC2, REDIS,Docker로 CI/CD 구축하기 (0) 2024.07.10 <스프링 기초 - 자바> 2차원 배열 정렬 (0) 2024.05.23 스프링 : DTO를 사용하는 이유 (0) 2024.03.07 스프링 : 세터(setter) 사용시 문제점 (0) 2024.03.02 스프링 : BindingResult 를 통한 에러 처리해보기 (0) 2024.02.19