사전 read-through
패턴

read-through

gabury1

캐시가 원본에서 값을 직접 읽어 오게 맡기는 방식입니다. 애플리케이션은 캐시에만 물어봅니다. 찾는 값이 캐시에 없으면 캐시가 원본을 읽어 채워 넣은 다음 돌려줍니다. 원본이 어디 있는지 아는 쪽은 애플리케이션이 아니라 캐시입니다.

상세

캐시에 로더를 붙여 두는 것이 이 방식의 전부입니다. read-through 에서 캐시는 SoR(system-of-record, 시스템 오브 레코드)에서 데이터를 어떻게 읽어 오는지 아는 로더 컴포넌트를 달고 설정됩니다. 값을 어디서 가져오는지 아는 코드가 애플리케이션 바깥, 캐시 쪽에 있습니다.

읽기 한 번을 키 하나로 풀면 이렇게 흐릅니다. 애플리케이션이 캐시에 키 X 를 묻습니다. X 가 캐시에 없으면 캐시가 자동으로 캐시 스토어에 위임해 밑에 깔린 데이터 소스에서 X 를 읽어 오라고 시킵니다. 데이터 소스에 X 가 있으면 캐시 스토어가 그것을 읽어 캐시에 돌려줍니다. 캐시는 다음 사용을 위해 그 값을 담아 둡니다. 마지막으로 X 를 요청한 애플리케이션 코드에 돌려줍니다.

sequenceDiagram
    autonumber
    participant 앱
    participant 캐시
    participant 로더
    participant 원본
    앱->>캐시: 키 읽기
    Note over 앱,캐시: 값이 있으면 여기서 끝난다
    캐시->>로더: 이 키를 실어 와라
    로더->>원본: 조회
    원본-->>로더: 값
    로더-->>캐시: 값
    Note over 캐시: 캐시에 넣는다
    캐시-->>앱: 값

같은 계약이 자바 캐시 표준의 get 한 줄에도 걸려 있습니다. 캐시가 read-through 를 쓰도록 설정돼 있고 항목이 캐시에 없어서 get 이 널을 돌려줄 상황이면, 캐시의 CacheLoader 가 그 항목을 실어 보려고 호출됩니다. 부르는 쪽 코드는 그대로 get 한 줄입니다. 미스가 났다는 사실도, 원본에 다녀왔다는 사실도 호출 자리에는 안 드러납니다.

미스가 난 호출은 원본까지 내려갔다 돌아옵니다. 그동안 부른 쪽은 기다립니다. 캐시 미스의 왕복을 애플리케이션이 직접 짜는 cache-aside 와 갈라지는 자리가 여기입니다. 원본을 조회하고 캐시를 채운 뒤 값을 돌려주는 일을 read-through 에서는 캐시가 합니다. cache-aside 에서는 애플리케이션 코드가 캐시를 직접 다룹니다.

읽기 쪽과 쓰기 쪽을 한 인터페이스로 묶어 두는 구현도 있습니다. read-through 와 write-through 의 개념을 CacheLoaderWriter 라는 인터페이스 하나 뒤로 합쳐 두고, 그중 load(K) 와 loadAll(Iterable<? super K>) 이 cache-through 의 read-through 쪽을 맡고, write(K, V) · writeAll(...) · delete(K) · deleteAll(...) 이 write-through 쪽을 맡습니다.

대가

얻는 것

  • 미스 분기가 호출 자리에 안 붙습니다. 캐시가 read-through 로 설정돼 있으면 항목이 없어 get 이 널을 돌려줄 상황에서 캐시가 CacheLoader 를 부릅니다. 원본을 조회하고 캐시를 채우는 절차를 부르는 쪽 코드에 적지 않습니다.
  • 원본에 접속하는 코드가 캐시 설정 한 곳에 모입니다. 원본에서 데이터를 어떻게 읽어 오는지 아는 것은 캐시에 붙인 로더 컴포넌트입니다. 캐시를 읽는 자리가 여럿이어도 그 절차는 한 벌입니다.

