~12 min · policy, consistency, judgment, governance
Level 0흩어진 부품
0 XP0/48 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
앱 둘, 자세 둘, 결정 0
저장 시 암호화는 원래 형제 셋 중 둘에 있었고, 그 둘이 서로 안 맞았어. 하나는 키가 없으면 아예 안 시작했어. 시작 때 죽는 관문. 조용히 평문을 쓰는 게 안 도는 것보다 나쁘다는 근거로. 다른 하나는 조용히 낮춰 돌았지. 키 없으면 평문 쓰고 경고 남기고. 세 번째는 암호화가 아예 없었고, 공용 층을 채택하면서 처음 갖게 됐어.
어느 자세도 틀리지 않았어. 틀린 건 그 차이가 한 번도 결정된 적이 없다는 거야. 실은 질문 하나인 것에 대해 저자 둘이 국소적으로 판단했고, 그 결과 생긴 갈라짐은 멀리서 보면 아키텍처 같고 가까이서 보면 안 따져본 의견 둘이었어.
통일하려면 누가 골라야 했고, 고른다는 건 구체적인 것에 답한다는 뜻이었어. 완전히 준비 안 된 기계를 쓰는 사람한테 뭐가 벌어져야 해? 나온 답은 어디서나 부드럽게였어. 키 없는 기계는 평문을 쓰고 경고 한 줄을 남기고, 다른 기계에서 암호화된 줄은 못 읽는 채로 그냥 지나가. 죽는 관문은 퇴역했고.
왜 그 답이고, 왜 그게 판단인가
근거는 암호학이 아니라 사람에 대한 거야. 그리고 암호화를 처음 갖게 되는 앱까지 덮어야 했어. 준비된 적 없는 기계를 만날 확률이 제일 높은 게 그 앱이니까. 이 앱들은 여러 기계에 걸쳐 돌고, 준비 상태가 고르지 않아. 새 기계, 복구된 기계, 아직 키가 안 깔린 동료 기계. 죽는 관문은 그 각각을 멈춘 애플리케이션으로 만들고, 그 앞의 사람은 보통 새 비밀을 쓰려던 게 아니라 어제 메모를 읽으려던 거야.
반대로 정해도 충분히 합리적이야. 합리적일 수 없는 건 그걸 두 번, 다르게, 같은 결정이라는 걸 못 알아챈 채로 정하는 거야. 그리고 아무도 그 질문을 안 갖고 있어서 세 번째 앱한텐 답이 아예 없었던 것도.
구현을 통일하면 각자 기본값으로 내려지던 결정을 강제로 하게 돼. 그리고 아예 답이 없던 자리에도 답을 내게 하고. 그게 통합의 진짜 이득 중 하나고 거의 안 언급돼. 추출은 중복 코드를 없애기만 하는 게 아니라, 그 복사본들이 각자 답하고 있던 질문 전부를 수면으로 올려. 거기에 예산을 잡아. 기술 작업은 보통 그게 드러내는 결정 집합보다 작아.
나머지 정책들, 그리고 그것들이 공유하는 모양
세 주 뒤에 세 개가 한꺼번에 더 나왔고, 서로 운을 맞춰. 공개 범위 어휘 하나, 어디서나. 앱 하나가 같은 상태에 다른 단어를 쓰고 있었고, 접을 때 도는 정규화가 옛 기록을 계속 투영해주니까 둘 다 계속 읽혀. 미디어 정책 하나. 기존 첨부 먼저, 새 것 그다음, 상한 있음, 그리고 없는 파일은 요청 전체를 깨뜨리는 대신 경고하고 건너뜀. 감정 목록 하나, 주인한테서 가져오고 공용 대체값이 있어서 아무도 다른 목록을 손으로 안 베낌.
각각이 앱들이 정하지도 않은 채 갈라져 있던 자리를 대체했어. 그리고 각각이 같은 구조야. 기술적으로 보이는 질문, 실은 사람한테 뭐가 벌어져야 하냐에 대한 답, 그리고 다음 앱이 질문 말고 답을 물려받게 하는 적힌 기록.
Code
퇴역한 관문, 그리고 그 자리를 대신한 자세·python
# BEFORE - one app, fail-fast. Defensible reasoning: silently
# writing plaintext is worse than not running at all.
def require_enabled() -> None:
if not _key_available():
raise RuntimeError(
"encryption key missing; refusing to start"
)
# ...called at startup. A machine without the key has a stopped app.
# AFTER - the family posture, chosen deliberately: graceful
# everywhere. The question this answers is not cryptographic, it is
# "what should happen to somebody whose machine is not fully
# provisioned yet" - and the answer is that they can still read
# yesterday's notes.
class Veil:
def __init__(self, service: str, account: str, env_var: str):
self._key = _load_key(service, account, env_var)
if self._key is None:
log.warning(
"no encryption key for %s/%s - writing plaintext. "
"Lines encrypted on another machine will pass "
"through unreadable.", service, account,
)
def encrypt_line(self, text: str) -> str:
return _encrypt(text, self._key) if self._key else text
def maybe_decrypt_line(self, line: str) -> str:
"""Opaque pass-through, never an exception: a line this
machine cannot read still belongs in the file, and a fold
that raises on it would take the whole record down."""
if not self._key or not _looks_encrypted(line):
return line
try:
return _decrypt(line, self._key)
except Exception:
return line
# What the unification COST, and the part worth budgeting for: it
# forced a decision that two authors had each been making by default
# - and produced one for the third app, which had never had to make
# it. The code change was small. Choosing the family's posture toward
# a half-provisioned machine was the actual work.
네 서비스 둘 이상에 구현돼 있으면서 에러 자세가 다른 동작을 하나 찾아. 하나는 재시도하고 하나는 죽거나, 하나는 기본값을 쓰고 하나는 예외를 던지거나. 그 차이가 어떤 질문에 답하고 있는지, 그리고 누가 그걸 결정한 적이 있는지 적어. 그다음 둘 다에 대한 답 하나를 제안하고 누가 동의해야 하는지 적어.
Hint
에러 자세가 제일 소득이 좋은 자리야. 거의 명세되는 일이 없고, 첫 판본을 쓴 사람이 암묵적으로 정하는 게 보통이거든. 그 차이들은 아무도 만든 걸 기억 못 하는 진짜 결정이고, 그래서 수면으로 올릴 값이 있어.
Progress
Progress is local-only — sign in to sync across devices.