Skip to content

2.14. 내용정리: 14일차

흔한 찐따 edited this page Mar 20, 2022 · 2 revisions

예외 처리 (Exception Handling)

파이썬에서 예외(Exception)가 발생하는 경우는 대표적으로 두 가지 경우로 나뉜다.

  • 외부적인 문제
    • 프로그램 외적인 부분에서 문제가 발생하는 경우
      • 코드에 오타를 입력해서 구문 오류가 발생하는 경우
      • 파일 입출력을 하는 도중 문제가 발생하는 경우
  • 내부적인 문제
    • 프로그램 내부에서 논리적으로 문제가 발생하는 경우
      • 즉, 프로그램 상에서 어떤 처리 과정 중 오류가 발생하는 경우
      • 알고리즘상의 논리적인 문제점이 발생하는 경우

그리고 이를 구조적인 예외로 범주를 정해 나눠보면 다음과 같다.

  • 하드웨어 예외 (Hardware Exception)
    • 주로 외부적인 문제에 해당하는 경우이다.
    • 하드웨어에서 인식하고 알려주는 예외를 의미한다.
    • 즉, 예외상황이 발생했음을 알리는 주체가 하드웨어라는 의미이다.
  • 소프트웨어 예외 (Software Exception)
    • 주로 내부적인 문제에 해당하는 경우이다.
    • 소프트웨어는 우리가 작성한 프로그램과 운영체제(OS)까지 포함된다.
    • 즉, 소프트웨어 예외라는 것은 프로그래머가 직접 정의할 수 있는 예외이다.

정리하자면, 하드웨어 예외는 이미 결정된 사안이지만, 소프트웨어 예외는 직접 결정해야 한다는 차이점이 있다.

예외(Exception)와 에러(Error; 오류)의 차이점

  • 프로그램 실행 시 발생하는 문제점 대부분은 예외이다.
  • 처리 불가능한 문제가 발생하면 프로그램은 종료하게 되는데, 이러한 문제를 오류라고 한다.
  • 따라서 외부적인 요소에 의한 문제점이든 내부적인 요소에 의한 문제점이든 구조적 예외처리 기법의 관점에서 보면 대부분 해결이 가능해야 한다.
  • 즉, 프로그램 실행 시 예측 가능한 대부분의 문제점을 예외로 간주한다.

정리하자면, "오류"는 프로그램 코드를 통해 자체적으로 처리 할 수없는 심각한 조건이고, "예외"는 프로그램의 코드를 통해 처리 할 수있는 예외적인 상황이다.

예외가 발생하는 상황

아래의 예시는 사용자로부터 두 수를 입력 받아서 나눗셈 연산을 하는 코드이다.

x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')
z = x / y
print(f'{x} / {y} = {z}')

그럼 아래와 같은 에러 메시지가 출력된다.

TypeError: unsupported operand type(s) for /: 'str' and 'str'
  • input 함수는 사용자로부터 입력받은 값을 str 타입으로 반환해주는 함수이다.
  • 위의 코드를 보면 문자열과 문자열끼리 나눗셈 연산을 한다.
  • 문자열과 문자열끼리의 나눗셈을 연산하는 것은 논리적으로 불가능하다.

그래서 아래와 같이 코드를 수정했다.

x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')
z = int(x) / int(y)
print(f'{x} / {y} = {z}')

그런데 여기서 분모값인 y0 이 된다면 다음과 같은 에러 메시지가 출력된다.

첫번째 수를 입력하세요:1
두번째 수를 입력하세요:0
Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 3, in <module>
    z = int(x) / int(y)
ZeroDivisionError: division by zero
  • 어떠한 수를 0 으로 나누는 것은 논리적으로 불가능하다.
  • 따라서 위의 코드를 실행해보면 ZeroDivisionError 라는 에러 메시지가 출력된다.

이번에는 0 을 나눌 수 없도록 다음과 같이 수정했다.

x = input('첫번째 수를 입력하세요:')
x = int(x)

y = input('두번째 수를 입력하세요:')
y = int(y)

# 'y'가 '0'이면 '1'을 대입시킨다.
if y == 0:
    y = 1

