"유튜브 조회수나 인스타그램 '좋아요'처럼 1초에 수천 번 클릭이 일어나는 데이터를 매번 DB에 UPDATE 쿼리로 날리면 어떻게 될까요?"
DB는 디스크 I/O가 발생하기 때문에 필연적으로 느립니다. 그래서 우리는 메모리 기반의 캐시(Redis, Memcached)를 앞에 둡니다. 하지만 캐시와 DB 사이의 데이터 동기화(Sync) 타이밍을 어떻게 잡느냐가 시스템의 성능을 결정짓습니다.
1. 방식 1: 룩 어사이드 (Look Aside / Cache Aside) - "정석이자 표준"
**"캐시를 옆(Aside)에 두고 필요할 때만 본다"**는 뜻으로, 엔터프라이즈 환경에서 가장 범용적으로 쓰이는 읽기 중심 전략입니다.
동작 순서
- 조회(Read):
- 애플리케이션이 먼저 캐시를 찌릅니다.
- 있으면(Cache Hit) 바로 반환합니다. (빠름)
- 없으면(Cache Miss) DB에서 데이터를 조회한 뒤, 캐시에 저장하고 반환합니다. (Lazy Loading)
- 수정(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에 몰아서 저장한다"**는 방식입니다. 쓰기 작업이 엄청나게 많은 서비스에서 사용합니다.
동작 순서
- 수정(Write):
- 데이터를 캐시(Redis)에만 저장하고 바로 "성공" 응답을 줍니다. (엄청 빠름)
- DB에는 저장하지 않습니다.
- 동기화(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를 보호해야 합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| CQRS 패턴(CQRS Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| API 게이트웨이 패턴(API Gateway) 완벽 정리 (0) | 2025.12.07 |
| 서킷 브레이커 패턴(Circuit Breaker Pattern) 완벽 정리 (1) | 2025.12.07 |
| ORM 패턴(ORM Pattern) 완벽 정리 (0) | 2025.12.07 |
| 사가 패턴(Saga Pattern) 완벽 정리: 코레오그래피(Choreography) vs 오케스트레이션(Orchestration) (0) | 2025.12.07 |