13. 실생활 문제 해결

지금까지 파이썬이라는 언어의 여러 가지 구성 요소에 대해 배웠습니다. 이제는 지금까지 배운 것들을 토대로, 뭔가 유용한 프로그램을 만들어 보겠습니다. 이 장의 목표는 여러분이 직접 파이썬 스크립트를 만들고 사용하는 법을 배우는 것입니다.


13.1 문제

먼저 다음 문제를 해결해 봅시다.

내 중요한 파일을 백업하는 프로그램을 만들고 싶어요.


간단한 문제이기는 하나, 아직 어떻게 접근하면 좋을지 판단할 정보가 부족합니다. 따라서 약간 '분석'을 해 봅시다. 예를 들어, 어떤 파일을 백업할지 어떻게 지정할까요? 파일은 어떻게 저장되어야 하며 또 어디에 저장되어야 할까요?

문제를 분석했다면 이제 프로그램을 '설계'해야 합니다. 이를 위해 우리가 만들 프로그램이 어떻게 동작하면 좋을지 목록을 만들어 봅시다. 저는 다음과 같이 제 나름의 방식대로 목록을 만들었습니다. 그러나 여러분의 생각은 다를 수도 있습니다.  달라도 아무 상관 없습니다.

  • 백업할 파일과 디렉토리는 리스트 형태로 지정한다.

  • 주 백업 디렉토리를 두고, 백업은 그 안에 저장되어야 한다.

  • 백업된 파일은 zip 파일로 압축한다.

  • zip 파일의 이름은 현재 날짜와 시간으로 한다.

  • GNU/Linux 환경이나 Unix 환경에서 기본으로 제공되는 zip 명령을 이용한다. (참고: 명령줄 인터페이스에서 사용할 수 있는 어떤 압축 유틸리티든지 사용이 가능합니다.)


NOTE

윈도 사용자를 위한 윈도 사용자는 GnuWin32 프로젝트 홈페이지에서 zip 명령을 내려받아 설치 할 수 있습니다. 설치한 후 파이썬 명령을 어디서든 실행할 수 있게 했던 것처럼 C:\Program Files\GnuWin32\bin 디렉토리를 시스템의 PATH 환경변수에 추가합니다. 그러면 어디서든 zip 명령을 사용할 수 있습니다.



13.2 첫번째 프로그램

일단은 다음 코드를 입력하여 프로그램을 구현합니다.


https://github.com/swaroopch/byte-of-python/blob/master/programs/backup_ver1.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# 3. The files are backed up into a zip file.
# 4. The name of the zip archive is the current date and time
target = target_dir + os.sep + \
         time.strftime('%Y%m%d%H%M%S') + '.zip'

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir) # make directory

# 5. We use the zip command to put the files in a zip archive
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# Run the backup
print "Zip command is:"
print zip_command
print "Running:"
if os.system(zip_command) == 0:
    print 'Successful backup to', target
else:
    print 'Backup FAILED'
