반응형
개념정리
SpringBoot기반 Redis Cache
실전프로젝트 시작 후 3주 동안 기본적으로 구상했던 기능들은 모두 구현하였다. 중간발표 후 성능에 대해 깊이 생각하지 않았던 것을 깨닫고 트래픽이 몰렸을 때 성능을 향상시킬 수 있도록 개선 중이다.
여러 부분 중 카테고리와 검색 시 로딩을 빠르게 하기위해 레디스의 캐시를 활용할려고 한다. 또한 검색어 랭킹 부분도 일일이 검색어를 DB에 저장해서 가장 많은 검색을 한 검색어 10개를 뽑아 냈는데 이부분도 캐시를 활용해 고쳐보자한다.
1. Cache의 개념과 특징
캐시란?
데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터에 접근할 수 있다.
- 한번 읽은(처리한) 데이타를 임시로 저장하고 필요에 따라 전송,갱신,삭제하는 기술로 보통은 데이타의 보관장소로 서버의 메모리를 사용하는 경우가 많다.
- 그렇기 때문에 디스크에서 정보를 얻어오는 것보다 훨씬 빠른 I/O성능을 얻을 수 있으나 서버가 다운되거나 재부팅되는 경우 사라지는 성격의 휘발성을 가지고 있어 임시적으로 보관하고 빠르게 그 정보에 접근하기 위한 용도로 사용해야 한다.
- 백앤드 영역에서 Cache를 적극적으로 사용하게 되면 드라마틱한 서비스 성능 개선을 가져올 수 있다.
- 반대로 용도에 맞는 않는 정보나 서비스요청에 캐시를 남용하게 되면 서비스 신뢰도에 큰 문제가 생길 수 있는 위험성도 내포하고 있다.
2. Cache의 목적과 필요성
2-1. Cache를 사용하는 이유
- 서버간 불필요한 트래픽을 줄일 수 있다.
- 트래픽을 줄이므로서 웹어플리케이션 서버의 부하를 감소시킨다.
- 어플리케이션의 빠른 처리성능(조회)을 확보해서 궁극적으로 어플리케이션를 사용하는 고객에게 쾌적한 서비스경험을 제공한다.
2-2. Cache의 대상이 되는 정보들
- 단순한, 또는 단순한 구조의 정보를 -> 정보의 단순성
- 반복적으로 동일하게 제공해야 하거나 -> 빈번한 동일요청의 반복
- 정보의 변경주기가 빈번하지 않고, 단위처리 시간이 오래걸리는 정보이고 -> 높은 단위처리비용
- 정보의 최신화가 반드시 실시간으로 이뤄지지 않아도 서비스 품질에 영향을 거의 주지 않는 정보
2-2. Cache의 활용
- 포탈의 검색어
- 쇼핑몰의 핫딜상품, 베스트셀러, 추천상품등
- 상품의 카테고리와 카테고리별 등록상품 수
- 방문자수, 조회수, 추천수
- 1회성 인증정보 (SMS 본인인증정보, IP정보등)
- 공지사항, Q&A
3. Redis Cache 사용하기
3-1. 스프링부트 의존성 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
- 우리 프로젝트에서는 채팅기능 구현이 되어 있어 미리 추가 되어 있었다.
3-2. Redis 서버 설정
- 처음사용해보는거라 일단 host는 localhost로 세팅했다.
// yml 파일에서
spring:
cache:
type: redis
redis:
host: localhost
port: 6379
// properties 파일에서
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
3-3. Springboot Main Application Class에 @EnableCaching 어노테이션 추가
@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}
3-5. redisConfig 파일 만들기
- Value로 yml에 세팅한 값을 넘겨주어야 한다.
- @Value를 사용할 때 자꾸 롬복을 import해서 오류가 났었다. 별거아니지만 꼭 확인하기!
- RedisCacheConfiguration의 valueSerializer인 Jackson2 는 LocalDate 타입을 인식하지 못하기 때문에 그에 따른 에러를 방지하기 위해 관련 모듈을 추가한 ObjectMapper를 사용했다. Serializer에 전달하기 위한 메서드이다.
더보기
전체코드
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableRedisRepositories
public class RedisConfiguration extends CachingConfigurerSupport {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
// jackson LocalDateTime mapper
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // timestamp 형식 안따르도록 설정
mapper.registerModules(new JavaTimeModule(), new Jdk8Module()); // LocalDateTime 매핑을 위해 모듈 활성화
return mapper;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}
//레디스 캐시
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
3-5. 사용할 Method ( controller 메서드 or service 메서드)에 어노테이션 달아주기
- 캐시의 key값은 파라미터를 이용해서 가져올 수 있다. 내가 원하는 key는 responsDto였는데 찾을 수 없어 한참 헤매다가 그냥 requestDto를 넣었는데 잘 나온다 이부분은 나중에 테스트를 하면서 조금 더 살펴봐야할 것 같다.
- value : 캐시 이름을 설정하는 속성
- key : 캐시의 key를 정하는 속성
- 화면에서 무한스크롤을 위한 페이징을 하고 있어서 PageNumber에 대한 정보까지 포함하여 키값으로 세팅했다
더보기
서비스 전체 코드
import com.team11.backend.dto.BookMarkDto;
import com.team11.backend.dto.CategoryDto;
import com.team11.backend.dto.PostDto;
import com.team11.backend.model.BookMark;
import com.team11.backend.model.Post;
import com.team11.backend.repository.BookMarkRepository;
import com.team11.backend.repository.CommentRepository;
import com.team11.backend.repository.querydsl.CategoryRepository;
import com.team11.backend.timeConversion.TimeConversion;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Service
public class CategoryService {
private final CategoryRepository categoryRepository;
private final CommentRepository commentRepository;
private final BookMarkRepository bookMarkRepository;
@Cacheable(value = "categoryList", key = "#categoryRequestDto.hashCode() + #pageable.pageNumber")
@Transactional
public List<CategoryDto.ResponseDto> categoryFilter(CategoryDto.RequestDto categoryRequestDto,
Pageable pageable) {
PageImpl<Post> posts = categoryRepository.categoryFilter(categoryRequestDto, pageable);
return posts.stream()
.map(s -> new CategoryDto.ResponseDto(
s.getCategory(),
s.getId(),
s.getUser().getProfileImg(),
s.getUser().getUsername(),
s.getUser().getNickname(),
s.getTitle(), s.getContent(),
s.getUser().getAddress(),
s.getImages(),
s.getBookMarks().stream()
.map(this::toBookmarkResponseDto)
.collect(Collectors.toList()),
s.getCurrentState(),
s.getMyItem(),
s.getExchangeItem(),
TimeConversion.timeConversion(s.getCreateAt()),
bookMarkRepository.countByPost(s).orElse(0),
commentRepository.countByPost(s).orElse(0)))
.collect(Collectors.toList());
}
private BookMarkDto.DetailResponseDto toBookmarkResponseDto(BookMark bookMark) {
return BookMarkDto.DetailResponseDto.builder()
.userId(bookMark.getUser().getId())
.build();
}
}
3-6. 캐싱이 잘 됐는지 확인하기
- redis-cli에서 조회해본 화면이다. 내용이 너무 많아서 중간에 잘랐지만 잘 나오는 것을 확인할 수 있다.
나는 단순히 조회기능만 필요해서 조회에 사용되는 @Cacheable만 사용했는데 삭제, 수정까지 가능하며 @Cacheable은 내가 사용한 속성외에도 여러가지 속성이 더 있다. 프로젝트 중이고 시간적인 여유가 많지 않아 여기까지 간단히 적어본다.
추가로 redis 캐시 테스트와 redis를 활용한 검색어랭킹을 보여주는 기능을 구현하는 방법도 포스팅으로 작성해볼려고 한다.
참고
반응형
'개발 하나둘셋 > Java & Spring' 카테고리의 다른 글
스프링 DI ,IOC, AOP (0) | 2022.04.11 |
---|---|
Spring에서 Redis Sorted set으로 인기검색어 순위 나타내기 (4) | 2022.01.15 |
웹소켓(WebSocket)으로 채팅기능 구현하기 (0) | 2022.01.03 |
OOP의 상속 / Implements와 Extends (0) | 2021.12.17 |
Spring Boot에서 CORS 방법 (0) | 2021.12.13 |