미국 공학박사 아빠의 코딩 연구소

예외(Exception) 처리로 코딩을 안전하게

코딩을 해서 만든 프로그램을 실행하다 보면, 예상하지 못한 이유로 인해 비정상적인 방식으로 종료되거나 멈추는 경우를 종종 보게되요. 이러한 비정상적인 작동도 그 원인을 찾아서 잘 처리해 주면, 프로그램이 비정상적으로 종료되거나 멈추는 것을 방지할 수 있답니다.
이러한 것을 예외(Exception) 처리라고 하는데요. 키클봇 예제를 통해 예외 발생을 직접 확인해 보고 처리해 볼게요.
예외 처리는 Python 라이브러리를 이용하거나 조금 더 다양하고 한 단계 높은 코딩을 하고자 하면 꼭 알아둬야 하는 내용이니 집중해 주세요.


강의들은 모두 무료입니다. 단, 저작권은 키클 코딩랩 에 있으며, 무단 복제 및 배포를 엄금합니다.
이를 어길 시, 본 사이트의 서버가 미국에 있으므로, 미국법에 의해 처벌될 수도 있습니다.




키클봇 서버의 비정상적 종료

먼저, 키클봇 서버에서 예외(Exception) 발생이 어떻게 이루어지는지 살펴볼게요.
원하는 폴더에, 이전 강의 딕셔너리(Dictionary)로 코딩을 편리하게에서 코딩했던 키클봇 서버(lec14_kiklebot.py) 파일을 다운받아 볼게요. 키클봇 서버를 돌리기 위해서는 키클봇 라이브러리(lec13_kiklebot_lib.py) 파일도 필요하니, 함께 다운로드 받지요. 그리고, 강의 원격으로 키클봇 조종하기에서 코딩했던 명령 클라이언트(lec12_cmd.py) 파일도 다운로드 받지요.

이제, 터미널에서 "python3 lec14_kiklebot.py" 명령으로 키클봇 서버를 실행해 보세요. (실행에 대한 자세한 복습이 필요하면, 여기로가서 확인하세요.) 그리고, 새로운 터미널 창을 열어서 "python3 lec12_cmd.py"으로 키클봇에 동작 명령을 내리는 클라이언트도 실행해 보세요.
클라이언트를 실행하면, 터미널 창 위에 figure창이 생성되지요? 이렇게 Figure창이 활성화되면, 키보드 명령을 내릴 수가 없으니, 클라이언트의 실행을 명령한 터미널 창을 마우스 왼쪽버튼 클릭하여 활성화 상태로 만드세요. 그리고, 'i', 'j', 'k', 'l' 키들을 누르면, figure 창에 움직이는 키클봇이 보인답니다.
위 네가지 키들 외에 한 가지 더 명령을 내릴 수 있는 키가 있지요? 바로 'q'인데요. 원격 조종을 멈추고 프로그램을 종료하고 싶을때 누르는 키이지요.

만약, 위 다섯가지 키들 외에, 예를 들어, 'd' 키를 누르면 어떻게 될까요? 어쩌면, 키클봇과 놀다가 이미 실수로 다른 키를 눌러본 경험이 있을지도 모르겠네요. 여하튼, 키 'd'를 누르면, 키클봇 서버를 실행했던 터미널 창에 아래와 같이 'd'가 KeyError를 야기했다는 에러 메세지가 출력이 되면서 프로그램이 종료되어 버린답니다.

Terminal (lec14_kiklebot.py)
waiting...
waiting...
waiting...
Traceback (most recent call last):
  File "lec14_kiklebot.py", line 36, in <module>
    idx_move = key_dict[key]
KeyError: 'd'
gildong@gildong-VirtualBox:~$

위의 메세지를 보면, 키클봇 서버 (lec14_kiklebot.py)의 줄36 (line 36)의 "idx_move = key_dict[key]" 코드에서 예외(Exception) 발생이 되었다고 하네요.

클라이언트를 실행한 터미널 창은 어떤가요? 클라이언트에서 예외가 발생한 것은 아니기 때문에, 아래에 보이는 것처럼 종료 되지는 않고 대기 상태로 있지요?

lec14_kiklebot.py
server received command
server received command
server received command
server received command
server received command

서버가 이미 종료되었기 때문에, 일단 클라이언트도 강제로 종료를 해볼까요?
터미널에서 어떤 프로그램이 돌아가고 있을 경우, 강제로 종료하고자 한다면, ctrl+c (즉, ctrl키와 c키를 동시에)를 누르면 된답니다. Ubuntu에서 굉장히 자주 사용하게 되는 단축키이니 기억해 두세요.

