Introduction
지난번에는 예외 처리와 관련해서, 왜 단순한 try, except 문을 사용하지 말아야 하는 지를 간단히 알아봤다.
이번에는 파이썬 모듈을 작성할 때 어떤 식으로 예외를 발생시키는 것이 바람직한지에 대해 소개해보려 한다.
결론부터 말하자면, 모듈 최상위 예외 클래스를 이용해 계층 구조를 가지는 예외 클래스를 이용하는 것이 기본 파이썬 예외 클래스를 사용하는 것 보다 훨씬 편리하다고 할 수 있다.
Possible problem of using built-in exeptions
먼저, 간단한 사용자 모듈을 하나 생성해 보자. 날아가는 물체의 착륙 지점을 계산하기 위한 코드이다.
user_module.py
# user_module.py
from math import sin, cos, pi
def projectile_landing_position(velocity, angle, g_constant):
'''
XY평면 (0,0) 좌표에서 X축 기준 angle(deg,<180,0<) 방향으로
velocity(0<) 값의 속력으로 출발한 물체가, -Y축 방향
중력 가속도 g(0<) 하 얼마의 x 값에서 y=0에 도달할 지 계산하는 함수.
'''
if velocity < 0:
raise ValueError("velocity는 0과 같거나 커야 합니다")
velocity_x = velocity * cos(angle / 180 * pi)
velocity_y = velocity * sin(angle / 180 * pi)
terminal_time = velocity_y / g_constant
landing_position = velocity_x * terminal_time
return landing_position
위와 같이 예상되는 값 오류에 대해서, 파이썬 내장 예외 클래스인 ValueError를 이용할 수도 있다 (12번 라인).
main.py
# main.py
import user_module
import logging
try:
user_module.projectile_landing_position(-1, 45, 9.81)
except Exception:
logging.exception("예외 발생!")
>>
ERROR:root:예외 발생!
Traceback (most recent call last):
File "main.py", line 6, in <module>
user_module.projectile_landing_position(-1, 45, 9.81)
File "C:\Users\machi\user_module.py", line 12, in projectile_landing_position
raise ValueError("velocity는 0과 같거나 커야 합니다")
ValueError: velocity는 0과 같거나 커야 합니다
한번 의도적으로 예외를 일으켜 봤다. 잘 작동한다.
하지만 이 경우, 모듈 함수를 호출할 때 반환될 것이라 예상되는 예외를 처리해야 하는 사용자 입장에서는 모듈 내부에서 기본 예외 클래스를 이용하고 있기 때문에
- 이게 정말 모듈 내부에서 의도를 가지고 일으킨 예외인지,
- 모듈 내부에서 아직 디버그가 되지 않은 버그인지,
- 모듈 자체가 아닌 다른 곳에서 발생한 문제인지
사실상 에러의 원인을 파악하기 이전엔 판단할 길이 없다. ValueError는 앞의 세 케이스 모두에서 발생할 수 때문이다.
또한 파이썬 내장 예외 타입과 그 계층 구조는 실제 모듈에서 표현하고자 하는 에러 상황을 정확히 표현하지 못 할수도 있다.
Solution: Custom module-level exeptions
그렇다면 이번엔 모듈 단위의 예외 클래스를 적용한 예시를 보자.
user_module.py
# user_module.py
from math import sin, cos, pi
class ModuleError(Exception):
''' 모듈 최상위 에러 '''
pass
class InvalidVelocityError(ModuleError):
'''velocity 값이 잘못 된 경우'''
def projectile_landing_position(velocity, angle, g_constant):
'''
XY평면 (0,0) 좌표에서 X축 기준 angle(deg,<180,0<) 방향으로
velocity(0<) 값의 속력으로 출발한 물체가, -Y축 방향
중력 가속도 g(0<) 하 얼마의 x 값에서 y=0에 도달할 지 계산하는 함수.
'''
if velocity < 0:
raise InvalidVelocityError("velocity는 0과 같거나 커야 합니다")
velocity_x = velocity * cos(angle / 180 * pi)
velocity_y = velocity * sin(angle / 180 * pi)
terminal_time = velocity_y / g_constant
landing_position = velocity_x * terminal_time
return landing_position
먼저 모듈 최상위 예외 클래스를 정의하고 (ModuleError), 이를 상속받는 InvalidVelocityError를 하나 정의해 기존의 ValueError를 대체했다. 이 경우엔 모듈에서 미리 예상한 예외만을 처리할 수 있도록 호출 함수 바깥에서 예외 처리 구문을 정의할 수 있다. 모듈에서 raise 하는 모든 에러가 모듈 최상위 에러를 상속하고 있기 때문이다.
main.py
# main.py
import user_module
import logging
try:
user_module.projectile_landing_position(-1, 45, 9.81)
except user_module.ModuleError:
logging.exception("모듈 정의 예외 발생!")
>>
ERROR:root:모듈 정의 예외 발생!
Traceback (most recent call last):
File "main.py", line 6, in <module>
user_module.projectile_landing_position(-1, 45, 9.81)
File "C:\Users\machi\user_module.py", line 20, in projectile_landing_position
raise InvalidVelocityError("velocity는 0과 같거나 커야 합니다")
user_module.InvalidVelocityError: velocity는 0과 같거나 커야 합니다
이전과 똑같이 잘 작동하지만, 이번에는 모듈 내부에서 의도를 가지고 정의한 예외 클래스만을 따로 처리해 줄 수 있다는 부분이 다르다. 이제 사용자는 모듈 내부에서 의도적으로 발생시킨 에러와 그렇지 않은 에러를 효과적으로 구분해 낼 수 있다.
(이전 글에서도 간단히 언급이 되었지만, InvalidVelocityError는 ModuleError를 상속받고 있기 때문에 except: ModuleError를 통해 함수 호출 중 발생한 InvalidVelocityError를 잡아낼 수 있다)
사용자가 모듈 기능을 이용할 때 모듈에서 발생시키는 에러 종류에 대해 모두 대비를 하지 못 하는 경우가 있기 때문에, 위와 같이 모듈 최상위 예외 클래스를 이용한 방어적인 예외 처리를 이용하면 사용자가 미처 인지하고 있지 못 하던 모듈 에러를 처리할 수 있는 기회를 준다.
Catching unexpected exeption raises
또한 이런 식으로 모듈 최상위 에러를 이용하는 방식에는 다른 이점이 있는데, 모듈 개발 단계에서 예상하지 못 했던 버그를 찾아내기 쉽다는 점이다. 한번 아래의 예시를 보자.
user_module.py
# user_module.py
from math import sin, cos, pi
class ModuleError(Exception):
''' 모듈 최상위 에러 '''
pass
class InvalidVelocityError(ModuleError):
'''velocity 값이 잘못 된 경우'''
class InvalidAngleError(ModuleError):
'''angle 값이 잘못 된 경우'''
def projectile_landing_position(velocity, angle, g_constant):
'''
XY평면 (0,0) 좌표에서 X축 기준 angle(deg,<180,0<) 방향으로
velocity(0<) 값의 속력으로 출발한 물체가, -Y축 방향
중력 가속도 g(0<) 하 얼마의 x 값에서 y=0에 도달할 지 계산하는 함수.
'''
if velocity < 0:
raise InvalidVelocityError("velocity는 0과 같거나 커야 합니다")
if angle > 180 or angle < 0:
raise InvalidAngleError("angle은 [0~180] 범위의 값이어야 합니다")
velocity_x = velocity * cos(angle / 180 * pi)
velocity_y = velocity * sin(angle / 180 * pi)
terminal_time = velocity_y / g_constant
landing_position = velocity_x * terminal_time
return landing_position
예외 클래스를 조금 추가했지만 이 코드에는 치명적인 문제가 있다. g_constant 값을 0로 줄 경우 에러가 발생하기 때문이다. 이를 몰랐다고 가정하고, 다음 코드로 이루어진 main문을 실행시켜 보자.
main.py
# main.py
import user_module
import logging
try:
user_module.projectile_landing_position(10, 45, 0.0)
except user_module.ModuleError:
logging.exception("모듈 정의 예외 발생!")
except Exception:
logging.exception("디버그 되지 않은 에러 발생!")
>>
ERROR:root:디버그 되지 않은 에러 발생!
Traceback (most recent call last):
File "main.py", line 6, in <module>
user_module.projectile_landing_position(10, 45, 0.0)
File "C:\Users\machi\user_module.py", line 30, in projectile_landing_position
terminal_time = velocity_y / g_constant
ZeroDivisionError: float division by zero
모듈 함수 실행 중에, 예상치 못한 ZeroDivisionError가 발생했고, 이것이 두번째 except: Exception 블록에서 처리되고 있다 (9번 라인). 이제 이 ZeroDivisionError는 모듈 단에서 고려되지 않았으나 실제로 발생 가능한 에러임이 인지되었고, 개발자가 모듈 코드를 수정할 수 있다.
이렇듯이, 모듈 내부에서 정의된 최상위 예외 클래스는 모듈을 이용하면서 예외 처리 동작을 설계하는 사용자 입장에서 뿐만 아니라 모듈 코드를 작성하는 개발자 입장에서도 유용하다.
이제 마지막 유용성에 대해서 예시와 함께 살펴 보고, 마무리 하자.
Expandability in exception handling
만약 모듈 개발 과정에서 더 specific한 예외 처리가 필요할 것이라 판단해, 더 세세한 상황을 설명하는 예외 클래스를 추가하고 싶다 하자. 이 경우 다음과 같이 general 한 중간 예외 클래스를 상속받는 하위 예외 클래스를 작성하는 식으로 해결할 수 있다. 아래의 예시를 보자.
user_module.py
# user_module.py
from math import sin, cos, pi
class ModuleError(Exception):
''' 모듈 최상위 에러 '''
pass
class InvalidVelocityError(ModuleError):
'''velocity 값이 잘못 된 경우'''
class InvalidAngleError(ModuleError):
'''angle 값이 잘못 된 경우'''
class LargeAngleError(InvalidAngleError):
'''angle 값이 180 초과인 경우'''
class NegativeAngleError(InvalidAngleError):
'''angle 값이 0 미만인 경우'''
def projectile_landing_position(velocity, angle, g_constant):
'''
XY평면 (0,0) 좌표에서 X축 기준 angle(deg,<180,0<) 방향으로
velocity(0<) 값의 속력으로 출발한 물체가, -Y축 방향
중력 가속도 g(0<) 하 얼마의 x 값에서 y=0에 도달할 지 계산하는 함수.
'''
if velocity < 0:
raise InvalidVelocityError("velocity는 0과 같거나 커야 합니다")
if angle > 180:
raise LargeAngleError("angle은 180 이하의 값이어야 합니다")
if angle < 0:
raise NegativeAngleError("angle은 0 이상의 값이어야 합니다")
velocity_x = velocity * cos(angle / 180 * pi)
velocity_y = velocity * sin(angle / 180 * pi)
terminal_time = velocity_y / g_constant
landing_position = velocity_x * terminal_time
return landing_position
angle 값이 180보다 큰 경우, 0보다 작은 경우에 대해 각각 InvalidAngleError를 상속받는 exception 클래스를 새로 정의했다. 기존의 예외 클래스를 상속받고 있기 때문에, 코드가 수정되어 모듈에서 raise하는 exception의 타입이 달라져도 모듈 바깥에서 동작하고 있던 예외 처리 동작은 그대로 기존처럼 작동할 수 있다.
main_original.py
# main_original.py
import user_module
import logging
velocity = 10
angle = 45
g_constant = 9.8
try:
user_module.projectile_landing_position(velocity, angle, g_constant)
except user_module.InvalidAngleError:
if angle > 180:
angle = 180
...
elif angle < 0:
angle = 0
...
except user_module.ModuleError:
logging.exception("모듈 정의 예외 발생!")
except Exception:
logging.exception("디버그 되지 않은 에러 발생!")
이 코드는 위 모듈 변경 사항과 관계없이 그대로 작동한다!
만약 새로운 모듈 예외 타입에 대응하고 싶다면, 기존의 예외 처리 동작에서 분기를 추가해 주기만 하면 된다.
main_modified.py
# main_modified.py
import user_module
import logging
velocity = 10
angle = 45
g_constant = 9.8
try:
user_module.projectile_landing_position(velocity, angle, g_constant)
except user_module.LargeAngleError:
angle = 180
...
except user_module.NegativeAngleError:
angle = 0
...
except user_module.ModuleError:
logging.exception("모듈 정의 예외 발생!")
except Exception:
logging.exception("디버그 되지 않은 에러 발생!")
모듈 수정 사항에 맞추어 예외 처리 구문을 수정했다
이처럼 모듈 예외 클래스에 계층 구조를 사용하면, 새로운 예외를 추가할 때에도 기존의 내장 예외 클래스만을 사용했을 때와는 비교가 되지 않는 강건성과 유연성을 제공해 준다.
Summary
오늘의 내용을 간단히 요약하면 다음과 같다.
모듈 내에 최상위 예외 클래스를 가지는 계층 예외 구조를 사용해서 얻을 수 있는 이득은,
- 구체적인 에러 타입과 계층 구조를 새로 정의해, 모듈에서 발생하는 에러에 대한 표현력을 강화하고 모듈에 적합한 계층 구조로 인한 편의성을 제공한다.
- 최상위 모듈 예외를 잡아내는 방어적인 except 블록을 이용해, 모듈 내부에서 발생시키는 여러 예외를 사용자가 모듈 바깥에서 제대로 처리할 기회를 줄 수 있다.
- 파이썬 기본 Except 클래스를 이용해 모듈 개발 단에서 예상하지 못 한 새로운 종류의 버그를 쉽게 찾아낼 수 있다.
- 더욱 구체적인 예외 상황을 추가하고 싶을 때, 기존 예외의 하위 예외를 추가로 정의함으로써 기존의 예외 처리 구문이 망가지는 것을 방지할 수 있다.