[Spring] @Cacheable, @CachePut, @CacheEvict: 데이터 흐름으로 이해하는 캐시 전략

Jun_k·2026년 4월 13일

Spring

목록 보기
9/9

캐시(Cache)는 왜 쓸까?

  • 데이터베이스는 거대한 창고와 같다.
    원하는 정보를 찾으려면 그 넓은 창고에서 일일이 찾아야 한다.
    당연히 시간도 많이 소모되고, 그에 대한 비용도 소모될 것이다.

  • 반면 캐시는 책상 위에 놓인 메모지이다.
    자주 찾는 정보는 메모지에 적어두고, 필요할 때마다 펼쳐서 보면
    당연히 창고까지 일일이 갈 필요 없이 바로 답을 얻을 수 있기에
    속도가 압도적으로 빠르다.

  • Spring Cache의 어노테이션들은 "메모지를 관리하는 규칙들"이다.


@Cacheable: "메모지에 있으면 그거 주고, 없으면 창고 가서 적어와"

  • 가장 많이 쓰이는 조회 규칙

  • 동작 흐름

    • 사용자가 "1번 책 어딨어?"라고 물으면, 사서는 먼저
      책상 위 메모지(@Cacheable)를 본다.

    • 케이스 A (Cache Hit): 메모지에 "1번 책은 A 구역에 있음"이라고,
      적혀 있으면 -> 창고에 안 가고, 바로 필요한 값을 준다. (메서드 실행 X)

    • 케이스 B (Cache Miss): 메모지에 내용이 없다면? 사서가 직접
      창고(DB)에 가서 데이터를 가져오고, 다음에 또 물어볼 것에 대비하여
      메모지에 그 내용을 적어둔다. (메서드 실행 O)

    • 똑같은 질문을 여러 번 할 때, 두 번째부터는
      창고에 가는 수고(연산/조회 비용)를 사실상 0으로 만든다.


@CachePut: "창고 작업은 무조건 하고, 메모지도 최신으로 고쳐 써"

  • 이것은 수정 규칙이다.

  • @Cacheable과 가장 큰 차이는 "일단 창고에 가느냐 마느냐"이다.

  • 동작 흐름

    • 사서는 메모지를 보지 않고, 무조건 창고(DB)로 달려가서 정보를 수정한다.
      (메서드 무조건 실행)

    • 수정이 끝났으면, 그 결과물을 메모지에 새로 덮어쓴다.

  • 왜 @Cacheable을 안 쓰고 이걸 쓰나?

    • @Cacheable은 메모지에 기록이 있으면 창고(DB)를 안 가버린다.
      하지만 데이터가 수정되었을 때는 실제 DB를 고치는 작업이
      반드시 일어나야 하기 때문에 @CachePut을 사용한다.

@CacheEvict: "데이터가 바뀌었으니 낡은 메모지는 버려"

  • 삭제 or 초기화 규칙이다.

  • 동작 흐름

    • 삭제: 사서가 창고에서 책을 폐기하거나 아주 크게 정보를 바꾼다.

    • 파기: 이때 책상 위에 있던 낡은 메모지를 찢어서 버린다.

  • 왜 버려야 하나?

    • 창고(DB)에서는 책이 사라졌는데 메모지에는 아직 'A구역에 있음'이라고,
      적혀 있으면 사서는 사용자에게 거짓말을 하는 꼴이 된다.
      이를 데이터 불일치라고 한다.
      이를 방지하기 위해 메모리를 깨끗하게 비우는 것이다.

데이터 수정의 2가지 경우

  • 방법 A (@CachePut): "DB도 고치고, 메모지(캐시)도 새 내용으로 고쳐 쓰자."

    • 장점: 다음 조회 시 즉시 캐시에서 데이터를 가져올 수 있어 매우 빠름.

    • 단점: 수정하는 과정에서 메모지에 적을
      데이터를 만드는 연산이 복잡하거나 비용이 크다면 비효율적임.

  • 방법 B (@CacheEvict): "DB는 고치고, 메모지는 그냥 찢어서 버리자."

    • 장점: 메모지를 새로 적는 수고를 덜 수 있음.
      어차피 나중에 누군가 요청할 때 다시 창고(DB)에서
      최신 데이터를 가져와서 적으면 됨.

    • 논리: "낡은 데이터(수정 전)를 보여주느니,
      차라리 메모지를 없애서(삭제) 다음 사람에게 정확한 정보를
      새로 가져와서 전달해주겠다."는 전략이다.


코드 예시

DTO (캐시에 저장할 데이터는 불변의 record 사용)


// record를 사용해 불변 객체로 정의
public record Book(Long id, String title, String author) {}

서비스 로직


@Service
public class BookService {

    // 조회: 메모지에 있으면 주고, 없으면 창고(DB) 가서 가져와서 적어둠
    // books는 캐시의 저장소 이름을 지정한 것
    // key = "#id"는 메모지들 사이에서 특정 데이터 찾기 위한 이름표
    @Cacheable(value = "books", key = "#id")
    public Book getBook(Long id) {
        simulateSlowService(); // DB 조회를 시뮬레이션하는 무거운 로직
        return new Book(id, "Spring 공부법", "Junyoung");
    }

    // 수정: 창고(DB) 내용을 고치고, 메모지(캐시) 내용도 최신으로 업데이트함
    @CachePut(value = "books", key = "#book.id()")
    public Book updateBook(Book book) {
        // 실제로는 repository.save(book) 등이 호출됨
        return book; 
    }

    // 삭제: 창고(DB)에서 지우고, 메모지(캐시)도 찢어서 버림
    @CacheEvict(value = "books", key = "#id")
    public void deleteBook(Long id) {
        // 실제로는 repository.deleteById(id) 등이 호출됨
        System.out.println(id + "번 책이 삭제되었습니다.");
    }

    // 성능 차이를 확인하기 위한 가상 지연 메서드
    private void simulateSlowService() {
        try {
            Thread.sleep(3000); // 3초 대기
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }
}
profile
개발을 즐겨보자.

0개의 댓글