이제, 키클봇 서버에서 왜/어떻게 예외(Exception) 발생이 되었는지 알아볼게요.
아래와 같이 키클봇 서버 파일(lec14_kiklebot.py)을 살펴보지요.

lec14_kiklebot.py
import socket               
import matplotlib.pyplot as plt
import numpy as np
from lec13_kiklebot_lib import KikleBot   # import lec13_kiklebot_lib as my_lib
	
s = socket.socket()         
host = "localhost" 
port = 5000                
s.bind((host, port))        
s.listen(5)                 
conn, addr = s.accept()     

my_bot = KikleBot(np.array([[0, 0]]))   # my_bot = my_lib.KikleBot(np.array([[0, 0]]))
fig = plt.figure()

key_dict = {"l": 0, "i": 1, "j": 2, "k": 3}

while (True):
	fig.clf()
	plt.plot(my_bot.coord[0, 0], my_bot.coord[0, 1], 'bs', markersize=10)
	plt.plot(my_bot.traj[:, 0], my_bot.traj[:, 1], 'b')
	plt.xlim([-10 + my_bot.coord[0, 0], 10 + my_bot.coord[0, 0]])	
	plt.ylim([-10 + my_bot.coord[0, 1], 10 + my_bot.coord[0, 1]])	
	plt.pause(0.01)

	print("waiting...")
	key = conn.recv(5)
	conn.send("server received command".encode("utf-8"))
	key = key.decode('utf-8')

	idx_move = 0
	if (key == 'q'):
		conn.close()               
		break
	else:
		idx_move = key_dict[key]
	
	my_bot.move_bot(idx_move)

앞서, 키클봇 서버를 실행했던 터미널 창에서 말하기를, 줄36 (line 36)의 "idx_move = key_dict[key]" 코드에서 예외 발생이 되었다고 했지요?
클라이언트에서 'd'키를 누르면, 그 값이 서버로 전달이 되고, 줄36의 "key"가 'd'를 간직하게 되지요. 그러면, "key_dict[key]" 코드는 줄16에서 선언된 딕셔너리 key_dict가 'd'를 키값으로 갖고 있는지 검색하지요. 하지만, 딕셔너리 key_dict는 'l', 'i', 'j', 'k'만 키값으로 갖고 있기 때문에, Python은 'd'가 올바른 키값이 아니라는 KeyError 예외를 발생하게 된답니다. 주목할 점은 KeyError에서 말하는 Key라는 것이, 일반적으로 말하는 키보드의 키들을 의미하는 것이 아니라, 딕셔너리의 키값을 의미한다는 것이예요. 즉, KeyError라는 예외(Exception)는 딕셔너리를 대상으로 발생하는 예외라는 것이지요.




If문으로 예외 처리하기

키클봇 서버에서 예외 발생이 어떻게 발생하는지 알았으니, 적절한 예외 처리를 해서 키클봇 서버가 비정상적으로 종료되는 것을 방지해 볼게요.
먼저, if문을 이용하여 예외 처리를 해 보지요. 새로운 파일을 생성하고, "lec15_kiklebot_iferr.py"라고 이름을 지어 보지요. 그리고, 아래와 같이 코딩을 해 볼게요. 키클봇 서버 파일 "lec14_kiklebot.py"에서 예외가 발생했던 줄36 주변의 코드들, 즉, 줄35~줄38을 아래와 같이 "lec15_kiklebot_iferr.py"의 줄35~줄40으로 교체했고 나머지는 똑같답니다.

lec15_kiklebot_iferr.py
import socket               
import matplotlib.pyplot as plt
import numpy as np
from lec13_kiklebot_lib import KikleBot   # import lec13_kiklebot_lib as my_lib
	
s = socket.socket()         
host = "localhost" 
port = 5000                
s.bind((host, port))        
s.listen(5)                 
conn, addr = s.accept()     

my_bot = KikleBot(np.array([[0, 0]]))   # my_bot = my_lib.KikleBot(np.array([[0, 0]]))
fig = plt.figure()

key_dict = {"l": 0, "i": 1, "j": 2, "k": 3}

