한글이 깨지는 문제를 고쳤다. 돌려 보니 이번엔 경로 오류가 났다.
경로를 고쳤더니 그다음엔 권한 오류가 났다.
Table of contents
Open Table of contents
앞의 문제에서 멈추고 있었다
처음 실행하면 cp949 로 잡힌 출력 인코딩에서 죽었고 그 뒤 코드는 아예 안 돌았다.
인코딩을 고치니 다음 줄로 갔고 거기서 /data/tmp 가 없다는 문제가 나왔다. 그것을 고치니 또 그다음 줄로 갔다.
앞의 문제가 뒤의 문제를 가리고 있었던 것이다. 뒤의 것이 새로 생긴 것이 아니라 도달을 못 했을 뿐이다.
그러니 만나는 순서는 문제가 코드에 놓인 순서일 뿐이다. 어느 것이 더 중요한지와는 아무 관계가 없었다.
남은 개수를 알 수 없는 구조
한 번에 하나씩 나오니 언제 끝날지를 말할 수 없었다. 이번이 마지막인지 아직 다섯이 남았는지 알 방법이 없다.
mysqldump 를 부르는 구간이 있어 한 번 돌리는 데 몇 분이 걸렸다. 그 시간이 오류 수만큼 쌓이는데 그 수를 모르니 일정도 못 잡는다.
세 번째 오류가 났을 때 방식을 바꿨다. 실행해서 만나는 대신 실행 전에 확인할 것을 훑기로 했다.
사전 준비 — 실행 전에 훑기
필요한 것을 한 번에 보는 함수를 만들었다.
def preflight():
problems = []
if sys.stdout.encoding.lower() not in ('utf-8', 'utf8'):
problems.append(f"출력 인코딩: {sys.stdout.encoding}")
for p in (DATA_DIR, LOG_DIR, TMP_DIR):
if not os.path.isdir(p):
problems.append(f"경로 없음: {p}")
elif not os.access(p, os.W_OK):
problems.append(f"쓰기 불가: {p}")
for cmd in ('ffmpeg', 'mysqldump'):
if shutil.which(cmd) is None:
problems.append(f"명령 없음: {cmd}")
return problems
preflight 는 확인만 하고 아무것도 실행하지 않는다. 그래서 중간에 안 멈추고 끝까지 훑는다.
이것을 시작할 때 부른다.
probs = preflight()
if probs:
for p in probs:
print(f"[사전확인] {p}")
sys.exit(1)
problems 에 담긴 것을 전부 찍고 나서 sys.exit 한다. 하나 찾고 멈추는 것과 여기가 다르다.
한 번에 전부 나왔다
돌려 보니 네 개가 한꺼번에 나왔다.
[사전확인] 출력 인코딩: cp949
[사전확인] 경로 없음: /data/tmp
[사전확인] 쓰기 불가: /var/log/collect
[사전확인] 명령 없음: ffmpeg
하나씩 고치고 다시 돌리는 대신 네 개를 한 번에 처리했다. 네 번 돌릴 것을 한 번으로 줄인 셈이다.
실행하며 만나는 것과 preflight 로 미리 확인하는 것은 걸리는 시간이 다르고 본 작업이 오래 걸릴수록 그 차이가 커진다.
판단 기준 — 무엇을 확인할지
전부 확인할 수는 없으니 대상을 가르는 기준을 뒀다.
확인한다
외부에 의존하는 것 (경로, 명령, 접속, 환경 변수)
없으면 반드시 실패하는 것
확인 안 한다
실행해 봐야 아는 것 (자료 내용, 계산 결과)
없어도 일부만 실패하는 것
이 기준으로 추린 것은 대부분 DATA_DIR 같은 경로와 ffmpeg 같은 명령이었다. 자료 내용이 맞는지는 돌려 봐야 아는 것이라 preflight 에 넣지 않았다.
확인 자체가 오래 걸리면 본 작업만큼 느려지므로 os.path.isdir 과 shutil.which 로 있는지만 보는 선을 넘지 않게 했다.
경고와 중단을 나눴다
걸린 것 중에 없어도 도는 것이 섞여 있었다. 하나 때문에 시작조차 못 하면 그것대로 곤란하다.
probs = preflight()
fatal = [p for p in probs if p.startswith(('경로 없음', '쓰기 불가'))]
warn = [p for p in probs if p not in fatal]
for p in warn:
print(f"[경고] {p}")
if fatal:
for p in fatal:
print(f"[중단] {p}")
sys.exit(1)
fatal 에 걸리면 멈추고 warn 은 알리기만 하고 계속 간다. ffmpeg 은 영상 처리에만 쓰므로 없어도 나머지가 도는 쪽이었다.
startswith 로 가르는 기준을 코드에 적어 두니 다음 사람이 판단할 것이 없고 새 항목은 어느 쪽인지만 정하면 된다.
고친 것을 되짚었다
네 개를 고치고 나서 앞의 셋도 다시 봤다. 한글 문제를 고친 방식이 다른 자리에도 필요한지가 궁금했다.
# 출력 인코딩 고정
sys.stdout.reconfigure(encoding='utf-8')
sys.stdout 만 고정하면 끝이 아니었고 파일을 읽고 쓰는 자리에도 같은 명시가 필요했다.
open(path, encoding='utf-8')
encoding 을 안 준 open 이 여섯 군데 있었다. 그중 셋은 아직 안 걸렸을 뿐 언젠가 걸릴 자리였다.
하나를 고칠 때 같은 종류를 찾아보는 편이 낫다. 나중에 하나씩 만나면 그때마다 오늘 한 일을 다시 하게 된다.
검증 — 확인 결과를 기록
사전 확인이 통과했을 때도 로그를 남기게 했다.
[사전확인] 통과 (경로 3, 명령 2, 환경변수 4)
무엇을 몇 개 확인했는지가 함께 남는다. 확인 항목이 0개인데 통과로 찍히는 상황을 이것으로 거른다.
통과라는 글자만 남기면 problems 가 빈 것이 확인의 결과인지 확인을 안 한 결과인지 구분이 안 된다. 센 개수가 있어야 그 통과의 근거를 안다.
정리
- 앞의 문제가 뒤의 문제를 가리고 있을 수 있다
- 첫 오류에서 멈추면 그 뒤는 드러날 기회가 없다
- 만나는 순서는 코드에 놓인 순서일 뿐 중요도와 무관하다
- 하나씩 만나면 몇 개가 남았는지 몰라 일정도 못 잡는다
preflight로 실행 전에 훑으면 한 번에 전부 나온다- 외부에 의존하는 것만 확인하고 돌려 봐야 아는 것은 넣지 않는다
fatal과warn을 갈라 중단과 경고로 다룬다- 하나를 고칠 때
encoding을 안 준 자리처럼 같은 종류를 찾아본다 - 통과했을 때도 몇 개를 확인했는지 수를 남긴다