만료
고친 사람 github-actions[bot]
만료는 정해 둔 시각이 지난 값을 더는 믿고 쓰지 않게 하는 일입니다. 값을 만들 때 언제까지 유효한지를 함께 적어 둡니다. 그 값을 쓸 때마다 적어 둔 시각과 지금 시각을 견줍니다. 시각이 지난 값을 어떻게 다룰지는 그 값이 무엇이냐에 따라 다릅니다.
쉽고 빠른 이해
만료는 「이 값은 언제까지만 쓴다」를 미리 정해 두는 일입니다. 로그인해서 받은 토큰에 30분을 붙여 두면 30분 뒤부터 그 토큰은 통하지 않습니다.
수명이 없으면 한 번 만든 값이 언제까지고 통합니다. 원본이 바뀌어도 낡은 사본이 계속 읽힙니다. 새어 나간 토큰은 영영 열리는 열쇠가 됩니다.
어떻게 도는가:
- 값을 만들 때 언제까지 쓸 수 있는지를 같이 적어 둡니다
- 그 값을 쓸 때마다 적어 둔 시각과 지금 시각을 견줍니다
- 지난 값은 거절하거나, 원본에 다시 묻거나, 지웁니다
대가는 시계와 몰림입니다. 두 기계의 시계가 어긋나면 같은 값을 두고 판정이 갈립니다. 한쪽이 아직 유효하다고 보는 값을 다른 쪽은 이미 지났다고 봅니다. 자주 읽히는 값이 만료되는 순간에는 원본으로 요청이 몰릴 수 있습니다.
원본이 바뀔 때 알림을 받아 사본을 버리는 방법도 있습니다. 이 무효화는 바뀐 순간을 만료보다 빨리 잡습니다. 만료는 그런 알림을 주고받을 수 없는 사이에서도 값을 더는 못 쓰게 해 줍니다.
상세
도서관에서 빌린 책은 반납일이 지나도 책상에 그대로 있습니다. 카운터에 들고 가 날짜를 보여야 그다음이 정해집니다. 기한을 늘려 받으면 도로 빌린 책이 됩니다.
만료는 값에 수명을 붙여 두는 일입니다. 수명이 다한 값은 더는 유효하게 대하지 않습니다. 로그인해서 받은 토큰에 30분짜리 수명을 붙여 두면, 30분 뒤부터는 그 토큰을 내밀어도 통하지 않습니다.
저장해 둔 값을 언제까지 믿을지를 사람이 그때그때 판단하지 않습니다. 미리 적어 둔 시각이 정합니다. 그래서 만료는 사건이 아니라 시각입니다. 아무 일도 일어나지 않아도 시각이 지나면 그 값은 만료된 것입니다.
수명을 적는 두 표기
수명을 적는 방법은 둘입니다. 끝나는 시각을 못 박거나, 지금부터 얼마 동안인지를 길이로 적습니다.
| 무엇을 적나 | 예 | |
|---|---|---|
| 만료 시각 | 유효가 끝나는 시점 | 3월 1일 09시까지 |
| 수명 길이 | 지금부터 유효한 기간 | 지금부터 600초 |
수명 길이를 흔히 TTL(Time To Live)이라고 합니다. 「살아 있을 시간」이라는 뜻입니다.
길이로 받은 수명을 받는 쪽이 시각으로 바꿔 적어 두기도 합니다. 받은 시각에 길이를 더하면 만료 시각이 됩니다. 그 뒤로는 그 만료 시각과 지금 시각만 견주면 됩니다.
두 표기는 누구의 시계에 기대느냐가 다릅니다. 만료 시각은 값을 만든 기계가 제 시계로 적습니다. 판정은 값을 받은 기계가 제 시계로 합니다. 그래서 두 기계의 시계가 맞아야 판정도 맞습니다.
수명 길이는 받은 기계가 제 시계로만 셉니다. 만든 기계의 시계와 안 맞아도 됩니다. 대신 값이 오는 동안 흐른 시간을 어디에 넣을지가 남습니다.
만료된 때와 없어지는 때
값은 만료되는 순간에 사라지지 않습니다. 만료는 「이 값을 이제 어떻게 대할까」를 바꿀 뿐입니다. 만료된 값이 저장 공간에 남아 있는 동안이 있습니다. 누군가 치워야 비로소 없어집니다.
만료된 값이 언제나 없어지는 길로만 가는 것도 아닙니다. 원본에 물어보니 안 바뀌었으면 수명을 새로 받아 도로 유효해집니다.
stateDiagram-v2
[*] --> 유효: 수명을 붙여 만든다
유효 --> 만료됨: 시각이 지난다
만료됨 --> 유효: 원본에 물어보니 안 바뀌었다
만료됨 --> 없어짐: 치운다
없어짐 --> [*]
그림에서 만료됨과 없어짐은 서로 다른 상태입니다. 둘 사이에 얼마가 걸리는지는 치우는 방식이 정합니다.
축출·무효화와의 차이
만료와 헷갈리는 이웃이 둘 있습니다. 셋을 가르는 것은 무엇이 그 일을 일으키느냐입니다. 축출은 저장 공간이 모자랄 때 값을 밀어냅니다. 그때는 수명이 남은 값도 밀려납니다.
무효화는 원본이 바뀌었다는 알림이 올 때 사본을 버립니다. 그래서 바뀐 순간을 만료보다 빨리 잡습니다. 만료는 공간도 알림도 기다리지 않습니다. 미리 적어 둔 시각만 보고 값을 더는 유효하게 대하지 않습니다.
배경
값을 한 번 만들어 여러 곳에 나눠 두면 원본이 바뀌어도 나눠 둔 값은 그 사실을 모릅니다. 사본을 든 쪽은 낡은 값을 계속 읽습니다. 자격을 증명하는 값도 같습니다. 한 번 새어 나가면 누군가 눈치채고 막을 때까지 계속 통합니다.
이걸 막으려면 누군가 값을 하나하나 찾아 치워야 합니다. 그런데 원본은 사본이 어디에 몇 벌 퍼져 있는지 대개 다 알지 못합니다. 지워야 할 기록도 사람이 챙기지 않으면 계속 쌓입니다.
그래서 값을 만드는 순간에 그 끝을 정해 두는 방법이 필요했습니다. 그 끝나는 시각을 값에 붙여 두는 일을 만료(expiration)라고 합니다. 시각이 지난 값은 더는 유효하게 대하지 않습니다. 끝을 「얼마 동안 살아 있나」로 적으면 TTL 이라는 이름이 붙습니다.
예시
만료 시각을 적는 필드는 분야마다 따로 있습니다. 아래 여섯 실물은 다섯 분야에서 왔습니다. 시각으로 적는 것과 길이로 적는 것이 섞여 있습니다.
괄호 안 대문자 낱말은 표준 문서가 정한 강도입니다. MUST 는 반드시, SHOULD 는 되도록, MAY 와 OPTIONAL 은 골라도 된다는 뜻입니다. NOT 이 붙으면 하지 말라는 쪽입니다.
JWT 의 exp
JWT(JSON Web Token)는 주장을 담아 주고받는 토큰입니다. 토큰에 담긴 주장 하나하나를 클레임이라고 합니다. exp 클레임에는 만료 시각을 적습니다. 그 시각과 같거나 그 뒤에는 이 토큰을 처리용으로 받아들이면 안 됩니다(MUST NOT).
처리하려면 지금 시각이 exp 보다 앞이어야 합니다. 값은 NumericDate 라는 숫자입니다. 1970-01-01T00:00:00Z UTC(Coordinated Universal Time, 협정 세계시)부터 그 시각까지 센 초 수입니다. 윤초는 셈에 넣지 않습니다.
exp 를 넣을지 말지는 고를 수 있습니다(OPTIONAL).
HTTP 응답의 max-age 와 Expires
HTTP(HyperText Transfer Protocol) 응답은 수명을 길이로도, 시각으로도 적을 수 있습니다. 먼저 알아 둘 낱말은 응답의 나이(age)입니다. 원본 서버가 그 응답을 만든 뒤로, 또는 원본 서버에 물어 그 응답이 아직 맞다고 확인받은 뒤로 흐른 시간입니다.
Cache-Control: max-age=600, stale-while-revalidate=30
max-age=600 은 길이입니다. 이 응답의 나이가 600초를 넘으면 낡은(stale) 응답으로 봅니다. 뒤의 stale-while-revalidate=30 은 낡은 뒤에도 30초까지는 더 내줘도 된다는 허락입니다.
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Expires 는 시각입니다. 이 시각이 지나면 응답을 낡은 것으로 봅니다. 이 시각이 원본이 그때 바뀐다거나 사라진다는 뜻은 아닙니다. 날짜 형식이 틀렸거나 값이 0 이면 받는 캐시는 이미 지난 시각으로 해석해야 합니다(MUST).
한 응답에 둘이 같이 오면 max-age 가 이깁니다. 받는 쪽은 Expires 를 무시해야 합니다(MUST).
X.509 인증서의 notBefore 와 notAfter
X.509 인증서는 서버 같은 상대가 누구인지 증명하는 전자 문서입니다. 인증 기관이 서명해서 발급합니다.
인증서는 시각을 둘 적습니다. 유효 기간이 시작하는 notBefore 와 끝나는 notAfter 입니다. 두 시각을 모두 포함한 그 사이가 유효 기간입니다.
유효 기간은 발급한 인증 기관이 그 인증서의 상태 정보를 유지하겠다고 보증하는 기간입니다. 상태 정보는 그 인증서가 폐기되지 않았는지처럼 지금도 믿어도 되는지를 알려 주는 정보입니다. 끝을 정하지 않으려는 인증서는 notAfter 에 99991231235959Z 를 넣기를 권합니다(SHOULD). 9999년 12월 31일 23시 59분 59초입니다.
DNS 레코드의 TTL
DNS(Domain Name System)는 도메인 이름을 주소 같은 정보로 풀어 주는 체계입니다. 그 답 한 줄이 자원 레코드입니다. 자원 레코드마다 TTL 칸이 있습니다. TTL 은 초 단위의 32비트 정수입니다.
TTL 은 이 레코드를 캐시해 둬도 되는 시간입니다. 그 시간이 지나면 버리고 정보의 출처에 다시 물어야 합니다. TTL 이 0 이면 지금 진행 중인 한 번의 처리에만 쓰고 캐시하지 않습니다.
캐시에 넣을 때는 남은 길이를 절대 시각으로 바꿔 적어 두기를 권합니다. 답으로 내보낼 때는 다시 남은 길이로 되돌립니다. 되돌린 값이 음수면 그 레코드는 만료된 것이라 무시합니다.
쿠키의 Max-Age 와 Expires
쿠키는 브라우저가 사이트 대신 들고 있다가 요청마다 실어 보내는 작은 값입니다. Expires 속성은 쿠키가 만료되는 시각을 적습니다. Max-Age 속성은 만료까지 남은 초를 적습니다.
둘 다 있으면 Max-Age 가 이깁니다. 둘 다 없으면 브라우저는 「지금 세션이 끝날 때」까지 쿠키를 들고 있습니다. 세션이 언제 끝나는지는 브라우저가 정합니다.
Max-Age 는 받는 순간 시각으로 바뀝니다. 지금 시각에 그 초를 더한 것이 만료 시각입니다. 값이 0 이하이면 나타낼 수 있는 가장 이른 시각이 만료 시각이 됩니다.
Redis 의 EXPIRE
Redis 는 키에 수명을 거는 EXPIRE 명령을 따로 둡니다. 대괄호 안은 골라 붙이는 인자입니다.
EXPIRE key seconds [NX | XX | GT | LT]
시간이 다 되면 그 키는 저절로 지워집니다. 수명이 걸린 키를 Redis 에서는 휘발성(volatile) 키라고 부르기도 합니다. 이미 수명이 걸린 키에 다시 EXPIRE 를 부르면 남은 수명이 새 값으로 바뀝니다.
수명은 초 단위 길이로 줍니다. Redis 안에는 그 수명이 1970년부터 센 유닉스 타임스탬프, 곧 절대 시각으로 적힙니다. 2.6 판부터는 그 단위가 밀리초입니다. 절대 시각으로 적었기 때문에 Redis 가 꺼져 있는 동안에도 수명은 줄어듭니다.
여섯을 한 표로
앞의 여섯을 필드 · 적는 방식 · 지나면 어떻게 되나로 견줍니다.
| 대상 | 필드 | 적는 방식 | 지나면 |
|---|---|---|---|
| JWT | exp |
시각 · 1970년부터 센 초 | 받지 않는다 |
| HTTP 응답 | max-age · Expires |
길이 · 시각 | 낡은 응답이 된다 |
| X.509 인증서 | notBefore · notAfter |
시각 둘 | 유효 기간 밖이 된다 |
| DNS 자원 레코드 | TTL | 길이 · 초 | 버리고 출처에 다시 묻는다 |
| 쿠키 | Max-Age · Expires |
길이 · 시각 | 저장소에서 내보낸다 |
| Redis 키 | EXPIRE |
길이 · 초 | 지운다 |
갈래
만료가 정하는 것은 「시각이 지났다」는 판정뿐입니다. 지난 값을 어떻게 대할지는 그 값이 무엇이냐가 정합니다. 갈래는 셋입니다.
| 갈래 | 값의 성격 | 만료되면 | 왜 |
|---|---|---|---|
| 거절 | 자격 증명 | 받지 않는다 | 수명 자체가 안전장치다 |
| 재검증 | 캐시 사본 | 원본에 다시 묻는다 | 수명이 다해도 내용까지 틀린 것은 아니다 |
| 삭제 | 저장해 둔 키 · 기록 | 지운다 | 안 쓰는 것으로 끝나지 않는다. 공간을 비워야 한다 |
거절
자격을 증명하는 값은 만료되면 받지 않습니다. 만료 시각과 같거나 그 뒤의 JWT 는 처리용으로 받아들이면 안 됩니다(MUST NOT). X.509 인증서도 같습니다. 인증서를 검증하는 단계는 지금 시각이 그 인증서의 유효 기간 안에 들어야 다음으로 넘어갑니다.
이 값들은 수명이 곧 안전장치입니다. 지난 값을 들고 온 쪽은 대개 새 값을 받아 다시 와야 합니다.
재검증
캐시가 든 사본은 수명이 다해도 내용까지 틀린 것은 아닙니다. HTTP 캐시는 원본 서버가 응답을 만든 때부터 만료 시각까지의 길이를 신선도 수명(freshness lifetime)이라 합니다. 나이가 이 길이를 넘지 않은 신선한(fresh) 응답은 원본에 연락하지 않고 바로 내줄 수 있습니다. 넘은 응답은 낡은 응답이라 원칙적으로 내주면 안 됩니다(MUST NOT).
내줄 수 있는 사본이 없으면 캐시는 조건부 요청으로 원본에 「이 사본 아직 맞나」를 물을 수 있습니다. 이렇게 물어 확인하는 일이 재검증입니다. 원본이 「안 바뀌었다」는 짧은 답인 304 Not Modified 를 보내면, 캐시는 그 답에 실린 헤더로 저장해 둔 응답의 헤더를 갱신해야 합니다(MUST). 그 사본은 다시 쓰입니다.
sequenceDiagram
participant 클라이언트
participant 캐시
participant 원본
클라이언트->>캐시: 요청
Note over 캐시: 신선하면 원본에 묻지 않고 여기서 내준다
Note over 캐시: 든 사본이 낡았다
캐시->>원본: 조건부 요청 · 이 사본 아직 맞나
Note over 캐시,원본: 바뀌었으면 원본이 새 응답을 보낸다
원본-->>캐시: 304 Not Modified
Note over 캐시: 저장해 둔 응답의 헤더를 갱신한다
캐시-->>클라이언트: 그 사본을 내준다
삭제
키를 담는 저장소와 오래 쌓이는 기록은 만료되면 지웁니다. 다만 만료된 순간과 지워지는 순간 사이에는 틈이 있습니다. Redis 는 키를 지우는 두 방식을 같이 씁니다.
| 방식 | 언제 지우나 | 남는 틈 |
|---|---|---|
| 읽을 때 거르기 | 클라이언트가 그 키를 읽으러 왔을 때 시간이 지났으면 | 아무도 다시 안 읽는 키는 이것만으로는 안 지워진다 |
| 표본을 훑어 지우기 | 주기적으로 수명이 걸린 키 몇 개를 무작위로 골라 지난 것을 | 표본에 뽑히기 전까지 남는다 |
쿠키도 만료되면 지웁니다. 브라우저는 만료된 쿠키를 쿠키 저장소에서 내보내야 합니다(MUST).
Amazon S3(Simple Storage Service)는 수명 주기 설정에 만료 동작을 넣어 객체를 지우게 할 수 있습니다. 만료 때 S3 가 하는 일은 객체를 담은 버킷이 버전 관리를 켰는지에 따라 다릅니다. 켜지 않은 버킷이면 만료된 객체를 지울 줄에 올렸다가 비동기로 영구히 지웁니다. 그래서 만료일과 지워지는 날 사이에 지연이 있을 수 있습니다.
대가
만료를 쓰기로 하면 넷을 떠안습니다. 시계 어긋남 · 전달 지연 · 몰림 · 수명 길이입니다.
시계 어긋남
만료 시각과 지금 시각을 견주는 판정은 벽시계를 읽습니다. 벽시계는 사람이 보는 날짜와 시각을 알려 주는 시계입니다. 기계마다 이 시계가 조금씩 어긋납니다. 이 어긋남이 클럭 스큐(clock skew)입니다.
그래서 발급한 기계에서는 아직 남은 토큰이 받는 기계에서는 이미 지난 토큰이 될 수 있습니다. JWT 를 처리하는 쪽은 클럭 스큐를 메우려고 작은 여유를 둘 수 있습니다(MAY). 그 여유는 대개 몇 분을 넘지 않습니다. 여유를 둔 만큼 만료 시각이 지난 토큰도 그동안 통합니다.
전달 지연
수명을 길이로 주면 두 기계의 시계를 맞출 필요는 줄어듭니다. 대신 값이 오는 데 걸린 시간이 문제가 됩니다. 받은 순간부터 길이를 세기 시작하면 오는 동안 흐른 시간만큼 수명이 늘어납니다.
응답에는 나이가 실려 옵니다. 나이는 원본에서 오는 길의 캐시마다 머문 시간에 네트워크를 지나온 시간을 더한 값입니다. 실려 온 나이는 응답이 떠나기 전에 적힌 값입니다. 떠난 뒤 이 캐시에 닿기까지 흐른 시간은 거기에 빠져 있습니다.
HTTP 캐시는 그 빠진 시간을 응답의 나이에 더해 보정합니다. 나이가 그만큼 커집니다. 그래서 신선한 채로 남는 시간은 그만큼 줄어듭니다.
캐시는 요청을 보낸 시각과 응답을 받은 시각의 차이를 응답 지연으로 잡습니다.
response_delay = response_time - request_time
corrected_age_value = age_value + response_delay
request_time 은 요청을 보낸 때의 시계 값입니다. response_time 은 응답을 받은 때의 시계 값입니다. age_value 는 응답에 실려 온 나이입니다. corrected_age_value 는 거기에 응답 지연을 더한 보정한 나이입니다.
보정한 나이는 요청을 보낸 시각을 기준으로 읽는 값입니다. 캐시는 이 값을 응답을 받은 시각이 아니라 요청을 보낸 시각에 견주어 해석해야 합니다(MUST). 나이는 응답 지연만큼 커지고, 기준으로 삼는 시각은 그만큼 앞으로 갑니다. 그래서 응답 지연은 셈에 한 번만 들어갑니다.
sequenceDiagram
participant 캐시
participant 원본
Note over 캐시: request_time · 요청을 보낸 시각
캐시->>원본: 요청
Note over 캐시,원본: 이 사이에 흐른 시간이 응답 지연
원본-->>캐시: 응답 · 실려 온 나이 age_value
Note over 캐시: response_time · 응답을 받은 시각
Note over 캐시: 보정한 나이 = age_value + 응답 지연
Note over 캐시: 이 나이는 요청을 보낸 시각부터 센 것으로 읽는다
몰림
자주 읽히는 값 하나가 만료되면, 그 값을 찾던 여러 요청이 한꺼번에 캐시에서 값을 못 찾을 수 있습니다. 그러면 그 요청들이 같은 값을 동시에 다시 만들러 갑니다. 이 현상이 캐시 스탬피드(cache stampede)입니다. 데이터베이스와 웹 서버의 성능을 크게 떨어뜨립니다.
몰리는 요청 수는 요청이 들어오는 속도와 값을 다시 만드는 데 걸리는 시간에 함께 달려 있습니다. 1초에 10번 읽히는 값을 다시 만드는 데 3초가 걸리면 30개 요청이 같은 값을 다시 만듭니다.
sequenceDiagram
participant 요청들
participant 캐시
participant 원본
Note over 캐시: 자주 읽히는 값이 만료된다
요청들->>캐시: 값을 찾는다
캐시-->>요청들: 없다
요청들->>원본: 같은 값을 동시에 다시 만든다
Note over 요청들,원본: 1초에 10번 × 다시 만드는 3초 = 30개 요청
대응 하나는 만료 시각 전에 미리 다시 만드는 것입니다. 요청을 처리하는 쪽이 만료 전에 무작위로 재생성을 청하게 합니다. 이 방식이 확률적 조기 만료입니다.
수명 길이
수명을 짧게 잡으면 원본에 다시 묻는 일이 잦아집니다. 길게 잡으면 낡은 값이 오래 읽힙니다. 새어 나간 토큰도 그만큼 오래 통합니다. 어느 쪽으로 잡아도 하나는 나빠집니다.
만료된 값을 조금 더 내주는 선택도 있습니다. HTTP 의 stale-while-revalidate 는 응답이 낡은 뒤에도 정한 초만큼 캐시가 그 응답을 내줘도 된다고 허락합니다(MAY). 그동안 캐시는 요청을 붙잡아 두지 않은 채 재검증을 시도해야 합니다(SHOULD). stale-if-error 는 오류를 만났을 때 낡은 응답을 써도 되는 상한을 정합니다.
대가는 낡은 값이 나가는 시간이 그만큼 길어진다는 것입니다. max-age=600, stale-while-revalidate=30 은 600초 동안 신선합니다. 그 뒤 30초까지는 낡은 채로 더 나갈 수 있습니다.
stateDiagram-v2
state "신선 · 바로 내준다" as 신선
state "낡았지만 내줘도 된다 · 내주며 재검증을 시도한다" as 유예
state "낡음 · 원칙적으로 재검증 없이 못 내준다" as 낡음
[*] --> 신선: 응답을 받는다
신선 --> 유예: 600초가 지난다
유예 --> 낡음: 30초가 더 지난다
대개 서버는 두 값의 합을 견딜 수 있는 가장 긴 시간에 맞추고 싶어 합니다. 둘을 다 600으로 잡으면 응답이 캐시에서 최대 20분까지 나가는 것을 견딜 수 있어야 합니다.
관련 항목
수명을 적어 두는 표기와 헤더
TTL · 만료 시각 · Cache-Control · max-age · Expires · s-maxage · 나이 · NumericDate · 유닉스 타임스탬프 · UTC · 윤초
수명이 다한 값을 대하는 처리
재검증 · 검증 · 조건부 요청 · 304 Not Modified · 무효화 · 축출 · 낡은 데이터 · 유예 기간 · 소프트 삭제
만료가 걸리는 캐시 계층
캐싱 · HTTP 캐시 · 신선도 · 휴리스틱 신선도 · stale-while-revalidate · stale-if-error · DNS · 자원 레코드
만료 순간에 몰리는 요청과 그 대응
캐시 스탬피드 · 썬더링 허드 · 확률적 조기 만료 · 지터
수명이 붙는 자격 증명
토큰 · 액세스 토큰 · 리프레시 토큰 · JWT · 세션 · 쿠키 · 인증서 · X.509 · 인증 기관 · 폐기 목록
만료된 키와 객체를 지우는 저장소
Redis · Amazon S3 · 수명 주기 규칙 · 휘발성 키
기간이 차면 데이터를 지우는 운영 절차
보존 기간 · 로그 로테이션 · 아카이빙 · 가비지 컬렉션 · 데이터 파기
만료 판정이 딛는 시계와 시간 장치
다른 이름: expiration · expiry