19장. MLOps 와 LLMOps, 워크플로 자동화
워크플로와 DAG
김장하는 날을 떠올려 봅니다. 배추를 절이는 동안 양념을 만듭니다. 둘 다 끝나야 버무리고, 버무려야 통에 담습니다. 담은 뒤에 배추를 절일 수는 없습니다. 이렇게 앞뒤가 정해진 작업 묶음이 워크플로이고, 그 앞뒤 관계를 화살표로 그린 것이 DAG 입니다.
그림은 이 장의 색인 갱신 작업입니다. 화살표는 "앞 작업이 끝나야 뒤 작업을 한다"는 뜻입니다. DAG 의 A 는 방향(화살표), C 는 순환이 없다(되돌아오는 길이 없다)는 뜻입니다. 김장에서 배추 절이기와 양념 만들기처럼 서로 기다리지 않는 작업은 함께 돌릴 수 있습니다.
워크플로를 이루는 것
| 것 | 뜻 | 예 |
|---|---|---|
| 작업(task) | 한 가지 일 | 문서 읽기 |
| 의존 | 먼저 끝나야 하는 작업 | 검사는 문서 읽기 뒤 |
| 다시 시도 | 실패하면 몇 번 더 해 볼지 | 공유 폴더가 잠깐 끊겨도 한 번 더 |
| 일정 | 언제 돌릴지 | 매일 새벽 2시 |
| 기록 | 작업마다 성공, 실패, 건너뜀 | 운영 기록표 |
작업 순서 정하기
파이썬의 graphlib 은 앞뒤 관계를 받아 돌릴 차례를 정해 줍니다. 순환이 있으면 오류를 냅니다.
from graphlib import CycleError, TopologicalSorter
# 작업: [먼저 끝나야 하는 작업들]
kimchi = {"배추 절이기": [], "양념 만들기": [], "버무리기": ["배추 절이기", "양념 만들기"], "통에 담기": ["버무리기"]}
# 결과: 앞 작업이 늘 먼저 나오는 차례
print(list(TopologicalSorter(kimchi).static_order()))
# 담기가 끝나야 절이기를 한다고 잘못 적으면 순환이 생긴다
try:
list(TopologicalSorter(dict(kimchi, **{"배추 절이기": ["통에 담기"]})).static_order())
except CycleError as e:
print("순환:", e.args[1])코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
graphlib | graph(그래프, 점과 선으로 된 짜임)를 다루는 파이썬 표준 모듈이다. 따로 설치하지 않는다 |
TopologicalSorter(kimchi) | topological sort(위상 정렬)를 해 주는 도구를 만든다. 위상 정렬은 앞뒤 관계를 지키며 한 줄로 세우는 일이다 |
.static_order() | 돌릴 차례를 한꺼번에 정해 내준다. list(…) 로 감싸면 목록이 된다 |
dict(kimchi, **{"배추 절이기": ["통에 담기"]}) | kimchi 를 복사하면서 "배추 절이기"의 앞 작업만 바꾼다. 열쇠에 띄어쓰기가 있어 이름=값 꼴로 쓸 수 없으므로 **{…} 꼴로 넘긴다 |
CycleError | 순환(cycle)이 있을 때 나는 오류다 |
e.args[1] | 오류에 담긴 두 번째 값으로, 순환을 이루는 작업들의 목록이다 |
이 코드가 뜻하는 것
학교의 선수 과목을 떠올려 봅니다. "수학 II 는 수학 I 을 들은 뒤에"처럼 먼저 들어야 할 과목이 정해져 있으면, 그것을 지키는 수강 순서표를 짤 수 있습니다. "A 는 B 뒤에, B 는 A 뒤에"처럼 서로가 서로를 기다리면 순서표를 짤 수 없습니다. 여기서 과목이 작업, 선수 과목이 의존, 수강 순서표가 static_order(), 서로 기다리는 과목이 순환입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
kimchi = {…} | 작업마다 먼저 끝나야 할 작업을 적는다 | 과목마다 선수 과목을 적는다 |
list(TopologicalSorter(kimchi).static_order()) | 앞뒤를 지킨 차례를 낸다 | 수강 순서표를 받는다 |
dict(kimchi, **{…}) | 일부러 순환을 만든다 | 서로가 서로의 선수 과목이 되게 적는다 |
except CycleError as e | 순환 오류를 받아 순환을 이루는 길을 보여 준다 | "이 순서표는 짤 수 없다"며 꼬인 고리를 알려 준다 |
둘째 출력 ['배추 절이기', '버무리기', '통에 담기', '배추 절이기'] 는 처음과 끝이 같은 고리입니다. 이 고리를 따라가면 영원히 시작할 수 없습니다. 비유와 다른 점은, 학교에서는 한 학기에 여러 과목을 함께 듣지만 static_order() 는 함께 할 수 있는 작업도 한 줄로 늘어놓는다는 것입니다. 함께 할 수 있는 작업은 아래에서 봅니다.
작게 줄여서 눈으로 보기
get_ready() 를 쓰면 "지금 함께 돌릴 수 있는 작업"을 한 묶음씩 받을 수 있습니다.
from graphlib import TopologicalSorter
kimchi = {"배추 절이기": [], "양념 만들기": [], "버무리기": ["배추 절이기", "양념 만들기"], "통에 담기": ["버무리기"]}
ts = TopologicalSorter(kimchi)
ts.prepare() # 순환이 없는지 확인하고 시작한다
round_no = 1
while ts.is_active():
ready = ts.get_ready() # 지금 함께 돌릴 수 있는 작업들
print(round_no, "차례:", list(ready))
ts.done(*ready) # 끝났다고 알린다
round_no += 11 차례: ['배추 절이기', '양념 만들기']
2 차례: ['버무리기']
3 차례: ['통에 담기']| 결과 | 읽는 법 |
|---|---|
1 차례: ['배추 절이기', '양념 만들기'] | 앞 작업이 없는 둘은 서로 기다리지 않으므로 함께 돌릴 수 있다 |
2 차례: ['버무리기'] | 앞의 둘이 모두 끝났다고 알린 뒤에야 나온다 |
3 차례: ['통에 담기'] | 마지막 작업이다. 이 뒤에는 is_active() 가 거짓이 되어 반복이 끝난다 |
prepare() 는 준비, get_ready() 는 "준비된 것 받기", done() 은 "끝났다고 알리기", is_active() 는 "아직 남은 작업이 있나"라는 뜻입니다. 워크플로 도구의 스케줄러가 작업을 나눠 맡길 때 하는 일이 이것과 닮았습니다.
주의할 점
- 작업 하나는 한 가지 일만 합니다. 작게 나눠야 실패한 곳만 다시 돌릴 수 있습니다.
- 앞 작업이 실패하면 뒤 작업은 돌리지 않고 건너뜁니다. 망가진 결과 위에 쌓지 않기 위해서입니다.