문제 해결

지금까지 파이썬의 다양한 부분들을 알아봤으므로 이제 뭔가 유용한 작업을 수행하는 프로그램을 설계하고 작성해 보면서 이러한 부분들이 어떻게 하나로 합쳐지는지 확인하겠습니다. 그러자면 파이썬 스크립트를 어떻게 작성하는지 배울 필요가 있습니다.

문제

이번에 풀고자 하는 문제는 다음과 같습니다.

중요한 파일의 백업을 만드는 프로그램을 만들고 싶다.

간단한 문제이긴 하지만 해법을 만들기에는 정보가 충분하지 않습니다. 좀 더 분석이 필요합니다. 예를 들어, 백업할 파일이 어느 것인지 어떻게 지정해야 할까요? 파일을 어떻게 저장할까요? 파일들을 어디에 저장해야 할까요?

문제를 적절히 분석하고 나면 프로그램을 설계(design)합니다. 즉, 프로그램이 어떻게 동작해야 할지에 관한 작업 목록을 만듭니다. 이 경우 제 입장에서는 프로그램의 동작 방식에 관해 다음과 같은 목록을 만들었습니다. 여러분이 프로그램을 설계한다면 저와 동일한 분석 과정을 거치지 않을지도 모릅니다. 왜냐하면 모든 사람들이 각자 작업을 수행하는 방법이 다르기 때문인데, 그렇더라도 전혀 문제가 없습니다.

  • 백업할 파일과 디렉터리를 리스트에 기술합니다.
  • 백업은 반드시 메인 백업 디렉터리에 저장해야 합니다.
  • 파일은 zip 파일로 백업됩니다.
  • zip 압축 파일의 이름은 현재 날짜와 시간입니다.
  • 표준 GNU/리눅스나 유닉스 배포판에서 기본적으로 사용 가능한 표준 zip 명령을 사용합니다. 참고로 명령줄 인터페이스가 제공되기만 한다면 원하는 어떤 압축 명령을 사용해도 됩니다.

윈도우 사용자를 위한 팁

윈도우 사용자는 GnuWin32 프로젝트 페이지에서 zip 명령을 설치하고 C:\Program Files\GnuWin32\bin을 시스템 PATH 환경변수에 추가할 수 있습니다. 이는 파이썬 명령 자체를 인식시키기 위해 했던 작업과 비슷합니다.

해법

이제 프로그램의 설계가 적당히 정리됐으므로 해법의 구현(implementation)에 해당하는 코드를 작성할 수 있습니다.

backup_ver1.py 파일:

import os
import time

# 1. 백업할 파일과 디렉터리는 리스트에 지정됩니다.
# 윈도우에서의 예:
# source = ['"C:\\My Documents"']
# macOS와 리눅스에서의 예:
source = ['/Users/swa/notes']
# 이름에 공백이 포함된 문자열 안에서는 큰따옴표를 써야 합니다.
# [r'C:\My Documents']와 같이 원시 문자열을 사용해도 됩니다.

# 2. 백업 파일은 메인 백업 디렉터리에 저장해야 합니다.
# 윈도우에서의 예:
# target_dir = 'E:\\Backup'
# macOS와 리눅스에서의 예:
target_dir = '/Users/swa/backup'
# 사용할 폴더로 이 값을 변경하는 것을 잊지 마세요.

# 3. 파일은 zip 파일로 백업됩니다.
# 4. zip 파일의 이름은 현재 날짜와 시간으로 구성됩니다.
target = target_dir + os.sep + \
         time.strftime('%Y%m%d%H%M%S') + '.zip'

# 대상 디렉터리가 존재하지 않는 경우 생성합니다
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # 디렉터리를 생성

# 5. zip 명령어를 사용해 파일을 zip 파일에 추가합니다
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# 백업 실행
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

출력 결과:

$ python backup_ver1.py
Zip command is:
zip -r /Users/swa/backup/20140328084844.zip /Users/swa/notes
Running:
  adding: Users/swa/notes/ (stored 0%)
  adding: Users/swa/notes/blah1.txt (stored 0%)
  adding: Users/swa/notes/blah2.txt (stored 0%)
  adding: Users/swa/notes/blah3.txt (stored 0%)
Successful backup to /Users/swa/backup/20140328084844.zip

이제 프로그램이 적절히 동작하는지 검사하는 테스트 단계입니다. 프로그램이 기대한 바대로 동작하지 않으면 프로그램을 디버깅(debug), 즉 프로그램에서 버그(bug. 오류)를 제거해야 합니다.