backup_ver1.py(이 코드를 backup_ver1.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ 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


이제 테스트 단계로 넘어와서 프로그램이 잘 동작하는지 확인합니다. 만약 예상대로 프로그램이 동작하지 않으면, 프로그램의 버그bug를 찾아 제거하는 디버그debug를 해야 합니다.

예시 프로그램이 잘 동작하지 않는 경우, 실행 결과의 Zip command is 줄 뒤의 내용을 복사한 후 쉘 (GNU/Linux 나 Mac OS X) 혹은 cmd (윈도우 환경)에 입력한 후 무엇이 문제인지 확인하고 수정하세요. 또 무엇이 잘못되었는지 zip 명령 설명서를 확인합니다. 만약 명령에 이상이 없다면  문제는 파이썬 프로그램 자체에 있는 것일 수도 있습니다. 프로그램을 올바르게 정확히 작성했는지 다시 한번 확인합니다.


지금부터는 앞서 한 설계를 코드로 어떻게 바꿨는지 살펴봅니다.

먼저 os 와 time 모듈을 불러왔습니다. 그리고, 백업할 파일과 디렉토리를 source라는 리스트에 담아두었고, 또 백업을 저장할 대상 디렉토리는 target_dir 변수에 지정했습니다. zip 파일의 이름은 현재 날짜와 시간으로 할 것이므로 time.strftime() 함수를 사용하여 현재 날짜와 시간을 얻은 후 .zip 확장자를 붙여 target_dir 디렉토리에 저장합니다.

여기서 os.sep 변수를 주목하시기 바랍니다. 이것은 여러분의 운영체제에 다른 디렉토리 구분자를 나타내는 것으로 GNU/Linux 와 Unix 환경에서는 '/' 이고, 윈도 환경에서는 '\\' , Mac OS에서는 ':' 입니다. 이러한 문자를 직접 사용하지 않고 os.sep을 사용하면 프로그램에 범용성을 제공하여 여러 운영체제에서 수정 없이 사용할 수 있게 됩니다.

이 프로그램에서 사용한 것처럼 time.strftime() 함수는 특별한 형식을 인수로 넘겨 받습니다. %Y는 네 자리의 연도로 치환되며, %m은 두 자리의 달, 즉 01과 12 사이의 숫자입니다. 날짜 형식에 대해서는 파이썬 레퍼런스 매뉴얼을 참고하시기 바랍니다.

이제 대상 zip 파일의 이름을 생성하기 위해 문자열 결합 연산자인 더하기 연산자를 사용하여 두 문자열을 합쳐 하나의 문자열로 만듭니다. 그리고, zip_command라는 문자열을 만들어 실제로 실행할 문자열을 만듭니다. GNU/Linux 터미널이나 DOS 프롬프트에서 이 문자열을 실행해 잘 동작하는지 확인할 수 있습니다.

앞으로 우리가 사용할 zip 명령은 몇 가지 옵션과 매개 변수를 필요로 합니다. -r 옵션은 zip 명령이 주어진 디렉토리에 대해 회귀적recursive으로, 즉 해당 디렉토리가 포함하고 있는 모든 하위 디렉토리와 파일을 포함하도록 하는 명령입니다. 여기서는 두 가지 옵션을 결합하여 하나의 축약 옵션 -qr 을 지정했습니다. zip 명령 뒤에는 차례로 이 옵션이 지정되고, 그 뒤에는 생성될 zip 파일의 이름이, 마지막으로는 압축할 대상 디렉토리와 파일이 지정됩니다. 이를 위해 source 리스트를 앞서 설명한 join 메소드를 통해 문자열로 바꿔서 넘겼습니다.

그러면, 최종적으로 os.system 함수를 통해 이 명령을 실제 시스템system 의 쉘에서 실행시킵니다. 그러면 프로그램이 성공적으로 실행된 경우 0을, 그렇지 않으면 오류 코드가 반환됩니다.

이제 명령의 실행 결과에 따라, 적절한 메시지를 출력하고 백업이 성공했는지 실패했는지를 화면에 출력합니다.

여기까지입니다. 이제 우리는 중요한 파일을 백업하는 스크립트를 성공적으로 만들었습니다!


윈도 사용자를 위한 노트

이스케이프 문자 백슬래시를 두 번씩 입력하는 것 대신 raw 문자열을 이용할 수도 있습니다. 예를 들어, 'C:\\Documents'는 r’C:\Documents' 로도 쓸 수 있습니다. 그러나, 'C:\Documents'와 같이 사용하지 마세요. 이것은 \D 라는 알 수 없는 이스케이프 문자를 의미하기 때문에 오류가 발생합니다.



백업 스크립트가 완성되었습니다. 이제 이 스크립트를 언제든지 사용하여 중요한 파일들을 백업할 수 있습니다. 이 단계를 소프트웨어의 활용Operation 혹은 배포Deployment 단계라고 합니다.

이 프로그램은 잘 동작하지만, 첫번째로 만든 프로그램은 (종종) 예상한 대로 실행되지 않습니다. 예를 들어 프로그램 설계를 잘못했다든지 코드를 입력할 때 실수를 했다던지가 원인일 수 있습니다. 이 경우 상황에 맞춰서 설계 단계로 돌아가거나 프로그램을 디버깅해야 합니다.


13.3 두 번째 프로그램

첫 번째로 만든 프로그램은 일단 잘 동작합니다. 그러나 아직은  더 개선할 여지가 있습니다. 이 단계를 소프트웨어의 유지보수maintenance 단계라고 합니다.

제가 이 백업 프로그램을 사용하며 느낀 불편한 점은 파일에 이름을 짓는 부분이었습니다. 주 백업 디렉토리에 날짜로 된 하위 디렉토리를 만들고 시간으로 된 압축 파일들을 그 안에 넣는 것입니다. 이렇게 하면 백업된 파일이 계층적으로 저장되므로 좀 더 쉽게 관리할 수 있을 것입니다. 또한, 파일명도 좀 더 짧아집니다. 마지막으로 백업 파일이 디렉토리에 각각 나뉘어 저장되므로 어떤 날에 백업을 했는지를 해당 날짜에 해당하는 디렉토리가 있는지 여부만으로 쉽게 확인할 수도 있습니다.


https://github.com/swaroopch/byte-of-python/blob/master/programs/backup_ver2.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir) # make directory

# 3. The files are backed up into a zip file.
# 4. The current day is the name of the subdirectory
# in the main directory.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# The current time is the name of the zip archive.
now = time.strftime('%H%M%S')

# The name of the zip file
target = today + os.sep + now + '.zip'

# Create the subdirectory if it isn't already there
if not os.path.exists(today):
    os.mkdir(today)
    print 'Successfully created directory', today

# 5. We use the zip command to put the files in a zip archive
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# Run the backup
print "Zip command is:"
print zip_command
print "Running:"
if os.system(zip_command) == 0:
    print 'Successful backup to', target
else:
    print 'Backup FAILED'
backup_ver2.py(이 코드를 backup_ver2.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ 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 함수를 통해 디렉토리를 새로 만듭니다.


13.4 세 번째 프로그램

두 번째 프로그램도 잘 동작하지만, 백업을 많이 하고 싶을 때, 많은 백업 파일이 생성되므로 어떤 파일이 어떤 것의 백업인지 알기 어려웠습니다! 예를 들어, 어떤 문서나 프로그램에 큰 변화를 주었을 때 그 내용을 zip 파일의 이름에 추가하면 구분이 쉬울 것입니다. 이 문제는 zip 파일을 생성할 때 이름 뒤에 사용자 정의 꼬리말을 다는 기능을 추가하면 될 것입니다.


WARNING

다음 프로그램은 동작하지 않으니 놀라지 마시고 쭉 따라하기 바랍니다. 이를 통해 무언가를 배울 수 있을 것입니다.



https://github.com/swaroopch/byte-of-python/blob/master/programs/backup_ver3.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir) # make directory

# 3. The files are backed up into a zip file.
# 4. The current day is the name of the subdirectory
# in the main directory.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# The current time is the name of the zip archive.
now = time.strftime('%H%M%S')

# Take a comment from the user to
# create the name of the zip file
comment = raw_input('Enter a comment --> ')
# Check if a comment was entered
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' +
        comment.replace(' ', '_') + '.zip'

# Create the subdirectory if it isn't already there
if not os.path.exists(today):
    os.mkdir(today)
    print 'Successfully created directory', today

# 5. We use the zip command to put the files in a zip archive
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# Run the backup
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


이 프로그램은 동작하지 않습니다! 실행시켜면 구문 오류가 있다는 메시지가 출력됩니다. 이것은 이 스크립트가 파이썬의 문법 규칙을 만족하지 않았기 때문입니다. 파이썬의 오류 메시지에는 어디에서 이 오류가 발생했는지를 알려줍니다. 따라서 그 줄부터 '디버깅'을 시작합니다.

코드를 자세히 살펴보면, 한 개의 논리적 명령줄이 2개의 물리적 명령줄로 나뉘어 작성되어 있습니다. 그런데 2개의 물리적 명령줄이 사실 하나의 명령줄이라는 것을 파이썬에게 알릴 무언가가 누락되어 있습니다. 여기서 파이썬은 더하기 연산자 (+)를 발견했으나 그 논리적 명령줄에 피연산자가 없어 어떻게 해야 할지 모르는 상황에 처한 것입니다.

앞서 두 물리적 명령줄을 하나로 연결하려면 맨 뒤에 백슬래시를 추가해야 한다고 이미 배웠었습니다. 누락된 백슬래시를 추가해 봅시다. 이처럼 프로그램에서 문제를 찾고 수정하는 이러한 과정을 '버그 수정'이라고 합니다.


13.5 네 번째 프로그램


https://github.com/swaroopch/byte-of-python/blob/master/programs/backup_ver4.py

import os
import time

# 1. The files and directories to be backed up are
# specified in a list.
# Example on Windows:
# source = ['"C:\\My Documents"', 'C:\\Code']
# Example on Mac OS X and Linux:
source = ['/Users/swa/notes']
# Notice we had to use double quotes inside the string
# for names with spaces in it.

# 2. The backup must be stored in a
# main backup directory
# Example on Windows:
# target_dir = 'E:\\Backup'
# Example on Mac OS X and Linux:
target_dir = '/Users/swa/backup'
# Remember to change this to which folder you will be using

# Create target directory if it is not present
if not os.path.exists(target_dir):
    os.mkdir(target_dir) # make directory

# 3. The files are backed up into a zip file.
# 4. The current day is the name of the subdirectory
# in the main directory.
today = target_dir + os.sep + time.strftime('%Y%m%d')
# The current time is the name of the zip archive.
now = time.strftime('%H%M%S')

# Take a comment from the user to
# create the name of the zip file
comment = raw_input('Enter a comment --> ')
# Check if a comment was entered
if len(comment) == 0:
    target = today + os.sep + now + '.zip'
else:
    target = today + os.sep + now + '_' + \
        comment.replace(' ', '_') + '.zip'

# Create the subdirectory if it isn't already there
if not os.path.exists(today):
    os.mkdir(today)
    print 'Successfully created directory', today

# 5. We use the zip command to put the files in a zip archive
zip_command = "zip -r {0} {1}".format(target,
                                      ' '.join(source))

# Run the backup
print "Zip command is:"
print zip_command
print "Running:"
if os.system(zip_command) == 0:
    print 'Successful backup to', target
else:
    print 'Backup FAILED'
backup_ver4.py(이 코드를 backup_ver4.py로 저장하세요)

실행 결과는 다음과 같습니다.

$ 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 함수를 통해 확인합니다. 만약 사용자가 아무것도 입력하지 않고 enter 키를 입력한 경우 (아마도 특별한 꼬릿말이 필요 없는 일상적인 백업일 것입니다.) 이전과 동일하게 처리합니다.

그러나 사용자가 꼬릿말을 입력한 경우에는 zip 파일명을 생성할 때 이름 뒤에 이 꼬릿말을 붙이고 .zip 확장자를 붙입니다. 여기서 사용자가 입력한 꼬릿말에 포함된 공백 문자를 모두 밑줄로 치환하였는데, 이것은 공백 문자가 없는 게 관리하기가 더 쉽기 때문입니다.


13.6 더 많은 개선점

아마 네 번째 프로그램은 많은 경우에 만족스럽게 사용할 정도가 되기는 하나, 찾아오면 개선할 사항은 언제나 넘쳐납니다. 예를 들면, 사용자가 -v옵션을 통해 출력verbosity 단계를 지정하게 하여 프로그램이 실행될 때 단계별로 처리되는 사항을 화면에 출력하도록 할 수도 있고, -q 옵션을 통해 아무 출력 없이(quiet) 프로그램이 실행되도록 할 수도 있습니다.

다른 개선 사항은 추가로 백업할 파일이나 디렉토리를 명령줄로부터 넘겨받아 함께 백업하는 것을 생각해 볼 수 있습니다. 이러한 추가 파일의 이름은 sys.argv 리스트를 이용하면 넘겨받을 수 있고, 이것을 리스트 클래스의 extend메소드를 이용하여 source 리스트 뒤에 추가할 수 있습니다.

생각해볼 수 있는 가장 중요한 개선사항은 os.system을 사용하지 않고 파이썬에서 제공되는 내장 모듈인 zipfile 이나 tarfile을 이용하여 압축 파일을 생성하는 것입니다. 이들은 표준 라이브러리에 포함되어 있기 때문에 zip 프로그램과 같은 추가 프로그램 등을 설치하지 않아도 프로그램이 동작할 것입니다.

여기서는 순전히 교육적인 목적에서 os.system을 이용하여 백업 파일을 생성했습니다. 그래야 누구나 알아볼 수 있을 만큼 프로그램이 간단해지고, 이렇게 만든 프로그램을 실제로 사용하기에도 당장 무리가 없기 때문입니다.

자, 이제 os.system을 호출하지 않고 zipfile 모듈을 사용하여 여러분이 직접 다섯번째 프로그램을 만들어 보지 바랍니다.


13.7 소프트웨어 개발 단계

지금까지 소프트웨어를 개발하면서 여러 단계를 거쳐 왔습니다. 이 단계를 정리하면 다음과 같습니다.

① 무엇을 만들 것인가? (분석 단계)
② 어떻게 만들 것인가? (설계 단계)
③ 만들기 (구현 단계)
④ 테스트 하기 (테스트와 디버깅 단계)
⑤ 실제로 사용하기 (활용 또는 배포 단계)
⑥ 유지 및 보수하기 (개선 단계)


앞으로 프로그램을 작성할 때 지금까지 백업 스크립트를 만들면서 거쳐 왔던 과정을 그대로 따라하기 바랍니다. 문제를 분석하고 프로그램을 설계하세요. 구현은 가장 단순한 프로그램으로 시작하세요. 테스트하고 디버그를 하세요. 한번 사용해보고 제대로 동작하는지 확인하세요. 이제, 원하는 기능을 추가하고, 만들고 테스트하고 사용하는 일련의 과정을 반복하며 프로그램을 개선해 나가세요.


소프트웨어는 성장하는 것이며, 만들어지는 것이 아니다. (Software is grown, not built.)

— Bill de hOra


13.8 요약

지금까지 여러분이 직접 파이썬 프로그램/스크립트를 만드는 법과 이러한 프로그램을 만들기 위해 거쳐 야 했던 여러 가지 단계에 대해 배웠습니다. 이 장에서 배운 것을 통해 이 장에서 배웠던 것을 떠올리면 실제 문제를 해결하는 데 도움이 되고 파이썬에도 더 익숙해질 것입니다.

다음으로는, 객체 지향 프로그래밍에 대해 살펴보겠습니다.