while (True):
	fig.clf()
	plt.plot(my_bot.coord[0, 0], my_bot.coord[0, 1], 'bs', markersize=10)
	plt.plot(my_bot.traj[:, 0], my_bot.traj[:, 1], 'b')
	plt.xlim([-10 + my_bot.coord[0, 0], 10 + my_bot.coord[0, 0]])	
	plt.ylim([-10 + my_bot.coord[0, 1], 10 + my_bot.coord[0, 1]])	
	plt.pause(0.01)

	print("waiting...")
	key = conn.recv(5)
	conn.send("server received command".encode("utf-8"))
	key = key.decode('utf-8')

	idx_move = 0
	if (key == 'q'):
		conn.close()               
		break
	elif (key in key_dict.keys()):
		idx_move = key_dict[key]
	
		my_bot.move_bot(idx_move)
	else:
		print("Use keys i, j, k, l") 

위에서 예외 처리를 어떠한 방식으로 했는지 자세히 짚어 볼까요?
줄35에서는 elif문을 사용하고 있는데요. 조건을 보면, 클라이언트에서 받은 'key'값이 딕셔너리 key_dict의 키값으로 존재하는지를 점검하고 있어요. 주목할 점은 Python에서 딕셔너리를 생성하면, 그 딕셔너리가 자동으로 갖게 되는 keys()라는 함수를 사용한 것이예요. 즉, 어느 딕셔너리든지 자동으로 keys()라는 함수를 갖게 되고, 이 함수의 리턴값은 해당 딕셔너리가 갖고 있는 모든 키값들이랍니다. 또 한 가지 주목할 점은 for 루프에서처럼 "in"을 사용했다는 것이예요. 즉, "key in key_dict.keys()" 코드의 의미는 "key_dict가 가지고 있는 키값들 안에(in) key가 가지고 있는 값도 있는가" 인 것이지요.
줄36에서는, 줄35의 조건문이 참(True)라면, 즉, key가 key_dict의 키값들 중 하나라면, 그 키값과 짝을 이루는 value값을 idx_move에 할당하고 있답니다
줄38에서는, idx_move의 방향으로 키클봇을 이동시키고 있지요. 키클봇의 move_bot() 메서드에 대한 복습이 필요하다면 이 곳에서 확인하세요.
줄39~줄40에서는, 클라이언트로 받은 key가 'q'도 아니고 key_dict의 키값도 아닌 경우, 사용자에게 경고 메세지를 출력하여 보여주고 있답니다. 저는 "키보드에서 i, j, k, l 키들만 사용하라"고 경고를 하였답니다.

자, 예외 처리된 키클봇 서버 "lec15_kiklebot_iferr.py"를 실행하여, 예외 처리가 적절히 되었는지 확인해 보지요.
터미널에서 "python3 lec15_kiklebot_iferr.py" 명령으로 새 키클봇 서버를 실행하구요, 또 다른 터미널 창에 "python3 lec12_cmd.py"으로 클라이언트를 실행해 보세요. 그리고, 앞서 설명한 것처럼 클라이언트를 실행한 터미널 창을 활성화 시키고, 'i', 'j', 'k', 'l' 키들을 이용하여 키클봇을 움직여 보세요.
그리고, 예외를 발생시켰던 'd'키를 눌러보세요. 어떤가요? 이제는 키클봇 서버가 비정상적으로 종료하지 않고, 아래와 같이 경고문을 출력만 하고 계속해서 클라이언트의 명령을 받고 있는 것을 확인할 수 있지요?

Terminal (lec15_kiklebot_iferr.py)
waiting...
waiting...
Use keys i, j, k, l
waiting...
Use keys i, j, k, l
waiting...
waiting...
waiting...

프로그램의 종료를 원하면, 클라이언트를 실행한 터미널 창에서 'q'키를 누르면, 서버와 클라이언트 모두 정상적으로 종료된답니다.




Try문으로 예외 처리하기

예외 처리를 하는 또 다른 방법을 공부해 볼게요. If문을 이용한 것보다 조금 더 수준 높은 방식이라고 할 수 있는데요. 바로, try문을 이용하는 것이랍니다.

[문법] try...except...

이제, "lec15_kiklebot_try.py"이라는 이름의 새로운 파일을 생성하고, 그 안에 아래의 줄36~줄41과 같이 try...except..를 이용하여 키클봇 서버의 예외 처리를 해볼게요. 나머지 코드들은 "lec14_kiklebot.py"와 똑같답니다.

Terminal (lec15_kiklebot_try.py)
import socket               
import matplotlib.pyplot as plt
import numpy as np
from lec13_kiklebot_lib import KikleBot   # import lec13_kiklebot_lib as my_lib
	