앞의 프로그램이 적절히 동작하지 않으면 출력 결과에서 Zip command is라는 줄 다음에 출력된 줄을 복사한 다음, 셸(GNU/리눅스 및 macOS) 또는 cmd(윈도우)에서 붙여넣어 오류가 무엇인지 확인한 후 고쳐봅니다. 무엇이 잘못될 수 있는지 zip 명령어 매뉴얼도 확인합니다. 명령이 성공하면 문제가 파이썬 프로그램 자체에 있을 수 있으므로 프로그램이 앞에서 작성한 것과 정확히 일치하는지 확인합니다.

동작 원리

앞의 예제를 통해 설계코드로 어떻게 바꿀 수 있는지 단계별로 알 수 있을 것입니다.

프로그램에서는 먼저 ostime 모듈을 임포트해서 활용합니다. 그런 다음 백업할 파일과 디렉터리를 source 리스트에 지정합니다. 대상 디렉터리는 모든 백업 파일을 저장할 곳이고, target_dir 변수에 지정돼 있습니다. 프로그램을 통해 생성하려는 zip 압축 파일의 이름은 현재 날짜와 시간으로서 time.strftime() 함수를 이용해 생성합니다. 압축 파일은 이름에 .zip 확장자도 포함하고 target_dir 디렉터리에 저장될 것입니다.

os.sep 변수를 사용하는 데 주목하세요. 이 변수는 현재 사용 중인 운영체제의 디렉터리 구분자를 나타냅니다. 즉, GNU/리눅스, 유닉스, macOS에서는 이 변수의 값이 '/'이 될 테고, 윈도우에서는 '\\'가 될 것입니다. 이러한 문자를 직접 사용하지 않고 os.sep를 사용하면 프로그램의 이식성이 높아지고 앞에서 나열한 모든 운영체제에서 동작하게 됩니다.

time.strftime() 함수는 앞의 프로그램에서 사용한 것과 같은 서식 명세를 받습니다. %Y 서식 명세는 세기를 포함한 연도로 대체됩니다. %m 서식 명세는 01 ~ 12 사이의 십진수로 표현되는 월로 대체됩니다. 이러한 서식 명세의 전체 목록은 파이썬 참조 매뉴얼에서 확인할 수 있습니다.

문자열을 연결(concatenate)하는, 즉 두 문자열을 합쳐서 새로운 문자열 하나를 반환하는 더하기 연산자를 이용해 대상 zip 파일의 이름을 만듭니다. 그런 다음, 실행하려는 명령이 담긴 zip_command 문자열을 만듭니다. 이를 셸(GNU/리눅스 터미널이나 DOS 프롬프트)에서 실행해 이 명령이 문제 없이 동작하는지 확인할 수 있습니다.

여기서 사용하는 zip 명령에는 몇 가지 옵션이 있으며, 그중 하나는 -r입니다. -r 옵션은 zip 명령이 디렉터리에 재귀적으로(recursively) 동작해야 한다는 것을 나타냅니다. 즉, 모든 하위 디렉터리와 파일을 포함해야 합니다. 옵션은 생성할 zip 압축 파일명 다음에 나오고, 옵션 이후에 백업할 파일과 디렉터리의 목록이 따라옵니다. 이미 앞에서 사용법을 살펴본 문자열의 join 메서드를 이용해 source 리스트를 문자열로 변환합니다.

그런 다음, 최종적으로 os.system 함수를 이용해 명령을 실행합니다. 이 함수는 시스템, 즉 셸에서 실행하는 것처럼 명령을 실행하고, 명령이 성공하면 0을 반환하고, 그렇지 않을 경우 오류 번호를 반환합니다.

명령의 실행 결과에 따라 백업이 성공했거나 실패했다는 적절한 메시지를 출력합니다.

이게 끝입니다. 이렇게 해서 중요 파일을 백업하는 스크립트를 만들었습니다!

윈도우 사용자를 위한 팁

이중 백슬래시 이스케이프 시퀀스(\\)가 아닌 원시 문자열도 사용할 수 있습니다. 예를 들어, 'C:\\Documents'r'C:\Documents'를 사용하면 됩니다. 하지만 'C:\Documents'는 사용해서는 안 되는데, 이 경우 \D라는 알 수 없는 이스케이프 시퀀스를 사용하는 것이 되기 때문입니다.

이제 동작하는 백업 스크립트를 만들었으므로 파일을 백업하고 싶을 때마다 사용할 수 있습니다. 이를 운영(operation) 단계 또는 소프트웨어의 배포(deployment) 단계라고 합니다.

