사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
서버 캐싱
캐시는 값의 복사본을 잠깐 들고 있어서, 서버가 비싼 작업을 건너뛰게 해 줍니다. 인증 코드, 카운터, 요약, 계산해 둔 옵션처럼 잠시 재사용해도 안전한 데이터에 씁니다.
이 페이지에서 쓰는 말
용어설명
캐시 어댑터
캐시 값을 실제로 담는 엔진입니다. 데이터베이스 모드에 따라 SQLite 파일이나 Redis가 맡습니다.
topic
key 앞에 붙는 이름 공간입니다(예:
previewTokens). topic과 key가 합쳐져 값 하나를 가리킵니다.expireAtttl
expireAt은 값이 사라지는 시각(Dayjs)이고, ttl은 같은 기한을 밀리초 단위 수명으로 나타낸 것입니다.레플리카
같은 앱을 실행하는 여러 서버 프로세스 중 하나입니다.
캐시하는 네 가지 방법
Document 캐시
모든 model 클래스에 딸린 key-value 저장소입니다. key가 레코드 id일 때 씁니다.
this.articleCache.set(topic, id, value)서비스 메모리
service가 호출 사이에 들고 있는 값이나 map입니다. 같은 캐시를 쓰는 모든 레플리카가 공유합니다.
memory(String) · memory(Map, { of })로컬 메모리
이 프로세스에만 있는 일반 필드입니다. 가장 빠르지만 공유되지 않고, 재시작하면 사라집니다.
memory(Int, { local: true, default: 0 })Endpoint 캐시
query의 응답 전체를 선언한 밀리초 동안 모든 호출자에게 재사용합니다.
query(T, { guards, cache: 1000 })캐시 값이 저장되는 곳
엔진은 앱이 도는 데이터베이스 모드를 따릅니다.
akan.config.ts의 database.modes에 모드를 선언하고, 배포마다 AKAN_DATABASE_MODE로 그중 하나를 고릅니다. 어느 엔진이든 코드는 같습니다.| 데이터베이스 모드 | 캐시 엔진 |
|---|---|
| ↳ 저장 위치 | |
single (기본값) | SQLite 파일 |
개발 중에는 local/apps/<app>/, 운영에서는 sqlite/에 둡니다. AKAN_SOLID_DB_PATH로 파일을 지정합니다. | |
multiple · cluster | Redis |
REDIS_URI로 정하며 배포에서는 꼭 필요합니다. 개발자 PC에서는 localhost를 씁니다. | |
Document 캐시
into()로 만든 model 클래스에는 모두 this.<model>Cache가 있습니다. 미리보기 토큰이나 인증 코드처럼 캐시 값이 레코드 하나에 속할 때 씁니다.미리보기 토큰을 10분 동안 저장하고, 딱 한 번만 받아 주는 예입니다:
apps/blog/lib/article/article.document.ts
- 용도마다 topic 하나.
previewTokens가 topic, article id가 key이고, 토큰 하나하나가 그 아래 필드이며 필드마다 만료가 따로 걸립니다. model 이름이 앞에 자동으로 붙으므로, 다른 model의 topic과 겹치지 않습니다. - 한 번에 꺼내 씁니다.
hgetDel은 필드를 읽는 일과 지우는 일을 한 번에 하므로, 같은 토큰으로 동시에 들어온 두 요청 중 하나만 통과합니다. 틀린 토큰은 없는 필드를 가리키므로 아무것도 소비하지 않습니다. 읽은 뒤 따로 지우면 동시에 온 두 요청이 모두 통과합니다. - 값은 타입을 그대로 지닙니다. 숫자는 숫자로, 바이트는 바이트로 읽히며 SQLite와 Redis 모두 같습니다.
메서드
메서드설명
set(topic, key, value, { expireAt }?)
문자열, 숫자, boolean, 바이트, 객체를 저장합니다.
expireAt이 없으면 지울 때까지 남습니다.get<T>(topic, key)
저장한 값을 그대로 다시 읽습니다. 없거나 만료된 key는
undefined로 읽힙니다.delete(topic, key)
값을 바로 지웁니다.
getDel<T>(topic, key)
읽기와 삭제를 한 번에 합니다. 한 번만 쓸 값을 두고 다투는 두 호출자 중 하나만 받습니다.
setIfAbsent(topic, key, value, { expireAt }?)
살아 있는 값이 없을 때만 쓰고, 이 호출이 썼는지를 돌려줍니다.
incr(topic, key, by?, { expireAt }?)
by(기본 1)를 더하고 합계를 돌려줍니다. 만료는 이 호출이 값을 처음 만들 때만 걸립니다.hsethgethdelete
key 하나 아래의 해시입니다. 필드마다 따로 쓰고 읽고 지우며, 만료도 필드마다 걸립니다.
hkeyshentrieshclear
필드 이름을 나열하거나, 값과 함께 나열하거나, 해시를 비웁니다.
hgetDelhsetIfAbsenthincr
필드 하나에 대한 한 번에 끝나는
getDel, setIfAbsent, incr입니다.