z = x / y
print(f'{x} / {y} = {z}')
  • 위의 코드는 무려 두번에 걸쳐 코드를 수정했기에, 분명 문제가 없기 때문에 잘 동작해야 한다.
  • 그러나, 사용자가 실수로 숫자가 아니라 이상한 문자를 입력한다면 다음과 같은 에러가 발생한다.
첫번째 수를 입력하세요:a
두번째 수를 입력하세요:b
Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 3, in <module>
    z = int(x) / int(y)
ValueError: invalid literal for int() with base 10: 'a'
  • 즉, 프로그래머가 의도치 않은 결과가 발생하는, 이른바 버그(bug) 가 발생하는 것을 알 수 있다.
  • 또한, 수많은 예외적인 상황들을 전부 if-else 문으로 해결할 수가 없다.
  • 왜냐하면 프로그래머가 모든 예외적인 상황들을 예측 하기에는 분명 한계가 있기 때문이다.
  • 뿐만 아니라, if-else 문으로 모든 예외 상황을 처리하려고 하면 그만큼 코드의 양도 방대해진다.

try-except-finally 문

  • try-except 구문은 이러한 문제점을 해결해주는 문법적인 요소이다.
  • 사용하는 방법은 다음과 같다.
try:
    ...
except [발생 오류[as 오류 메시지 변수]]:
    ...

예시

아래의 예시는 위의 예제를 if-else 문 대신 try-except 문으로 대체한 예시이다.

x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')

try:
    x = int(x)
    y = int(y)
    z = x / y

except ZeroDivisionError as e:
    # 에러 메시지를 출력해서 확인할 수 있다.
    print(e)
    y = 1

except ValueError as e2:
    print(e2)
    print('정수를 입력해야 합니다.')
    x = 1
    y = 1

print(f'{x} / {y} = {z}')
  • try-except 구문의 마지막에 finally 절을 추가할 수 있다.
  • finally 절은 try 문 수행 도중 예외 발생 여부에 상관없이 항상 수행된다.
  • 주로 finally 절은 사용한 리소스를 close 해야 하는 상황에서 많이 사용한다.
    • ex. 파일 입출력
x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')

try:
    x = int(x)
    y = int(y)
    z = x / y

except ZeroDivisionError as e:
    # 에러 메시지를 출력해서 확인할 수 있다.
    print(e)
    y = 1

except ValueError as e2:
    print(e2)
    print('정수를 입력해야 합니다.')
    x = 1
    y = 1

finally:
    print(f'{x} / {y} = {z}')

오류를 명시하지 않고 단순히 try-except 구문만 사용하는 경우, 오류의 종류와는 상관없이 오류가 발생하면 except 블록을 수행한다.

x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')

try:
    x = int(x)
    y = int(y)
    z = x / y

except:
    z = 0

finally:
    print(f'{x} / {y} = {z}')

혹은 아래처럼 Exception 이라고 명시하는 경우, 위와 같은 결과를 낼 수 있다.

x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')

try:
    x = int(x)
    y = int(y)
    z = x / y

except Exception as e:
    print(e)
    z = 0

finally:
    print(f'{x} / {y} = {z}')

아래처럼 pass 키워드를 사용해 단순히 오류를 회피하기 위한 목적으로도 사용할 수 있다.

x = input('첫번째 수를 입력하세요:')
y = input('두번째 수를 입력하세요:')

try:
    x = int(x)
    y = int(y)
    z = x / y

except:
    pass

파일 입출력의 경우

  • 파일 입출력을 하는 경우, 프로그램에서 처리해야 하는 파일의 위치가 해당 디렉터리에 존재하지 않거나 용량이 꽉차서 파일을 생성할 수 없는 경우가 대표적이다.
  • 이런 외부적인 요인으로 예외가 발생하는 경우에 try-except 구문을 통해 해결할 수 있다.
file = None

try:
    file = open('test.txt')
    lines = file.readlines()
    for line in lines:
        print(line)

except FileNotFoundError as e:
    print(e)
    print('해당 파일이 존재하지 않습니다.')

finally:
    # 예외와는 상관없이 필수적으로 해줘야 하는 작업을 'finally' 절에서 해준다.
    if file:
        file.close()