앞에서 작성한 프로그램은 적절히 작동하지만 (일반적으로) 첫 번째 프로그램은 정확히 기대한 바대로 동작하지 않습니다. 예를 들어, 프로그램을 적절히 설계하지 않았거나 코드를 입력할 때 실수하는 등 문제가 있을 수도 있습니다. 이 경우 적당히 설계 단계로 되돌아가거나 프로그램을 디버깅해야 합니다.

두 번째 버전

첫 번째 버전의 스크립트는 동작합니다. 하지만 이 프로그램을 매일매일 더 잘 동작할 수 있게 개선할 수 있습니다. 이를 소프트웨어의 유지보수(maintenance) 단계라 합니다.

유용할 만한 개선 사항 중 하나는 파일명 생성 메커니즘을 개선하는 것입니다. 즉, 현재 날짜를 메인 백업 디렉터리 내의 디렉터리명으로 사용하고 시간을 디렉터리 내의 파일명으로 사용하는 것입니다. 이렇게 했을 때의 첫 번째 이점은 백업이 계층적인 방식으로 저장되어 관리하기가 훨씬 수월해진다는 것입니다. 두 번째 이점은 파일명이 훨씬 더 짧아진다는 것입니다. 세 번째 이점은 디렉터리로 구분하면 특정 일자의 백업을 만들었을 때만 디렉터리가 생성될 것이기 때문에 매일 백업을 만들었는지 여부를 확인하는 데 좋다는 점입니다.

backup_ver2.py 파일:

import os
import time

# 1. 백업할 파일과 디렉터리는 리스트에 지정됩니다.
# 윈도우에서의 예:
# source = ['"C:\\My Documents"', 'C:\\Code']
# macOS와 리눅스에서의 예:
source = ['/Users/swa/notes']
# 이름에 공백이 포함된 문자열 안에서는 큰따옴표를 써야 합니다.

# 2. 백업 파일은 메인 백업 디렉터리에 저장해야 합니다.
# 윈도우에서의 예:
# target_dir = 'E:\\Backup'
# macOS와 리눅스에서의 예:
target_dir = '/Users/swa/backup'
# 사용할 폴더로 이 값을 변경하는 것을 잊지 마세요.

# 대상 디렉터리가 존재하지 않는 경우 생성합니다
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 3. 파일은 zip 파일로 백업됩니다.
# 4. 현재 날짜를 메인 디렉터리 내의 하위 디렉터리명으로 사용합니다.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# 현재 시간을 zip 파일의 이름으로 사용합니다.
now = time.strftime('%H%M%S')

# zip 파일의 이름
target = today + os.sep + now + '.zip'

# 하위 디렉터리가 존재하지 않으면 생성합니다
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. zip 명령어를 사용해 파일을 zip 파일에 추가합니다
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# 백업 실행
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

출력 결과:

$ python backup_ver2.py
Successfully created directory /Users/swa/backup/20140329
Zip command is:
zip -r /Users/swa/backup/20140329/073201.zip /Users/swa/notes
Running:
  adding: Users/swa/notes/ (stored 0%)
  adding: Users/swa/notes/blah1.txt (stored 0%)
  adding: Users/swa/notes/blah2.txt (stored 0%)
  adding: Users/swa/notes/blah3.txt (stored 0%)
Successful backup to /Users/swa/backup/20140329/073201.zip

동작 원리

프로그램의 대부분은 그대로 유지됩니다. 변경된 부분은 os.path.exists 함수를 이용해 메인 백업 디렉터리 안에 이름이 현재 날짜로 지정된 디렉터리가 있는지 검사하는 것입니다. 이러한 디렉터리가 존재하지 않으면 os.mkdir 함수를 이용해 생성합니다.

세 번째 버전

두 번째 버전도 백업을 여러 번 할 때 잘 동작하지만 백업이 많을 때는 무엇 때문에 백업했는지 구분하기가 어려워집니다. 예를 들어, 프로그램이나 프로그램의 외양을 크게 변경할 수도 있는데, 그러한 변화를 zip 압축 파일의 이름에 반영하고자 합니다. 이것은 zip 파일의 이름에 사용자가 직접 주석을 다는 방식으로 손쉽게 해결할 수 있습니다.

경고: 다음 프로그램은 동작하지 않으므로 놀라지 마세요. 여기서 배울 점이 있으므로 계속 따라오시면 됩니다.

backup_ver3.py 파일:

import os
import time

# 1. 백업할 파일과 디렉터리는 리스트에 지정됩니다.
# 윈도우에서의 예:
# source = ['"C:\\My Documents"', 'C:\\Code']
# macOS와 리눅스에서의 예:
source = ['/Users/swa/notes']
# 이름에 공백이 포함된 문자열 안에서는 큰따옴표를 써야 합니다.