class 인스턴스는 평범한 JSON으로 돌아옵니다. 객체와 배열은 JSON으로 저장되므로, 다시 읽은 model에는 메서드가 없고 날짜는 문자열입니다. model 값은 model로 타입을 준
memory()를 대신 씁니다.서비스 메모리
memory()는 호출이 끝나도 남는 값을 service에 줍니다. serve()의 주입자에서 service(), plug()와 나란히 선언하며, adapt() 어댑터에서도 쓸 수 있습니다.세 가지 종류를 한 service에 모으면 이렇습니다:
apps/blog/lib/article/article.service.ts
this.x가 어떤 모양이 되는지는 선언 방식에 따라 다릅니다:선언받는 것
memory(ref)
async 메서드로 다루는 공유 값 하나입니다.
getDel, setIfAbsent, incr는 각각 한 번에 끝납니다.memory(Map, { of: ref })
공유되는 async key-value map입니다.
getOrInsert는 레플리카 사이에서도 먼저 쓴 값을 지킵니다.memory(ref, { local: true })
이 프로세스에 있는 일반 필드로, 바로 읽고 대입합니다.
Map이면 진짜 Map입니다.옵션
ofscalar | model class
Map memory의 값 타입입니다. 첫 인자가 Map이면 꼭 필요합니다.localboolean기본값 false
캐시 어댑터 대신 이 프로세스의 일반 필드로 둡니다.
defaultref 타입의 값
단일 값이 처음 쓰이기 전에 읽히는 값입니다. 없으면
null로 읽힙니다.ttlnumber (ms)
쓴 값 하나하나가 살아 있는 시간입니다.
set이 { expireAt }를 직접 주면 그 값이 우선합니다.get(stored) => value
저장된 값을 코드가 읽는 모양으로 바꿉니다.
set과 함께만 주며, local과는 못 씁니다.set(value) => stored
get의 반대로, 코드가 쓰는 값을 저장할 값으로 되돌립니다.규칙
- JSON을 직접 만들지 말고 model을 저장하세요.
memory(Map, { of: cnst.OauthClient })는 constant를 거쳐 직렬화됩니다.Stringmemory에 JSON을 손으로 넣지 마세요. - 첫 쓰기 전에는 빈 값이 나올 수 있습니다. 단일 값은
default를, 없으면null을 읽습니다.Map의get(key)는undefined를 읽으므로(await this.registrations.get(ip)) ?? 0처럼??로 감쌉니다. - memory는 선언한 service의 것입니다. 두 service가 모두
latestArticleId를 선언해도 값은 각자 따로 가지며, 어댑터도 마찬가지입니다. - Map 항목은 각자 만료됩니다. 선언한
ttl이나 쓰기에 준expireAt은 그 항목에만 걸리며, SQLite와 Redis 모두 같습니다. - 로컬 메모리는 프로세스마다 따로입니다. 레플리카마다 자기
localHitCount를 가지며, 재시작하면 처음부터 다시 셉니다.
무엇을 쓸까?
값의 주인이 누구인지, 누가 그 값을 봐야 하는지로 고릅니다.
| 이럴 때 | 보이는 범위 |
|---|---|
| ↳ 사용 | |
| key가 model id입니다. | 같은 캐시를 쓰는 모든 레플리카 |
| this.articleCache | |
| 값이 service 흐름에 속합니다. | 같은 캐시를 쓰는 모든 레플리카 |
| memory(T) · memory(Map, { of }) | |
| 레플리카마다 따로 들고 있어도 됩니다. | 이 프로세스만 |
| memory(T, { local: true }) | |
| query가 모든 호출자에게 같은 답을 줍니다. | 모든 호출자, 인자 조합마다 하나 |
| query(T, { guards, cache: ms }) | |
- 로컬 메모리는 공유할 필요가 없는 값에만 씁니다. 다른 레플리카도 봐야 하는 값은
memory()나 document 캐시에 둡니다. - Endpoint 캐시는 모두에게 같은 답에만 씁니다.
.with(Self)같은 내부 인자가 없는query에서만 동작합니다. 캐시 조회는 guard 다음에 일어나므로, 캐시된 답은 guard를 통과한 호출자에게만 갑니다.
팁
- TTL은 짧게 시작하세요. 동작이 안정되면 그때 늘립니다.
- 만료돼야 하는 값에는 수명을 주세요. memory에
ttl을 선언하거나 쓸 때expireAt을 줍니다. 둘 다 없는 값은 지울 때까지 남습니다. - key는 단순하게. 보통 topic과 id면 충분합니다.
- 원본을 바꾼 직후 캐시를 정리하세요. 원본 데이터가 바뀌면 바로 캐시를 지우거나 갱신합니다.
- 캐시를 원본으로 믿지 마세요. 언제든 사라질 수 있는 빠른 복사본일 뿐입니다.
이어서 볼 문서