어제 오늘 내일

[Spring Boot] 조회 속도 0.1초의 비밀? Redis 캐싱(Caching) 전략 완벽 정리 (Cache-Aside) 본문

IT/SpringBoot

[Spring Boot] 조회 속도 0.1초의 비밀? Redis 캐싱(Caching) 전략 완벽 정리 (Cache-Aside)

hi.anna 2026. 3. 20. 10:17

 
서비스를 운영하다 보면 사용자가 늘어날수록 데이터베이스(DB)가 힘들어하는 소리가 들리기 시작합니다.
"게시글 목록 좀 보여줘", "내 정보 좀 보여줘"... 똑같은 데이터를 수천 명이 동시에 요청하면 DB는 결국 뻗어버리고 맙니다.
이때 구세주처럼 등장하는 것이 바로 캐시(Cache)입니다.
오늘은 "메모리에 데이터를 저장해서 100배 빠르게 조회하는 기술", Redis와 가장 대중적인 캐싱 전략인 Look Aside(Cache Aside) 패턴을 스프링 부트로 구현해 보겠습니다.


1. 왜 Redis를 써야 하나요? (Disk vs Memory)

우리가 흔히 쓰는 MySQL, Oracle 같은 RDB는 데이터를 하드디스크(Disk)에 저장합니다. 안전하지만 느립니다.
반면 Redis는 데이터를 RAM(Memory)에 저장합니다. 휘발성이지만 속도가 미친 듯이 빠릅니다.

  • MySQL: "잠시만요, 창고(Disk) 가서 서류 좀 찾아올게요..." (몇 ms ~ 몇 초)
  • Redis: "아, 그거요? 여기 책상(Memory) 위에 바로 있어요." (0.몇 ms)

그래서 자주 조회되지만 잘 변하지 않는 데이터(예: 인기 검색어, 카테고리 목록, 랭킹)를 Redis에 넣어두면 성능이 획기적으로 개선됩니다.


2. 국민 캐싱 전략: Look Aside (Cache Aside) 패턴

캐시를 쓰는 방법은 여러 가지가 있지만, 읽기 전용으로 가장 많이 쓰는 방식은 Look Aside(룩 어사이드) 패턴입니다. 말 그대로 "캐시를 먼저 쳐다보고(Look), 없으면 DB로 간다(Aside)"는 뜻입니다.

  1. Client: "데이터 1번 줘!"
  2. Server: "잠깐, Redis에 있나?" (Look)
  3. Case 1 (Cache Hit): "오, 있다! 바로 줄게." (DB 안 감 -> 엄청 빠름)
  4. Case 2 (Cache Miss): "없네? DB 가서 가져오자." (DB 조회 -> Redis에 저장 -> 반환)

3. 스프링 부트에서 Redis 사용하기

Redis를 직접 설치해도 되지만, 로컬 개발 환경에서는 Docker를 쓰는 게 제일 편합니다.

# redis 실행 (포트 6379)
docker run -d -p 6379:6379 --name my-redis redis

① 의존성 추가 ()

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

② 설정 파일 ()

spring:
  data:
    redis:
      host: localhost
      port: 6379

4. 어노테이션으로 3초 만에 캐시 적용하기

스프링은 위대한 추상화를 제공합니다. RedisTemplate을 직접 짜서 데이터를 넣고 빼고 할 수도 있지만, @Cacheable 어노테이션을 쓰면 비즈니스 로직을 건드리지 않고 캐시를 붙일 수 있습니다.

1. 캐시 기능 켜기 ()

@EnableCaching // ★ 이거 꼭 붙여야 작동함!
@SpringBootApplication
public class MyServerApplication { ... }

2. 조회 메서드에 적용 ()

@Service
public class BoardService {

    @Autowired BoardRepository boardRepository;

    // cacheNames: 캐시 이름 (폴더 같은 개념)
    // key: 캐시 안에 저장할 키 (id 값에 따라 다르게 저장)
    // unless: 조건부 캐싱 (null이 아닐 때만 캐시해라)
    @Cacheable(cacheNames = "getBoard", key = "#id", unless = "#result == null")
    public BoardResponseDto getBoard(Long id) {
        log.info("DB에서 데이터 조회 중... (캐시 없어서 여기 왔음)");

        Board board = boardRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("게시글 없음"));

        return new BoardResponseDto(board);
    }
}

결과 확인:

  1. 첫 번째 요청: 로그에 "DB에서 데이터 조회 중..."이 찍힘. (Redis에 저장됨)
  2. 두 번째 요청: 로그가 안 찍힘! (Redis에서 바로 리턴됨)

5. 데이터가 수정되면 어떡해요? ()

치명적인 문제가 있습니다. 누군가 게시글을 수정했는데, Redis에는 옛날 데이터가 남아있다면? 사용자는 계속 수정 전 내용을 보게 됩니다. (데이터 불일치)
그래서 수정(Update)이나 삭제(Delete)가 일어날 때, 캐시를 날려버려야(Evict) 합니다.

    @Transactional
    @CacheEvict(cacheNames = "getBoard", key = "#id") // 해당 키의 캐시 삭제!
    public void updateBoard(Long id, BoardUpdateRequestDto requestDto) {
        Board board = boardRepository.findById(id).orElseThrow();
        board.update(requestDto);
        // 메서드가 성공적으로 끝나면 Redis에서 'getBoard::id' 데이터가 삭제됨
        // -> 다음 조회 때 DB에서 새 데이터를 가져오게 됨 (갱신)
    }

6. 주의사항: DTO에는 기본 생성자가 필요해요!

Redis는 자바 객체를 JSON이나 바이너리로 직렬화(Serialization)해서 저장합니다. 나중에 꺼낼 때 다시 자바 객체(역직렬화)로 만들려면, DTO 클래스에 기본 생성자(NoArgsConstructor)가 반드시 있어야 합니다.

@Getter
@NoArgsConstructor // ★ 필수! (Jackson 라이브러리가 사용함)
@AllArgsConstructor
public class BoardResponseDto implements Serializable {
    private Long id;
    private String title;
    // ...
}

또한, implements Serializable을 붙여주는 것이 정신 건강에 좋습니다.


마치며

오늘의 결론입니다.

  1. Redis는 메모리 기반이라 DB보다 압도적으로 빠르다.
  2. Look Aside 패턴은 "있으면 주고, 없으면 DB에서 가져와 저장하는" 가장 무난한 전략이다.
  3. @Cacheable로 저장하고, @CacheEvict로 삭제하여 데이터 일관성을 맞춘다.

이것만 적용해도 여러분의 서버는 트래픽 폭주 상황에서 DB를 지켜내는 든든한 방패를 얻게 된 것입니다.
다음 포스팅에서는 "선착순 이벤트 100명! 근데 101명이 결제됐다고?" 동시성 이슈를 해결하는 Redis 분산락(Distributed Lock)에 대해 알아보겠습니다. (재고 관리 시스템의 필수품이죠!)
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments