19장. MLOps 와 LLMOps, 워크플로 자동화
다시 돌려도 안전한 작업
엘리베이터 버튼은 열 번 눌러도 엘리베이터가 한 번 옵니다. 밤 작업도 이래야 합니다. 서버가 멈춰 작업을 다시 돌렸더니 문서가 두 번 들어가거나, 쓰다 만 색인을 서비스가 읽으면 안 됩니다.
그림의 세 카드가 다시 돌려도 안전하게 만드는 방법입니다. 왼쪽은 지문으로 바뀐 문서만 골라 일을 줄입니다(증분 처리). 가운데는 지금 쓰는 색인을 고치지 않고 새 판 폴더에 따로 씁니다. 오른쪽은 다 쓴 뒤에 "지금 판은 이것"이라는 가리킴 파일만 한 번에 바꿉니다. 버튼을 여러 번 눌러도 같은 층에 서는 것처럼, 같은 내보내기로 다시 돌리면 같은 판이 됩니다.
나쁜 예와 좋은 예
| 다시 돌리면 탈이 나는 작업 | 다시 돌려도 안전한 작업 | |
|---|---|---|
| 데이터 넣기 | 받은 것을 뒤에 덧붙인다(두 번 들어간다) | 지문을 열쇠로 넣는다(있으면 덮는다) |
| 색인 쓰기 | 서비스가 읽는 색인을 그 자리에서 고친다 | 새 판 폴더에 쓰고 가리킴을 바꾼다 |
| 처리 범위 | "지금까지 새로 온 것" | "이 날짜의 내보내기" |
| 바뀐 것이 없을 때 | 같은 판을 또 만든다 | 판을 만들지 않고 끝낸다 |
가리킴을 한 번에 바꾸기
os.replace 는 파일 이름을 한 번에 바꿉니다. 새 가리킴을 임시 파일에 다 쓴 뒤 바꾸면, 읽는 쪽은 옛 판 아니면 새 판만 봅니다.
import os
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as root: # 예시가 끝나면 지워지는 임시 폴더
root = Path(root)
def publish(version, chunks):
"""새 판 폴더에 다 쓴 뒤, 가리킴 파일을 한 번에 바꾼다"""
folder = root / version
folder.mkdir(exist_ok=True) # 같은 판을 다시 써도 오류 없이 덮는다
(folder / "chunks.txt").write_text("\n".join(chunks), encoding="utf-8")
tmp = root / "CURRENT.tmp"
tmp.write_text(version, encoding="utf-8")
os.replace(tmp, root / "CURRENT") # 이름 바꾸기는 한 번에 일어난다
publish("v001", ["대출 기간은 14일입니다."])
publish("v001", ["대출 기간은 14일입니다."]) # 두 번 돌려도
# 결과: 판 폴더 하나, 가리킴은 v001
print(sorted(p.name for p in root.iterdir()), (root / "CURRENT").read_text(encoding="utf-8"))코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
tempfile.TemporaryDirectory() | 임시 폴더를 만들고, with 블록이 끝나면 안의 파일까지 지운다 |
Path(root) | 경로 글자를 Path(경로) 객체로 바꾼다. / 로 경로를 이어 붙일 수 있다 |
root / version | 임시 폴더 안의 v001 폴더 경로를 만든다 |
folder.mkdir(exist_ok=True) | make directory(폴더 만들기)다. exist_ok 는 "이미 있어도 괜찮다"는 뜻으로, 있으면 오류 없이 넘어간다 |
.write_text(…, encoding="utf-8") | 파일에 글을 통째로 쓴다. 같은 파일이 있으면 덮어쓴다 |
os.replace(tmp, root / "CURRENT") | CURRENT.tmp 의 이름을 CURRENT 로 바꾼다. 같은 이름의 파일이 있으면 그 자리를 차지한다 |
root.iterdir() | iterate directory(폴더 안을 하나씩 돌기)다. 폴더 안의 파일과 폴더를 하나씩 내준다 |
이 코드가 뜻하는 것
식당 입구의 메뉴판을 떠올려 봅니다. 칠판을 지우고 새로 쓰면, 쓰는 동안 손님은 반쯤 지워진 메뉴를 봅니다. 새 메뉴판을 안에서 다 만든 뒤 액자에 한 번에 바꿔 끼우면, 손님은 옛 메뉴 아니면 새 메뉴만 봅니다. 여기서 새 메뉴판이 판 폴더(v001), 액자가 가리킴 파일 CURRENT, 바꿔 끼우기가 os.replace 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
with tempfile.TemporaryDirectory() as root | 끝나면 지워지는 임시 폴더에서 연습한다 | 연습용 빈 벽 |
folder.mkdir(…), (folder / "chunks.txt").write_text(…) | 판 폴더에 조각 글을 다 쓴다 | 새 메뉴판을 안에서 다 만든다 |
tmp.write_text(version, …) | 판 이름을 임시 가리킴 파일에 쓴다 | 액자에 넣을 종이를 준비한다 |
os.replace(tmp, root / "CURRENT") | 임시 파일의 이름을 CURRENT 로 바꾼다 | 액자에 한 번에 바꿔 끼운다 |
둘째 publish("v001", …) | 같은 판을 다시 써도 같은 폴더를 덮을 뿐이다 | 같은 메뉴판을 또 끼워도 벽은 그대로 |
결과 ['CURRENT', 'v001'] v001 은 두 번 돌려도 판 폴더가 하나이고 가리킴이 v001 이라는 뜻입니다. CURRENT.tmp 가 목록에 없는 것은 이름이 CURRENT 로 바뀌었기 때문입니다. 비유와 다른 점은, 식당은 바꿔 낀 옛 메뉴판을 버리지만 여기서는 옛 판 폴더를 남겨 두어 가리킴을 되돌릴 수 있게 한다는 것입니다.
주의할 점
- 판 폴더를 바로 지우지 않습니다. 앞 판이 있어야 가리킴을 되돌릴 수 있습니다. 오래된 판은 몇 개만 남기고 정리합니다.
- 지문은 내용으로 만듭니다. 파일의 수정 시각은 복사만 해도 바뀌어 믿을 수 없습니다.