# 2. 백업 파일은 메인 백업 디렉터리에 저장해야 합니다.
# 윈도우에서의 예:
# target_dir = 'E:\\Backup'
# macOS와 리눅스에서의 예:
target_dir = '/Users/swa/backup'
# 사용할 폴더로 이 값을 변경하는 것을 잊지 마세요.

# 대상 디렉터리가 존재하지 않는 경우 생성합니다
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 3. 파일은 zip 파일로 백업됩니다.
# 4. 현재 날짜를 메인 디렉터리 내의 하위 디렉터리명으로 사용합니다.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# 현재 시간을 zip 파일의 이름으로 사용합니다.
now = time.strftime('%H%M%S')

# 사용자에게서 텍스트를 입력받아 zip 파일의 이름을 만듭니다.
comment = input('Enter a comment --> ')
# 텍스트를 입력했는지 검사합니다.
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' + 
        comment.replace(' ', '_') + '.zip'

# 하위 디렉터리가 존재하지 않으면 생성합니다
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. zip 명령어를 사용해 파일을 zip 파일에 추가합니다
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# 백업 실행
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

출력 결과:

$ python backup_ver3.py
  File "backup_ver3.py", line 39
    target = today + os.sep + now + '_' +
                                        ^
SyntaxError: invalid syntax

동작(동작하지 않는) 원리

이 프로그램은 동작하지 않습니다! 파이썬이 문법 오류가 있다고 알려주며, 이 오류의 의미는 스크립트가 파이썬이 기대하는 구조로 작성되지 않았다는 뜻입니다. 파이썬에서 출력하는 오류를 관찰하면 오류가 발생한 위치도 알 수 있습니다. 그럼 해당 라인에서 프로그램을 디버깅해 봅시다.

오류가 발생한 라인을 유심히 관찰한 결과, 한 줄의 논리 라인이 두 줄의 물리적 라인으로 쪼개졌지만 이 두 줄의 물리적 라인이 한 묶음이라고 알려주지 않았습니다. 기본적으로 파이썬은 해당 논리적 라인에서 아무런 피연산자 없이 더하기 연산자(+)가 등장한 것을 발견했고, 따라서 어떻게 진행해야 할지 알 수 없습니다. 이 경우 물리적 라인 끝에서 역슬래시를 사용해 논리적 라인이 다음 물리적 라인에서 계속된다고 알려줄 수 있습니다. 따라서 프로그램에 이를 반영해 봅시다. 이처럼 오류를 발견했을 때 프로그램을 고치는 것을 버그 수정(bug fixing)이라 합니다.

네 번째 버전

backup_ver4.py 파일:

import os
import time

# 1. 백업할 파일과 디렉터리는 리스트에 지정됩니다.
# 윈도우에서의 예:
# source = ['"C:\\My Documents"', 'C:\\Code']
# macOS와 리눅스에서의 예:
source = ['/Users/swa/notes']
# 이름에 공백이 포함된 문자열 안에서는 큰따옴표를 써야 합니다.

# 2. 백업 파일은 메인 백업 디렉터리에 저장해야 합니다.
# 윈도우에서의 예:
# target_dir = 'E:\\Backup'
# macOS와 리눅스에서의 예:
target_dir = '/Users/swa/backup'
# 사용할 폴더로 이 값을 변경하는 것을 잊지 마세요.

# 대상 디렉터리가 존재하지 않는 경우 생성합니다
if not os.path.exists(target_dir):
    os.mkdir(target_dir)  # make directory

# 3. 파일은 zip 파일로 백업됩니다.
# 4. 현재 날짜를 메인 디렉터리 내의 하위 디렉터리명으로 사용합니다.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# 현재 시간을 zip 파일의 이름으로 사용합니다.
now = time.strftime('%H%M%S')

# 사용자에게서 텍스트를 입력받아 zip 파일의 이름을 만듭니다.
comment = input('Enter a comment --> ')
# 텍스트를 입력했는지 검사합니다.
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' + \
        comment.replace(' ', '_') + '.zip'

# 하위 디렉터리가 존재하지 않으면 생성합니다
if not os.path.exists(today):
    os.mkdir(today)
    print('Successfully created directory', today)

# 5. zip 명령어를 사용해 파일을 zip 파일에 추가합니다
zip_command = 'zip -r {0} {1}'.format(target,
                                      ' '.join(source))

# 백업 실행
print('Zip command is:')
print(zip_command)
print('Running:')
if os.system(zip_command) == 0:
    print('Successful backup to', target)
