Infra/Redis

Redis 3

nippycloud 2026. 2. 25. 08:50

스프링 서버에서 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)

'Infra > Redis' 카테고리의 다른 글

Redis 2  (0) 2026.01.24
Redis 1  (1) 2025.12.27