예외 발생시키기

assert

  • assert 키워드는 뒤의 조건이 True가 아니면 AssertError 를 발생시키는 키워드이다.
  • 사용하는 방법은 assert <조건식>, <에러 메시지(생략하면 아무런 메시지도 출력되지 않는다.)> 와 같이 사용한다.

예시

아래의 코드를 실행하면 다음과 같은 결과를 확인할 수 있다.

a = 3
assert a == 2

결과

Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 2, in <module>
    assert a == 2
AssertionError

예외가 발생할 경우, 예외 메시지를 출력하도록 할 수 있다.

a = 3
assert a == 2, "'a'의 값이 2가 아닙니다."

결과

Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 2, in <module>
    assert a == 2, "'a'의 값이 2가 아닙니다."
AssertionError: 'a'의 값이 2가 아닙니다.

assert 키워드를 사용하는 이유

  • assert 키워드는 개발자가 프로그램을 만드는 과정에 관여한다.
  • 원하는 조건의 변수 값을 보증받을 때까지 assert 키워드로 테스트 할 수 있다.
    • 즉, 코드를 테스트 하는 용도로 많이 사용된다.
  • 이는 단순히 에러를 찾는것이 아니라 값을 보증하기 위해 사용된다.
  • 예를 들어, 어떤 함수는 성능을 높이기 위해 반드시 정수만을 입력받아 처리하도록 만들 수 있다고 하자.
  • 이런 함수를 만들기 위해서는 반드시 함수에 정수만 들어오는지 확인할 필요가 있다.
  • 이를 위해 if 문을 사용할 수도 있고 앞서 살펴본 try-except 구문을 사용할 수도 있지만, assert 키워드로 가정 설정문을 사용하는 방법도 있다.
  • 이처럼 실수를 가정해 값을 보증하는 방식으로 코딩 하기 때문에 이를 방어적 프로그래밍이라 부른다.

raise

  • raise 키워드는 assert 키워드와는 달리, 무조건 예외를 발생시키는 키워드이다.
  • 사용하는 방법은 raise <예외 종류(생략 가능)> 와 같이 사용한다.

예시

아래의 코드를 실행하면 무조건 예외가 발생한다.

def f(x):
    raise

f(10)

결과

Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 4, in <module>
    f(10)
  File "C:/Users/iamjjintta/test.py", line 2, in f
    raise
RuntimeError: No active exception to reraise
  • assert 키워드와 마찬가지로 예외가 발생할 때 예외의 종류를 직접 정할 수 있다.
  • 단, 발생시킬 예외는 Exception 객체를 상속받은 예외여야만 한다.
def f(x):
    raise ValueError

f(10)

결과

Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 4, in <module>
    f(10)
  File "C:/Users/iamjjintta/test.py", line 2, in f
    raise ValueError
ValueError

assert 키워드처럼 메시지가 같이 출력되도록 하고 싶은 경우에는 다음과 같이 작성한다.

def f(x):
    raise Exception('예외 발생')

f(10)

결과

Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 4, in <module>
    f(10)
  File "C:/Users/iamjjintta/test.py", line 2, in f
    raise Exception('예외 발생')
Exception: 예외 발생

예외 만들기

  • 앞서 raise 키워드에서 살펴보았듯, 예외를 발생시키는 경우, 예외의 종류를 지정할 수 있다고 서술했다.
  • 기존에 정의된 예외뿐만 아니라 자신이 직접 예외 객체를 만들어서 정의할 수 있다.
  • 예외 객체를 만들기 위해서는 BaseException 이라는 클래스를 상속받은 객체여야만 한다.
# 'BaseException' 클래스를 상속받은 예외 클래스 'MyException' 정의
class MyException(BaseException):
    pass

def f(x):
    # 정의한 예외 발생시키기
    raise MyException('예외 발생')

f(10)

결과

Traceback (most recent call last):
  File "C:/Users/iamjjintta/test.py", line 7, in <module>
    f(10)
  File "C:/Users/iamjjintta/test.py", line 5, in f
    raise MyException('예외 발생')
MyException: 예외 발생