스프링 서버에서 Redis 적용
현재 진행 중인 중고책 거래 플랫폼 프로젝트에 Redis 적용
- Trade 단일 조회에 cache aside 적용 (TTL 10분)
- TradeList 전체 조회에 cache aside 적용 (TTL 5분)
*아래 메서드들은 Service 계층이고 기본적으로 @Transactional(readOnly = true) 적용 중
1. Trade 단일 조회
캐시 등록
// 판매글 단일 조회
@Cacheable(value = "trade", key = "#trade_seq", unless = "#result == null")
public TradeVO search(long trade_seq) {
// 쿼리를 2번 조회하기 때문에 cache
TradeVO findTrade = tradeMapper.findBySeq(trade_seq);
if (findTrade == null) {
log.info("데이터 조회 실패 : DB에서 데이터를 조회하지 못했습니다.");
throw new TradeNotFoundException("Cannot find trade_seq=" + trade_seq);
}
List<TradeImageVO> imgUrl = tradeMapper.findImgUrl(trade_seq); // imgUrl은 null 이어도 된다 (썸네일 이미지만 보여진다)
findTrade.setTrade_img(imgUrl);
return findTrade;
}
@Cacheable : Redis 캐시에서 조회, 없으면 DB 조회 후 캐시 저장 (캐시 등록)
@Cacheable(value = "trade", key = "#trade_seq", unless = "#result == null")
캐시 무효화
@Transactional
@CacheEvict(value = "trade", key = "#trade_seq")
public boolean modify(Long trade_seq, TradeVO updateTrade) {
// tradeSeq : 기존 trade의 seq, updateTrade : 변경값을 담은 trade 객체
// 변경하려는 trade에 현재 seq을 넣기
updateTrade.setTrade_seq(trade_seq);
int result = tradeMapper.update(updateTrade);
log.info("Updated result count = {}", result);
// 이미지 처리
List<String> newImgUrls = updateTrade.getImgUrls();
if (newImgUrls == null) newImgUrls = new ArrayList<>();
// 기존 이미지 목록 조회
List<TradeImageVO> existingImages = tradeMapper.findImgUrl(trade_seq);
// S3에서 삭제할 이미지 찾기 (기존 이미지 중 새 목록에 없는 것)
if (existingImages != null && !existingImages.isEmpty()) {
List<String> urlsToDelete = new ArrayList<>();
for (TradeImageVO existingImg : existingImages) {
String existingUrl = existingImg.getImg_url();
if (!newImgUrls.contains(existingUrl)) {
// S3 이미지인 경우만 삭제 (http로 시작하는 URL)
if (existingUrl != null && existingUrl.startsWith("http")) {
urlsToDelete.add(existingUrl);
}
}
}
// S3에서 삭제
if (!urlsToDelete.isEmpty()) {
try {
s3Service.deleteFilesByUrls(urlsToDelete);
log.info("Deleted S3 images: {}", urlsToDelete);
} catch (Exception e) {
log.error("Failed to delete S3 images: {}", e.getMessage());
}
}
}
// DB에서 기존 이미지 전부 삭제
bookImgMapper.deleteBySeq(trade_seq);
// 새 이미지 목록이 있으면 저장 (유지할 이미지 + 새로 업로드한 이미지)
if (!newImgUrls.isEmpty()) {
for (String imgUrl : newImgUrls) {
log.info("Saving image: {} for trade_seq: {}", imgUrl, trade_seq);
bookImgMapper.save(imgUrl, trade_seq);
}
}
return result > 0;
}
@CacheEvict(value = "trade", key = "#trade_seq")
// 판매글 삭제
@Transactional
@CacheEvict(value = "trade", key = "#trade_seq")
public boolean remove(Long trade_seq)
@CacheEvict : 해당 메서드가 실행되면 캐시에 있는 값을 제거 (캐시 무효화)
주로 DB에 데이터 insert, update, delete 시 데이터 정합성을 위해 사용한다.
value : "trade" : 캐시 그룹 이름 (Redis key - value의 value와 다른 개념)
key : #trade_seq : Redis에 적용되는 Key, 파라미터로 들어오는 trade_seq
value : trade_seq으로 캐시에서 조회한 객체, 캐시에 존재하지 않는다면 DB에서 조회되는 findTrade 객체
unless = "#result == null" : 메서드 실행 결과(result)가 null이면 캐시에 저장 X
2. TradeList 전체 조회
캐시 등록
// 리스트 : 페이징 조회
@Cacheable(value = "tradeList",
key = "'page:' + #page + ':size:' + #size + ':cat:' +#searchVO.category_seq + ':word:' + #searchVO.search_word + ':sort:' +#searchVO.sort")
public List<TradeVO> searchAllWithPaging(int page, int size, TradeVO searchVO) {
int offset = (page - 1) * size; // page가 1부터 시작한다고 가정
return tradeMapper.findAllWithPaging(size, offset, searchVO);
}
// 전체 개수
@Cacheable(value = "tradeList",
key = "'count:cat:' + #searchVO.category_seq + ':word:' +#searchVO.search_word")
public int countAll(TradeVO searchVO) {
return tradeMapper.countAll(searchVO);
}
value : "tradeList" : 캐시 그룹 이름 (Redis key - value의 value와 다른 개념)
searchAllWithPaging key 예시
tradeList::page:1:size:10:cat:3:word:어린왕자:sort:crt_dtm
- 'page:' + #page → 페이지 번호
- 'size:' + #size → 페이지당 데이터 개수
- 'cat:' + #searchVO.category_seq → 카테고리 필터
- 'word:' + #searchVO.search_word → 검색어
- 'sort:' + #searchVO.sort → 정렬 기준
countAll key 예시
tradeList::count:cat:3:word:노트북
- 페이지와 상관 없이 전체 개수는 동일하므로 page나 size는 key에서 제외
- DB 조회 없이 바로 Redis에서 가져오면 페이징 UI에서 총 페이지 계산을 빠르게 처리 가능
캐시 무효화
판매글 등록, 수정, 삭제 Service 메서드
// 판매글 등록
@Transactional
@CacheEvict(value = "tradeList", allEntries = true)
public boolean upload(TradeVO tradeVO)
// 판매글 수정
@Transactional
@Caching(evict = {
@CacheEvict(value = "trade", key = "#tradeSeq"),
@CacheEvict(value = "tradeList", allEntries = true)
})
public boolean modify(Long trade_seq, TradeVO updateTrade)
// 판매글 삭제
@Transactional
@Caching(evict = {
@CacheEvict(value = "trade", key = "#tradeSeq"),
@CacheEvict(value = "tradeList", allEntries = true)
})
public boolean remove(Long trade_seq)