s = socket.socket()         
host = "localhost" 
port = 5000                
s.bind((host, port))        
s.listen(5)                 
conn, addr = s.accept()     

my_bot = KikleBot(np.array([[0, 0]]))   # my_bot = my_lib.KikleBot(np.array([[0, 0]]))
fig = plt.figure()

key_dict = {"l": 0, "i": 1, "j": 2, "k": 3}

while (True):
	fig.clf()
	plt.plot(my_bot.coord[0, 0], my_bot.coord[0, 1], 'bs', markersize=10)
	plt.plot(my_bot.traj[:, 0], my_bot.traj[:, 1], 'b')
	plt.xlim([-10 + my_bot.coord[0, 0], 10 + my_bot.coord[0, 0]])	
	plt.ylim([-10 + my_bot.coord[0, 1], 10 + my_bot.coord[0, 1]])	
	plt.pause(0.01)

	print("waiting...")
	key = conn.recv(5)
	conn.send("server received command".encode("utf-8"))
	key = key.decode('utf-8')

	idx_move = 0
	if (key == 'q'):
		conn.close()               
		break
	else:
		try:
			idx_move = key_dict[key]
			my_bot.move_bot(idx_move)		
		except Exception as e:
			print(type(e), "---", repr(e))			
			print("Use keys i, j, k, l") 

줄36~줄38에서 보듯이, 클라이언트로부터 받은 키보드 정보를 간직한 key변수를 이용하여 이동 방향을 idx_move에 할당하고, move_bot() 메서드를 이용하여 키클봇을 움직이는 코드가 모두 들여쓰기를 통해 try의 하위 블락(block)에 들어왔지요. 즉, try가 실제 예외 발생 가능성이 있는 줄37과 키클봇을 움직이는 줄38을 감싸고 있는 형태이지요.
줄39~줄41은 예외 발생 시 처리하는 코드인데요. 발생한 예외의 정체를 출력하는 코드(줄40)와 'i', 'j', 'k', 'l' 키들을 사용하라는 경고를 출력하는 코드(줄41)를 작성하였답니다.

한가지 주목할 점이 있어요.
앞서, if문을 이용한 예외 처리에서는, "lec15_kiklebot_iferr.py"의 줄35에서처럼 클라이언트에서 누른 키보드의 키가 딕셔너리 key_dict의 키값으로 이용될 수 있는지를 확인해야 했지요.
하지만, 위의 try문을 이용한 예외 처리에서는, "key" 변수에 클라이언트에서 어떤 정보를 받았는지 확인 없이 그저 key_dict에 전달하고 있지요. 혹, 예외가 발생한다면, 그저 except문에서 처리하게끔 할 뿐이지요. 이처럼 try문을 이용한 방식의 예외 처리는, 사용하기에 조금 더 편하고, 작성한 코드들도 보다 체계적으로 보이게 되는 효과를 가져온답니다.

이제, "lec15_kiklebot_try.py" 파일을 저장한 후, 터미널에서 "python3 lec15_kiklebot_try.py" 명령으로 새 키클봇 서버를 실행하구요, 또 다른 터미널 창에 "python3 lec12_cmd.py"으로 클라이언트를 실행해 보세요. 그리고, 클라이언트를 실행한 터미널 창을 활성화 시키고, 'i', 'j', 'k', 'l' 키들을 이용하여 키클봇을 움직여 보세요. 그러다가 'd'키를 키보드에서 누르면, 아래와 같이 예외 처리에서 코딩했던대로 메세지를 출력 후, 비정상적인 종료 없이 다음 클라이언트 명령을 기다리게 된답니다.

Terminal (lec15_kiklebot_try.py)
waiting...
waiting...
waiting...
<class 'KeyError'> --- KeyError('d',)
Use keys i, j, k, l
waiting...

강의에서 작성된 소스 코드 (source code)를 다운받으려면, 다음 링크를 클릭하세요: lec15_kiklebot_iferr.py, lec15_kiklebot_try.py
혹시, 이해가 잘 안되는 부분에 대한 질문이 있거나 다루어 줬으면 하는 주제가 있으면, 화면 오른쪽 하단에 "질문하기" 버튼을 이용해 주세요.






발자취

2019-09-11 "키클 코딩랩 - 미국 공학박사 아빠의 코딩 연구소"로 이름 변경
2019-06-28 코딩 교실 공개
2019-03-18 코딩 교실 제작 시작

바로가기
About
Contact
Privacy Policy
강의목록
질문하기
처음으로