내주는 것

  • 미스가 난 호출이 원본 왕복을 그대로 기다립니다. 만료 시각을 넘긴 뒤에 접근된 객체는 캐시 스토어에서 동기 읽기로 값을 갱신합니다. 동기 읽기라는 말이 곧 호출자가 그 시간을 떠안는다는 뜻입니다. 이 지연을 없애려면 만료 전에 비동기로 다시 읽는 refresh-ahead 를 따로 켜야 합니다.
  • 캐시 설정이 원본 쪽 사정에 묶입니다. 로더는 원본에 어떻게 접속하고 무엇을 키로 삼는지 알아야 하기 때문입니다. 원본을 아는 코드를 캐시가 들고 있게 됩니다.
  • 읽기만 필요해도 쓰기 쪽 메서드를 어떻게 할지 정해야 합니다. 읽기와 쓰기를 한 인터페이스로 합쳐 둔 구현에서는 CacheLoaderWriter 가 쓰기와 삭제 메서드까지 요구합니다. 누군가 캐시에 put(K, V) 를 부르면 캐시가 밑에 깔린 시스템 오브 레코드와 어긋날 위험이 있습니다. 쓰기 쪽 메서드를 아무 일도 안 하게 두거나, 변경이 일어나면 예외를 던지게 하거나 둘 중 하나를 골라야 합니다.
  • 원본이 낸 예외를 애플리케이션이 원래 모습으로 받지 못합니다. 애플리케이션은 로더를 직접 부르지 않습니다. 캐싱 구현은 CacheLoader 가 던진 어떤 예외든 CacheLoaderException 으로 감싸야 합니다. 애플리케이션이 보는 것은 캐시 계층이 감싼 예외입니다.

예시

Ehcache 의 CacheLoaderWriter

Java
.withLoaderWriter(new SampleLoaderWriter<>(singletonMap(41L, "zero")))

캐시를 만들 때 로더라이터 한 벌을 붙입니다. 이 인터페이스에서 load(K) 와 loadAll(Iterable<? super K>) 이 read-through 쪽을 맡습니다. 나머지 write(K, V) · writeAll(...) · delete(K) · deleteAll(...) 은 write-through 쪽입니다. 읽기만 쓰더라도 한 인터페이스라 나머지 메서드를 어떻게 할지 정해 두어야 합니다.

JCache 의 setReadThrough

MutableConfiguration 의 setReadThrough 로 켭니다. 그 메서드의 문서는 CacheLoader 팩토리를 지정하지 않고 이 값을 true 로 두는 것은 잘못된 설정이라고 적습니다. 그래서 setCacheLoaderFactory 로 로더 팩토리를 같이 넘겨야 짝이 맞습니다. 켜 두면 Cache.get(K) 이 캐시에 없는 항목을 만났을 때 CacheLoader 를 부릅니다.

Oracle Coherence 의 cachestore-scheme

XML
<distributed-scheme>
   <scheme-name>distributed-rwbm</scheme-name>
   <backing-map-scheme>
      <read-write-backing-map-scheme>
      <internal-cache-scheme>
         <local-scheme/>
      </internal-cache-scheme>
      <cachestore-scheme>
         <class-scheme>
            <class-name>com.example.MyCacheStore</class-name>
               <init-params>
                  <init-param>
                     <param-type>java.lang.String</param-type>
                     <param-value>{cache-name}</param-value>
                  </init-param>
               </init-params>
            </class-scheme>
         </cachestore-scheme>
      </read-write-backing-map-scheme>
   </backing-map-scheme>
</distributed-scheme>

read-write-backing-map-scheme 이 ReadWriteBackingMap 구현을 설정합니다. 문서는 이 백킹 맵이 두 조각으로 이뤄진다고 적습니다. 데이터를 실제로 담는 내부 맵이 internal-cache-scheme 이고, 데이터베이스와 주고받는 캐시 스토어 구현이 cachestore-scheme 입니다. class-name 자리에 로더 구현 클래스를 적습니다. 위 조각은 distributed-rwbm 이라는 이름으로 묶여 있고, 캐시 이름 com.company.dto.* 가 이 스킴에 매핑됩니다.

Hazelcast 의 MapLoader

Hazelcast 는 외부 시스템에 붙는 자리를 MapLoader 와 MapStore 두 이름으로 나눠 둡니다. MapLoader 는 읽어 오기만 하고 MapStore 는 되쓰기까지 합니다. 이 구현을 맵에 걸어 두면 항목이 메모리에 없을 때 Hazelcast 가 그 구현에게 외부 시스템에서 실어 오라고 시킵니다. 같은 자리에서 쓰기를 동기로 내보내면 write-through 이고, 비동기로 내보내면 write-behind 입니다.

실패

로더가 던진 예외는 감싸여 올라옵니다. JCache 명세는 CacheLoaderException 을 CacheLoader 를 실행하다 문제가 생겼음을 알리는 예외로 정의하고, 캐싱 구현이 CacheLoader 가 던진 어떤 예외든 이 예외로 감싸야 한다고 적습니다. load(K) 와 loadAll(Iterable) 의 문서도 로더 실행에 문제가 있으면 이 예외를 던진다고 적습니다.