else:
    print('Backup FAILED')

출력 결과:

$ python backup_ver4.py
Enter a comment --> added new examples
Zip command is:
zip -r /Users/swa/backup/20140329/074122_added_new_examples.zip /Users/swa/notes
Running:
  adding: Users/swa/notes/ (stored 0%)
  adding: Users/swa/notes/blah1.txt (stored 0%)
  adding: Users/swa/notes/blah2.txt (stored 0%)
  adding: Users/swa/notes/blah3.txt (stored 0%)
Successful backup to /Users/swa/backup/20140329/074122_added_new_examples.zip

동작 원리

이제 프로그램이 동작합니다! 세 번째 버전에서 적용한 실제 개선 사항을 시험해 봅시다. input 함수를 이용해 사용자에게서 텍스트를 입력받아 len 함수로 입력 내용의 길이를 확인하는 방식으로 사용자가 실제로 뭔가를 입력했는지 검사합니다. 사용자가 아무 내용도 입력하지 않고 그냥 엔터만 눌렀다면(아마 일상적으로 수행하는 백업이나 특별한 변경사항이 없어서) 이전과 동일하게 진행합니다.

하지만 사용자가 텍스트를 입력했다면 입력한 내용이 zip 파일명의 .zip 확장자 바로 앞에 추가됩니다. 참고로 텍스트에 포함된 공백은 밑줄로 대체합니다. 이렇게 하는 이유는 파일명에 공백이 없어야 관리하기가 훨씬 수월하기 때문입니다.

추가 개선 사항

네 번째 버전은 대부분의 사용자에게 만족스럽게 동작하지만 항상 개선의 여지는 있습니다. 예를 들어, 프로그램의 동작 과정을 좀 더 상세하게 나타내는 -v 옵션이나 프로그램을 조용하게 동작시키는 -q 옵션을 지정해 zip 명령의 상세 출력 수준을 포함할 수 있습니다.

적용할 만한 또 다른 개선 사항은 명령줄에서 추가 파일 및 디렉터리를 스크립트에 지정할 수 있게 만드는 것입니다. 이러한 이름을 sys.argv 리스트에서 받아 list 클래스에서 제공하는 extend 메서드를 이용해 source 리스트에 추가할 수 있습니다.

가장 중요한 개선 사항은 압축 파일을 만드는 데 os.system를 사용하지 않고 그 대신 zipfile이나 tarfile이라는 파이썬에 내장된 모듈을 이용해 압축 파일을 생성하는 것일 것입니다. 이것들은 표준 라이브러리의 일부라서 곧바로 사용할 수 있으므로 별도의 외부 zip 프로그램이 필요하지 않습니다.

하지만 앞의 예제에서는 os.system으로 백업을 생성하는 방법을 이용했는데, 이는 순수하게 교육의 목적으로 예제를 모든 이들이 이해할 수 있을 정도로 단순하지만 동시에 충분히 실제 상황에서도 유용할 수 있게 만들기 위해서입니다.

여러분이 os.system을 사용하는 대신 zipfile 모듈을 사용하는 다섯 번째 버전을 작성해 보면 어떨까요?

소프트웨어 개발 프로세스

지금까지 소프트웨어를 작성하는 다양한 단계를 거쳤습니다. 이러한 단계는 다음과 같이 정리할 수 있습니다.

  1. 무엇을 (분석)
  2. 어떻게 (설계)
  3. 수행 (구현)
  4. 테스트 (테스트 및 디버깅)
  5. 사용 (운영 또는 배포)
  6. 유지보수 (개선)

추천하는 프로그램 작성 방법은 백업 스크립트를 만들면서 따랐던 절차입니다. 분석 및 설계를 수행합니다. 간단한 버전으로 구현을 시작합니다. 구현된 프로그램을 테스트하고 디버깅합니다. 프로그램을 사용해 보고 기대한 바대로 동작하는지 확인합니다. 이제 필요한 기능을 추가하고 수행-테스트-사용 사이클을 필요한 만큼 계속 반복합니다.

다음을 기억하세요:

소프트웨어는 구축되는 것이 아니라 성장하는 것이다. -- Bill de hÓra

정리

지금까지 파이썬 프로그램/스크립트를 직접 만들어보고 프로그램을 작성할 때 수반되는 다양한 단계를 살펴봤습니다. 파이썬을 비롯해 문제 해결에 익숙해질 수 있도록 이번 장에서 진행했던 것처럼 프로그램을 직접 작성해 보는 것이 유용하게 느껴졌을 것입니다.

다음 장에서는 객체지향 프로그래밍을 알아보겠습니다.