재시작 없이 운영 설정 바꾸기 — 세 서비스가 고른 세 가지 캐시 무효화
.env에만 있던 AI 키·모델·외부 연동 설정을 관리 화면에서 바꾸고 재시작 없이 반영하게 했습니다. 값을 DB로 옮기는 것보다 「다른 워커에게 바뀌었다고 알리는 법」이 어려웠고, 세 서비스는 각자 이미 가진 공용 자리에 따라 세 가지 방법을 골랐습니다.

재시작 없이 운영 설정 바꾸기
세 서비스가 고른 세 가지 캐시 무효화
배경 — 키 하나 바꾸는 데 서버에 들어가야 했습니다
샌드코프 서비스들의 운영 설정은 지금까지 서버의 .env 파일에만 있었습니다. AI API 키, 쓸 모델 이름, 기능을 켜고 끄는 스위치, 알림을 보낼 웹훅 주소 같은 값입니다. 하나를 바꾸려면 서버에 들어가 파일을 고치고, 그 값을 쓰는 컨테이너를 모두 다시 만들어야 했습니다. 키를 .env에 넣고도 컨테이너를 다시 만들지 않아, 떠 있는 서비스가 옛 키로 호출하는 일도 실제로 있었습니다.
그래서 2026년 11월 5일, 세 서비스에 같은 기능을 넣었습니다. 관리 화면에 「운영 설정」을 두고, 거기서 값을 바꾸면 재시작 없이 모든 프로세스가 새 값을 쓰게 하는 것입니다. 대상은 AI 뉴스 서비스 Foresight, 이 홈페이지(sandcorp.co.kr), 코딩 과제 AI 채점 서비스 Codemit입니다.
요구는 하나였지만 세 서비스가 고른 구현은 셋 다 달랐습니다. 이 글은 왜 갈렸는지, 그리고 셋이 공통으로 지킨 것이 무엇인지를 적습니다.
무엇이 문제였나
값을 DB로 옮기는 것은 쉬운 절반입니다
「DB에 값이 있으면 DB, 없으면 .env」로 읽는 함수 하나를 만들고, 설정을 읽던 곳을 모두 그 함수로 바꾸면 됩니다. 배포 직후에는 DB가 비어 있으니 동작이 그대로라는 장점도 있습니다.
어려운 절반은 「다른 프로세스에게 알리기」입니다
설정은 자주 읽힙니다. 요청마다, 배경 작업이 돌 때마다 DB를 치기는 아까워 보통 캐시에 담습니다. 그런데 Django의 기본 캐시는 프로세스 메모리에 있습니다. 웹 워커가 둘이고 스케줄러가 하나면 캐시도 셋입니다. 관리 화면에서 저장한 워커는 자기 캐시를 비우지만, 나머지 프로세스는 저장이 있었다는 사실을 알 길이 없습니다. 같은 요청을 몇 번 보내면 새 값과 옛 값이 번갈아 나옵니다.
화면에 올리면 안 되는 값도 있습니다
모든 설정을 화면으로 옮기면 편하지만, 어떤 값은 잘못 넣는 순간 관리 화면 자체가 열리지 않습니다. DB 접속 정보가 그렇고, 로그인을 검증하는 서명 키가 그렇습니다. 프로세스가 뜰 때 한 번 읽고 굳는 값(스케줄러 주기, 워커 수)은 화면에서 바꿔도 재시작 전에는 효과가 없어 오히려 헷갈리게 합니다.
세 서비스가 같이 지킨 것
구현은 달라도 바깥에서 보이는 규칙은 맞췄습니다.
- DB가 먼저, 없으면
.env. 배포 직후에는 DB가 비어 있어 동작이 바뀌지 않습니다. 「지우기」를 누르면.env값으로 돌아갑니다. 운영 배포 뒤 세 서비스 모두 새 표가 비어 있고 값이.env와 같다는 것을 확인했습니다. - 허용 목록(등록부). 코드에 고정한 이름만 저장할 수 있고, 이름마다 형식 검사(URL은 http(s), 모델은 가격표에 있는 것만 등)가 붙습니다.
- 비밀은 쓰기 전용. 저장된 비밀은 다시 보여 주지 않고 「설정됨 · 끝 4자리 · 마지막 변경 시각·변경자」만 보입니다. 빈 칸으로 저장하면 기존 값을 유지하고, 지우기는 따로 확인을 거칩니다. DB에는 암호문만 저장합니다.
- 값 없는 이력. 누가 언제 어느 이름을 바꿨는지만 남기고 값은 남기지 않습니다.
- 연결 시험 버튼. AI 키는 모델 목록 조회 같은 가벼운 호출 하나로 확인하고, 웹훅은 버튼을 누를 때만 짧은 메시지 하나를 보냅니다. 결과는 HTTP 상태만 보여 줍니다.
고른 방법 — 「이미 함께 보는 곳」에 신호를 둔다
세 서비스 모두 새 인프라를 들이지 않았습니다. 대신 프로세스들이 이미 함께 보고 있는 곳에 「바뀌었다」는 신호를 하나 두었습니다. 그곳이 서비스마다 달라서 방법이 갈렸습니다.
Foresight — DB 버전 행 한 줄
Foresight에는 공용 캐시가 없습니다. 웹 워커 둘과 스케줄러가 함께 보는 것은 DB뿐입니다. 그래서 DB에 버전 번호 한 줄을 두었습니다.
- 저장·지우기는 같은 트랜잭션에서 버전을
F() + 1로 올리고, 자기 프로세스 캐시는 바로 비웁니다. - 읽는 쪽은 값 전체가 아니라 버전 한 줄만 1초 간격으로 확인하고, 바뀌었을 때만 전체를 다시 읽습니다.
- 다른 프로세스에는 최대 1초 안에 반영되고, 비용은 1초에 기본 키 조회 한 번입니다. DB를 못 읽으면 마지막 값, 그것도 없으면
.env로 돕니다.
홈페이지 — 공용 DB 캐시에 스냅샷 통째로
이 홈페이지는 웹 워커들과 배경 작업 컨테이너가 함께 쓰는 DB 캐시 표를 이미 쓰고 있었습니다. 그래서 신호 대신 덮어쓴 설정 전체를 스냅샷 하나로 그 캐시에 둡니다.
- 저장·지우기가 커밋된 직후(
transaction.on_commit) DB에서 스냅샷을 다시 만들어 씁니다. - 스냅샷이 사라졌으면 읽는 쪽이 만들어 넣되
cache.add를 써서, 그사이 저장으로 생긴 새 스냅샷을 덮지 않습니다. - 요청 하나 안에서는 한 번만 읽어 재사용합니다. 프로세스 메모리에 따로 쥐는 시간이 없어 반영 지연이 0입니다. 비용은 요청마다 캐시 조회 한 번입니다.
- 비밀은 스냅샷 안에서도 암호문입니다.
Codemit — Redis 버전 번호와 두 겹의 안전장치
Codemit은 세션 저장에 Redis를 이미 씁니다. 그래서 Redis에 버전 번호 하나를 두고, 값은 각 프로세스 메모리에 둡니다.
- 읽을 때 버전만 보고 같으면 메모리 값, 다르면 DB를 다시 읽습니다. 저장은 커밋 뒤 버전을
INCR합니다. - Redis가 죽으면 30초 동안은 쥔 값을 쓰고 그 뒤 DB를 읽습니다. 그 30초 동안 Redis를 다시 두드리지 않고, 연결 대기도 0.3초로 끊습니다.
- Redis가 멀쩡해도 60초 상한으로 DB를 다시 읽습니다. 버전을 올리는 명령이 유실되거나 Redis가 비워진 경우를 덮기 위해서입니다. 버전을 못 올려도 저장 자체는 성공합니다.
Codemit은 버린 대안을 기록에 남겼습니다.
Codemit이 버린 대안
| 대안 | 버린 이유 |
|---|---|
| 기본 캐시(LocMem)에 그대로 | 워커마다 갈려 이 글의 출발점 문제가 그대로 남습니다 |
| Redis에 값 자체를 캐시 | 설정에 섞인 비밀이 Redis에 평문으로 앉습니다 |
| pub/sub로 변경 알림 | 프로세스마다 구독 스레드가 필요하고, 끊긴 동안 온 신호는 유실됩니다 |
| TTL만 짧게 | 즉시 반영과 DB 부하 가운데 하나를 포기해야 합니다 |
- 버전 번호 방식은 값은 각자 쥐고 「바뀌었다」만 공유하므로 비밀이 공용 캐시에 앉지 않습니다.
부딪힌 문제와 해결
모듈을 불러올 때 굳는 값
증상 — 설정을 읽는 함수를 바꿔도, 어떤 값은 모듈을 처음 불러올 때 전역 변수에 담겨 그 뒤로 다시 읽히지 않습니다. 화면에서 바꿔도 효과가 없는 설정이 생깁니다.
원인 — API_KEY = settings.X처럼 모듈 맨 위에서 값을 꺼내 두거나, 키를 쥔 클라이언트를 전역으로 한 번 만들어 두는 코드 때문입니다.
해결 — 옮길 설정마다 읽는 곳을 전부 찾아 호출할 때 읽도록 바꿨습니다. AI 클라이언트는 원래 호출마다 새로 만들고 있어서 키만 새 함수에서 읽게 하면 됐습니다. 그래도 읽는 곳을 고칠 수 없는 값(스케줄러 주기)은 목록에서 빼고 이유를 적었습니다.
비밀이 새는 뜻밖의 길
증상 — 화면에는 끝 4자리만 보여도, 비밀이 새는 길은 화면 말고도 많습니다.
원인 — 검증 실패 응답(422)이 받은 입력을 그대로 되싣는 경우, 예외 메시지에 값이 들어가는 경우, 객체의 repr, 변경 이력, 로그가 모두 길입니다.
해결 — 검증 오류 문구를 원문 없이 만들고, 로그에는 이름만 남기게 했습니다. 시험은 가짜 키 문자열을 하나 정해 두고 그것이 응답 HTML·로그 캡처·이력·repr 어디에도 없는지 확인합니다.
주소를 바꿨는데 옛 응답이 나옵니다
증상 — 홈페이지는 외부 API(Foresight 통계 등)의 응답을 캐시합니다. 화면에서 API 주소를 바꿔도 캐시가 만료될 때까지 옛 주소의 응답이 나올 수 있습니다.
해결 — 응답 캐시 키에 주소의 해시를 넣었습니다. 주소가 바뀌면 키가 달라져 다음 요청이 바로 새 주소를 부릅니다.
비밀을 무엇으로 암호화할까
세 서비스의 답이 또 갈렸습니다. Foresight는 설정 전용 암호화 키를 새로 두고, 그 키가 없으면 비밀 저장을 거절합니다(일반 설정은 저장됩니다). 홈페이지는 새 키를 만들지 않고 앱 비밀에서 용도 라벨을 붙여 키를 파생합니다. 앞서 관리자용 접근 코드 보기를 만들 때 쓴 방식을 용도 라벨만 바꿔 다시 썼습니다. Codemit은 이미 있던 암호화 키를 그대로 씁니다. 어느 쪽이든 복호화에 실패하면 그 값은 없는 것으로 보고 .env로 돌아가며, 화면에는 「다시 넣어야 함」을 띄웁니다.
결과
- 세 서비스 모두 2026년 11월 5일에 운영에 나갔습니다. 배포 직후 새 표는 비어 있고, 모든 값이
.env와 같다는 것을 운영에서 확인했습니다. - Foresight: AI 키 하나와 수집·편집 설정을 화면으로 옮겼습니다. 저장하면 웹 워커와 스케줄러가 1초 안에 새 값을 씁니다. 시험은 새로 24건, 「설정에서 AI 판별을 끄면 다음 수집부터 판별을 부르지 않는다」 1건을 더했습니다.
- 홈페이지: 웹훅 비밀 둘과 외부 연동 주소·검색엔진 인증값 등 일반 설정 열 개를 옮겼습니다. 시험 21건입니다.
- Codemit: 공모전 AI 키와 모델·온도 설정을 운영자 화면으로 옮겼습니다. 백엔드 시험 25건, 화면 단위 시험, 브라우저 시험 2건이 붙었습니다.
- 이제 키를 바꿀 때 서버에 들어가지 않습니다. 바꾼 사람과 시각이 이력에 남고, 잘못 넣으면 「지우기」 한 번으로
.env값으로 돌아갑니다.
배운 것
- 설정을 DB로 옮길 때 진짜 질문은 「어디에 저장하나」가 아니라 「다른 프로세스가 바뀐 것을 어떻게 아나」입니다.
- 답은 새 인프라가 아니라 이미 함께 보는 곳에서 찾았습니다. 같은 요구에서 세 가지 구현이 나온 것은 서비스마다 그 자리가 달랐기 때문입니다.
- 값을 공유하지 말고 「바뀌었다」만 공유하면, 비밀이 공용 캐시에 앉지 않습니다.
- 공유 신호가 사라질 때를 대비해 상한 시간을 둡니다. 신호가 유실돼도 언젠가는 DB를 다시 읽게 해 두면 영원히 옛 값에 갇히지 않습니다.
- 무엇을 화면에 올릴지만큼 무엇을 올리지 않을지를 먼저 정해야 합니다.
남은 한계
- 운영에서는 새로 띄운 프로세스로 값을 읽어 확인했습니다. 이미 떠 있는 워커가 저장 직후 새 값으로 바뀌는 모습은 시험 환경에서만 보았고, 운영에서 값 하나를 바꿨다 되돌려 보는 확인은 관리자 로그인이 필요한 일이라 따로 남겨 두었습니다.
- 반영 시간이 서비스마다 다릅니다. 홈페이지는 다음 요청부터, Foresight는 최대 1초, Codemit은 Redis 장애 때 최대 30초, 버전 신호가 유실되면 최대 60초입니다.
.env에서 DB로 값을 옮기는 스크립트는 만들지 않았습니다. 값이 두 곳에 있을 수 있어, 화면의 「출처」 표시(DB / .env)를 보고 판단해야 합니다.- 앱 비밀이나 암호화 키를 바꾸면 DB에 저장한 비밀은 열리지 않고
.env값으로 돌아갑니다. 키를 교체한 날에는 비밀을 다시 넣어야 합니다.