여러 키를 한꺼번에 실을 때는 조용히 빠지는 쪽입니다. loadAll 문서는 어떤 객체를 실을 수 없으면 그 객체는 결과 맵에 들어가지 않는다고 적습니다. 예외가 아니라 빠진 자리로 나타납니다.

조건 그때 벌어지는 일
로더가 예외를 던진다 캐싱 구현이 그 예외를 CacheLoaderException 으로 감싸 올립니다
loadAll 이 어떤 객체를 못 싣는다 그 객체는 결과 맵에 들어가지 않습니다
get 이 값을 가져오는 도중 문제가 생긴다 CacheException 이 납니다
키가 널이다 NullPointerException 이 납니다
캐시가 이미 닫혀 있다 IllegalStateException 이 납니다
런타임 타입 검사를 켜 두었는데 키나 값의 타입이 안 맞는다 ClassCastException 이 납니다
CacheLoader 팩토리 없이 read-through 를 true 로 둔다 그 자체가 잘못된 설정입니다

같은 키가 동시에 미스 나서 로더가 겹쳐 불릴 수 있는지, 그때 원자성이 어떻게 되는지는 이 항목의 출처들이 적지 않습니다.

운영

켜고 끄는 손잡이가 먼저입니다. JCache 의 read-through 는 기본이 꺼짐입니다. MutableConfiguration 생성자가 isReadThrough 를 false 로 둡니다. setReadThrough(true) 로 켜고, 켤 때 setCacheLoaderFactory 로 로더 팩토리를 같이 지정해야 합니다.

로더를 어느 인터페이스로 구현할지도 정해야 합니다. Coherence 는 셋 중 하나를 구현하라고 적습니다. CacheLoader 는 읽기 전용 캐시용, CacheStore 는 읽기·쓰기 캐시용, BinaryEntryStore 는 바이너리 엔트리 객체의 읽기·쓰기용입니다. 셋 다 com.tangosol.net.cache 패키지에 있습니다. CacheLoader 의 주요 메서드는 load(Object key) 와 loadAll(Collection keys) 둘이고, CacheStore 는 여기에 store · storeAll · erase · eraseAll 을 더합니다. 키 여럿이 한꺼번에 필요한 자리에서는 loadAll 을 구현해 두면 한 번의 호출로 받습니다.

미스 지연을 미리 줄이는 손잡이가 refresh-ahead 입니다. Coherence 는 최근 접근된 캐시 항목을 만료 전에 자동으로 그리고 비동기로 캐시 로더에서 다시 읽어 오도록 설정할 수 있게 합니다. 이 비동기 갱신은 객체가 만료 시각에 충분히 가까울 때 접근되어야만 발동합니다. 만료 시각을 넘긴 뒤에 접근하면 동기 읽기로 떨어집니다. 계수의 이름은 refresh-ahead-factor 이고 문서 예제의 값은 0.5 입니다. 같이 걸리는 만료 설정은 expiry-delay 이고 예제 값은 20s 입니다.

XML
<refresh-ahead-factor>0.5</refresh-ahead-factor>
XML
<local-scheme>
   <scheme-name>categories-eviction</scheme-name>
   <expiry-delay>20s</expiry-delay>
</local-scheme>

두 값이 짝으로 걸립니다. expiry-delay 가 항목의 만료를 정하고, refresh-ahead-factor 가 만료 시각에 얼마나 가까워야 미리 읽기를 발동할지를 정합니다. 문서의 예제는 이 둘을 같은 캐시 설정 안에 나란히 둡니다.

관련 항목

이것을 이 이름으로 정의한 제품·표준

Ehcache · Oracle Coherence · Hazelcast · JCache

이것을 구현하는 데 쓰는 구성 요소·환경

CacheLoader · CacheStore · CacheLoaderWriter · MapLoader · MapStore · BinaryEntryStore · MutableConfiguration · ReadWriteBackingMap · 클러스터

이것과 SoR 접근 방식을 놓고 갈라지는 캐싱 전략

cache-aside · cache-as-SoR · write-through · write-behind · cache-through

이것에서 자주 나는 오류·장애

CacheLoaderException · CacheException · 캐시 스탬피드

이것이 미스를 처리하며 거치는 요소

캐시 미스 · 시스템 오브 레코드 · 데이터베이스 · refresh-ahead · 지연

이것이 속하는 상위 분류

캐싱

다른 이름: read through · 리드 스루 · read-through caching · Read-Through