프로그래밍/디자인패턴

캐싱 패턴(Caching Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 19:52

"유튜브 조회수나 인스타그램 '좋아요'처럼 1초에 수천 번 클릭이 일어나는 데이터를 매번 DB에 UPDATE 쿼리로 날리면 어떻게 될까요?"

DB는 디스크 I/O가 발생하기 때문에 필연적으로 느립니다. 그래서 우리는 메모리 기반의 캐시(Redis, Memcached)를 앞에 둡니다. 하지만 캐시와 DB 사이의 데이터 동기화(Sync) 타이밍을 어떻게 잡느냐가 시스템의 성능을 결정짓습니다.


1. 방식 1: 룩 어사이드 (Look Aside / Cache Aside) - "정석이자 표준"

**"캐시를 옆(Aside)에 두고 필요할 때만 본다"**는 뜻으로, 엔터프라이즈 환경에서 가장 범용적으로 쓰이는 읽기 중심 전략입니다.

동작 순서

  1. 조회(Read):
    • 애플리케이션이 먼저 캐시를 찌릅니다.
    • 있으면(Cache Hit) 바로 반환합니다. (빠름)
    • 없으면(Cache Miss) DB에서 데이터를 조회한 뒤, 캐시에 저장하고 반환합니다. (Lazy Loading)
  2. 수정(Write):
    • DB에 데이터를 업데이트합니다.
    • **캐시 데이터를 삭제(Invalidate)**하거나 갱신합니다. (주로 삭제를 권장)

코드 예시 (Spring & Redis)

Java
 
@Service
@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;
    private final RedisTemplate<String, Object> redisTemplate;

    public BoardDTO getBoard(Long id) {
        String key = "board:" + id;

        // 1. 캐시 조회 (Look Aside)
        BoardDTO cachedBoard = (BoardDTO) redisTemplate.opsForValue().get(key);
        if (cachedBoard != null) {
            return cachedBoard; // Hit
        }

        // 2. 캐시 Miss -> DB 조회
        Board board = boardRepository.findById(id).orElseThrow();
        BoardDTO dto = new BoardDTO(board);

        // 3. 캐시에 저장 (다음 요청을 위해)
        redisTemplate.opsForValue().set(key, dto, Duration.ofMinutes(10));
        
        return dto;
    }
    
    public void updateBoard(BoardDTO dto) {
        // 1. DB 수정
        boardRepository.update(dto);
        // 2. 캐시 삭제 (데이터 정합성을 위해)
        redisTemplate.delete("board:" + dto.getId());
    }
}

장단점

  • 장점:
    • 안정성: Redis가 죽어도 DB에서 데이터를 가져오면 되므로 서비스가 중단되지 않습니다.
    • 데이터 정합성: DB에 확실히 저장된 데이터만 캐시에 올라옵니다.
  • 단점:
    • 첫 조회 속도: 캐시에 없는 데이터(Cold Data)를 처음 조회할 때는 DB를 거쳐야 하므로 느립니다. (이를 막기 위해 'Cache Warming'을 하기도 함)
    • Thundering Herd: 트래픽이 폭주할 때 캐시가 만료되면, 수만 개의 요청이 동시에 DB를 때려서 DB가 뻗을 수 있습니다.

2. 방식 2: 라이트 백 (Write Back / Write Behind) - "쓰기 최적화"

**"일단 캐시에 먼저 쓰고, 나중에 DB에 몰아서 저장한다"**는 방식입니다. 쓰기 작업이 엄청나게 많은 서비스에서 사용합니다.

동작 순서

  1. 수정(Write):
    • 데이터를 캐시(Redis)에만 저장하고 바로 "성공" 응답을 줍니다. (엄청 빠름)
    • DB에는 저장하지 않습니다.
  2. 동기화(Sync):
    • 배치(Batch) 작업이나 스케줄러가 주기적으로 돌면서, 캐시에 쌓인 데이터를 뭉쳐서 **DB에 한 번에 저장(Bulk Insert)**합니다.

코드 예시 (조회수 증가 로직)

Java
 
@Service
public class ViewCountService {
    
    // 1. 사용자가 클릭하면 DB 안 가고 캐시만 증가시킴
    public void increaseViewCount(Long videoId) {
        // Redis의 INCR 명령어 사용 (메모리 연산이라 초고속)
        redisTemplate.opsForValue().increment("views:" + videoId);
    }

    // 2. 스케줄러가 1분마다 실행 (비동기 동기화)
    @Scheduled(fixedRate = 60000)
    public void syncToDatabase() {
        Set<String> keys = redisTemplate.keys("views:*");
        
        for (String key : keys) {
            Integer views = (Integer) redisTemplate.opsForValue().get(key);
            Long videoId = Long.parseLong(key.split(":")[1]);
            
            // DB에 반영
            videoRepository.updateViews(videoId, views);
            
            // 반영 후 캐시 초기화 등 후처리
        }
    }
}

장단점

  • 장점:
    • 쓰기 성능 최강: DB 부하를 획기적으로 줄일 수 있습니다. (1000번의 UPDATE를 1번의 Bulk UPDATE로 처리)
  • 단점:
    • 데이터 유실 위험: DB에 저장되기 전에 캐시 서버(Redis)가 꺼지면 데이터가 날아갑니다. (조회수가 좀 날아가는 건 괜찮지만, 결제 데이터라면 대참사)
    • 구현 복잡도: 캐시와 DB 데이터가 일시적으로 불일치하는 상황을 관리해야 합니다.

3. 실무 비교: 언제 무엇을 쓰는가?

구분 Look Aside (일반적) Write Back (특수 목적)
주 목적 읽기 속도 향상 (Read Optimization) 쓰기 속도 향상 (Write Optimization)
DB 접근 Miss일 때 조회, 수정 시 즉시 반영 스케줄러에 의해 지연 반영(Lazy Write)
데이터 정합성 높음 (DB가 원본) 낮음 (캐시가 최신, 유실 가능성 있음)
장애 영향 캐시 죽어도 서비스 가능 캐시 죽으면 데이터 영구 손실
추천 상황 쇼핑몰 상품 정보, 사용자 프로필, 게시글 본문 유튜브 조회수, 좋아요 수, 로그 수집

결론

"대부분의 경우(90% 이상)에는 룩 어사이드(Look Aside) 패턴이 정답입니다." 데이터 안전성이 가장 중요하기 때문입니다.

하지만 **"데이터가 약간 유실되어도 괜찮으니, 실시간으로 폭주하는 쓰기 트래픽을 감당해야 한다"**면(예: 라이브 방송 채팅, 조회수 집계) 라이트 백(Write Back) 패턴을 부분적으로 도입하여 DB를 보호해야 합니다.