
소프트웨어 개발에서 ‘예외(Exception)’는 피할 수 없는 현실입니다. 간단한 입력 오류부터 예상치 못한 네트워크 장애까지, 프로그램이 마주치는 수많은 변수들은 언제든지 오류를 일으킬 수 있습니다. 하지만 중요한 것은 ‘오류가 발생했는가’가 아니라, ‘오류를 어떻게 처리했는가’입니다.
이 글에서는 Python 언어를 중심으로 에러 처리(try-except)와 로깅(logging) 전략을 단계적으로 살펴봅니다. 단순한 문법 설명에 그치지 않고, 실전 사례와 함께 구체적인 코드를 통해 실무에서 바로 활용할 수 있도록 구성했습니다.
에러 없는 코드보다 중요한 것은, 에러에 강한 코드입니다. 지금부터 Python에서의 예외 처리와 로깅 전략을 함께 탐색해 보겠습니다.
목차
- 예외가 코드의 품질을 결정짓는다
- Python에서의 예외 처리란 무엇인가?
- 다양한 try-except 패턴의 활용
- 실전 예제: 흔히 마주치는 예외 처리 시나리오
- 로깅의 필요성과 기본 개념
- logging 모듈 실전 활용법
- 예외 처리와 로깅의 통합 전략
- 유지보수성과 확장성을 고려한 고급 전략
- 결론: 예외 없는 코드보다 예외에 강한 코드
1. 예외가 코드의 품질을 결정짓는다
여러분이 개발하는 프로그램이 사용자의 입력 오류나 예기치 못한 파일 손상, 혹은 네트워크 지연과 같은 외부 요인 앞에서 얼마나 유연하게 대처할 수 있을까요? 단순히 멈춰버리는 프로그램은 신뢰를 잃기 쉽습니다.
프로그램에서 발생하는 예외는 단순한 버그가 아닙니다. 이는 시스템의 안정성과 사용자 경험의 질을 좌우하는 핵심적인 품질 지표입니다. 아무리 기능이 많고 빠른 코드라도, 에러가 났을 때 무력해지는 소프트웨어는 좋은 코드라 할 수 없습니다.
현대 소프트웨어 개발에서는 예외 상황을 ‘미리 예상하고 대비하는 설계’가 중요합니다. 그리고 그 핵심에는 Python의 try-except
구문과 logging
모듈이 있습니다. 이 두 가지 도구를 적절히 조합하면, 예외 상황을 통제하고 문제 해결을 위한 단서를 남길 수 있습니다.
이제 다음 단락에서는 예외 처리의 기본 개념과 구조부터 차근차근 살펴보겠습니다.
2. Python에서의 예외 처리란 무엇인가?
코드 실행 중 발생하는 예외(Exception)는 프로그램의 흐름을 중단시키는 돌발 상황입니다. 예외는 사용자의 잘못된 입력, 네트워크 장애, 파일 접근 실패 등 다양한 상황에서 발생할 수 있으며, 이러한 예외를 무시하면 프로그램은 강제 종료되어 버립니다.
Python은 이러한 예외 상황을 우아하게 처리할 수 있도록 try-except
구문을 제공합니다. 이 구문은 문제 발생 가능성이 있는 코드를 try
블록 안에 넣고, 실제 문제가 발생했을 때 어떻게 대응할지를 except
블록에 정의하는 방식입니다.
try:
result = 10 / 0
except ZeroDivisionError:
print("0으로 나눌 수 없습니다.")
위 코드는 0으로 나누는 오류(ZeroDivisionError
)를 감지하고, 프로그램이 중단되지 않도록 방지합니다. 만약 try
블록 내의 코드가 정상적으로 실행된다면 except
블록은 실행되지 않습니다.
예외(Exception) vs 오류(Error)
Python에서 오류(Error)는 프로그램의 실행 자체가 불가능한 심각한 문제를 의미하며, 예외(Exception)는 실행 중 발생할 수 있는 예외적인 상황을 의미합니다. 예를 들어 문법 오류(SyntaxError)는 개발자가 수정해야 할 오류이고, 파일이 존재하지 않는 상황은 런타임 예외입니다.
Python의 예외 처리 구조
Python의 기본 예외 처리 구문은 다음과 같은 구조로 이루어집니다.
try:
# 예외가 발생할 수 있는 코드
except SomeException:
# 예외가 발생했을 때 실행할 코드
else:
# 예외가 발생하지 않았을 때 실행할 코드
finally:
# 예외 발생 여부와 관계없이 무조건 실행되는 코드
try 블록은 감시 영역이며, except 블록은 특정 예외가 발생했을 때의 대응 코드입니다. else는 예외가 발생하지 않은 경우에만 실행되며, finally는 예외 발생 여부와 관계없이 반드시 실행되어야 할 마무리 작업을 정의할 때 유용합니다.
다음 단락에서는 이러한 예외 처리 구문을 다양한 패턴으로 활용하는 방법에 대해 자세히 살펴보겠습니다.
3. 다양한 try-except 패턴의 활용
Python의 예외 처리는 단순한 오류 대응을 넘어서, 상황에 맞게 다양한 방식으로 응용할 수 있습니다. 이 장에서는 try-except 구문을 실제 코드에서 어떻게 다양하게 활용할 수 있는지를 세부 패턴별로 나누어 살펴보겠습니다.
1) 기본적인 예외 처리
가장 단순한 형태의 예외 처리는 다음과 같습니다. 특정 예외를 지정하고, 그에 대한 대응 로직을 작성합니다.
try:
num = int(input("숫자를 입력하세요: "))
except ValueError:
print("유효한 숫자가 아닙니다.")
사용자가 숫자가 아닌 문자를 입력할 경우, ValueError
가 발생하고 프로그램은 친절하게 대응합니다.
2) 복수 예외 처리
하나의 코드 블록에서 여러 가지 예외가 발생할 수 있다면, 튜플 형태로 except
에서 동시에 처리할 수 있습니다.
try:
val = float(input("숫자를 입력하세요: "))
result = 10 / val
except (ValueError, ZeroDivisionError):
print("잘못된 입력이거나 0으로 나눌 수 없습니다.")
위 예제는 두 가지 예외 상황(ValueError
와 ZeroDivisionError
)을 한 번에 처리합니다.
3) 예외 메시지 객체 활용
as
키워드를 사용하면 발생한 예외 객체를 참조할 수 있어, 더 자세한 정보를 로깅하거나 출력할 수 있습니다.
try:
f = open("없는파일.txt", "r")
except FileNotFoundError as e:
print(f"파일 오류 발생: {e}")
이 방식은 나중에 로그에 예외 메시지를 남기거나, 예외의 속성에 접근할 때 유용합니다.
4) else 블록의 활용
else
블록은 try
블록에서 예외가 발생하지 않았을 때만 실행됩니다. 주로 정상적인 흐름을 별도로 분리하고 싶을 때 유용합니다.
try:
number = int(input("숫자를 입력하세요: "))
except ValueError:
print("잘못된 숫자입니다.")
else:
print(f"입력한 숫자는 {number}입니다.")
5) finally 블록의 활용
finally
는 예외 발생 여부에 관계없이 무조건 실행됩니다. 파일 닫기, 연결 종료 등의 마무리 작업에 사용됩니다.
try:
f = open("sample.txt", "r")
content = f.read()
except FileNotFoundError:
print("파일이 존재하지 않습니다.")
finally:
f.close()
print("파일 객체를 닫았습니다.")
예외가 발생해도 finally
블록이 실행되기 때문에, 자원 누수를 방지할 수 있습니다.
이처럼 try-except
구문은 다양한 방식으로 구성될 수 있으며, 예외 발생 상황을 정밀하게 제어할 수 있는 도구입니다. 다음 장에서는 이러한 패턴을 실제 업무 환경에서 어떻게 활용할 수 있는지, 구체적인 예제를 통해 살펴보겠습니다.
4. 실전 예제: 흔히 마주치는 예외 처리 시나리오
실제 개발 현장에서 예외 처리는 이론과는 다르게 복잡하고 다양한 상황을 수반합니다. 이번 단락에서는 자주 마주치는 현실적인 코드 사례를 통해 어떻게 예외를 다루는지 구체적으로 살펴보겠습니다.
1) 파일 입출력에서의 예외 처리
파일을 읽거나 쓸 때 가장 흔한 예외는 FileNotFoundError
, PermissionError
등이 있습니다. 다음은 안전한 파일 열기 예제입니다.
filename = "data.txt"
try:
with open(filename, 'r') as file:
data = file.read()
except FileNotFoundError:
print(f"{filename} 파일을 찾을 수 없습니다.")
except PermissionError:
print(f"{filename} 파일에 접근 권한이 없습니다.")
else:
print("파일 내용을 성공적으로 읽었습니다.")
with
문은 파일을 안전하게 열고 닫아주는 Python의 문법이며, 예외 발생 여부와 관계없이 자원을 자동으로 정리해줍니다.
2) 외부 API 호출 시의 네트워크 예외
인터넷 연결 문제, 응답 지연, 타임아웃 등 외부 API와의 통신에서는 다양한 네트워크 예외가 발생할 수 있습니다. Python의 requests
라이브러리를 활용한 예시를 보겠습니다.
import requests
try:
response = requests.get("https://example.com/api", timeout=5)
response.raise_for_status()
except requests.exceptions.Timeout:
print("요청이 타임아웃되었습니다.")
except requests.exceptions.HTTPError as e:
print(f"HTTP 오류 발생: {e}")
except requests.exceptions.RequestException as e:
print(f"요청 중 문제가 발생했습니다: {e}")
else:
print("정상적으로 응답 받음.")
raise_for_status()
는 응답 코드가 200이 아닐 경우 HTTPError
예외를 발생시킵니다. 이는 API 통신의 실패 원인을 보다 명확히 파악할 수 있게 합니다.
3) 사용자 입력 처리에서의 예외
사용자 입력은 항상 오류 가능성을 동반합니다. 입력값의 형식이나 유효성 검사를 통해 예외를 처리해야 합니다.
try:
age = int(input("나이를 입력하세요: "))
if age < 0:
raise ValueError("나이는 음수가 될 수 없습니다.")
except ValueError as e:
print(f"입력 오류: {e}")
else:
print(f"당신의 나이는 {age}살입니다.")
이 예제는 int()
변환 오류뿐만 아니라, 논리적 오류(음수 입력)도 함께 검증하고 있습니다.
4) JSON 데이터 파싱 오류
외부 시스템으로부터 JSON 데이터를 수신했을 때, 형식이 잘못되어 파싱이 실패할 수 있습니다.
import json
response = '{"name": "Amy", "age": 30' # 괄호 누락된 잘못된 JSON
try:
data = json.loads(response)
except json.JSONDecodeError as e:
print(f"JSON 파싱 실패: {e}")
else:
print(data)
이처럼 JSON 파싱에서도 구체적인 예외(JSONDecodeError
)를 명확히 지정해 줌으로써, 오류 원인을 쉽게 진단할 수 있습니다.
실제 상황에 맞는 예외 처리는 코드의 복원력과 유지보수성을 향상시킵니다. 다음 단락에서는 이런 예외 상황들을 어떻게 효과적으로 기록하고 추적할 수 있는지, 로깅(logging)의 개념과 필요성에 대해 자세히 다뤄보겠습니다.
5. 로깅(logging)의 필요성과 기본 개념
예외를 처리하는 것만큼이나 중요한 것이 바로 무슨 일이 일어났는지를 기록하는 것입니다. 로깅(logging)은 단순한 메시지 출력이 아닌, 시스템의 동작 내역을 체계적으로 기록하고 분석하기 위한 핵심 수단입니다.
왜 print()가 아닌 logging인가?
많은 초보 개발자들이 디버깅을 위해 print()
를 사용합니다. 하지만 다음과 같은 이유로 실무에서는 반드시 logging을 사용해야 합니다.
- 레벨 기반 출력: 로그의 중요도에 따라 출력 수준을 설정할 수 있습니다.
- 파일 저장: 로그를 파일로 저장하여, 나중에 분석 및 감사가 가능합니다.
- 운영 환경 적합: 배포 환경에서는 콘솔 출력 대신 로그 파일 기록이 일반적입니다.
- 구조화된 포맷: 시간, 레벨, 메시지 등 체계적인 로그 기록이 가능합니다.
logging 모듈의 핵심 구성요소
Python의 logging
모듈은 표준 라이브러리로 제공되며, 다음과 같은 요소들로 구성됩니다.
구성 요소 | 설명 |
---|---|
Logger | 애플리케이션 코드에서 로그를 생성하는 객체 |
Handler | 로그를 출력할 위치를 결정 (콘솔, 파일, 네트워크 등) |
Formatter | 로그 메시지의 출력 형식을 정의 |
Level | 로그의 심각도 수준 (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
기본 로그 레벨
로그 레벨은 로그 메시지의 중요도를 나타냅니다. 일반적으로 다음과 같은 레벨이 존재합니다.
DEBUG
– 상세한 디버깅 정보INFO
– 정상적인 흐름을 알리는 메시지WARNING
– 경고. 동작은 가능하지만 주의가 필요한 상황ERROR
– 오류. 기능이 일부 실패함CRITICAL
– 심각한 오류. 프로그램이 더 이상 실행 불가능한 상태
로깅은 단순한 출력 이상의 전략적 도구입니다. 이제 다음 단락에서는 logging
모듈을 실제 코드에 어떻게 구현하고 설정할 수 있는지 구체적으로 다뤄보겠습니다.
6. logging 모듈 실전 활용법
Python의 logging
모듈은 매우 유연하고 확장성이 뛰어난 시스템입니다. 이 장에서는 실제로 logging을 어떻게 설정하고 활용하는지 다양한 예제를 통해 설명합니다.
1) 기본 로그 출력
아래는 가장 간단한 logging 사용 예제입니다. basicConfig()
를 통해 기본 설정을 한 뒤, 로그 메시지를 출력합니다.
import logging
logging.basicConfig(level=logging.INFO)
logging.info("프로그램이 시작되었습니다.")
level=logging.INFO
는 INFO 이상 수준의 로그만 출력한다는 뜻입니다.
2) 로그 레벨별 메시지 출력
다양한 상황에 따라 적절한 로그 레벨을 사용하면, 디버깅과 운영 환경에서 큰 도움이 됩니다.
logging.debug("디버그용 상세 정보")
logging.info("일반 정보 메시지")
logging.warning("경고: 처리 속도 저하 감지")
logging.error("에러 발생: 파일 없음")
logging.critical("치명적인 오류: 시스템 종료")
3) 로그 포맷 지정하기
출력되는 로그 형식을 커스터마이즈하면 로그 분석이 쉬워집니다. 예를 들어 로그에 시간, 레벨, 메시지를 포함하고 싶다면 다음과 같이 설정합니다.
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
로그는 다음과 같은 형식으로 출력됩니다:
2025-05-09 15:42:10 [INFO] 프로그램이 시작되었습니다.
4) 파일 로그 저장
운영 환경에서는 로그를 파일로 저장해야 할 필요가 많습니다. 아래는 로그를 app.log
파일에 기록하는 예제입니다.
logging.basicConfig(
filename="app.log",
level=logging.WARNING,
format="%(asctime)s [%(levelname)s] %(message)s"
)
이 설정은 WARNING 이상의 로그를 파일로 저장합니다. DEBUG나 INFO 수준의 로그는 저장되지 않습니다.
5) 로그 설정을 함수/클래스에 통합하기
복잡한 애플리케이션에서는 로깅 구성을 함수화하여 재사용성을 높일 수 있습니다.
def setup_logger(name, level=logging.INFO, file=None):
logger = logging.getLogger(name)
logger.setLevel(level)
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
if file:
file_handler = logging.FileHandler(file)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
logger = setup_logger("myapp", logging.DEBUG, "myapp.log")
logger.info("로거 설정 완료")
이렇게 구성하면 모듈 단위로 로거를 관리할 수 있으며, 출력 포맷이나 저장 위치 등을 유연하게 조정할 수 있습니다.
이제 로깅을 설정하는 방법을 익혔다면, 다음은 예외 처리 구문과 logging을 결합하여 더 견고한 프로그램 구조를 만드는 전략에 대해 살펴보겠습니다.
7. 예외 처리와 로깅의 통합 전략
실무에서 가장 중요한 것은 예외를 처리하면서 동시에 문제를 추적할 수 있는 정보도 남기는 것입니다. 단순히 예외를 잡아내는 것에 그치지 않고, logging
모듈을 통해 오류 상황을 기록함으로써 디버깅과 유지보수가 가능해집니다.
1) except 블록 내에서 로그 기록하기
예외가 발생했을 때 단순히 메시지를 출력하기보다는, 로그로 남겨야 나중에 분석이 가능합니다.
import logging
logging.basicConfig(
filename="errors.log",
level=logging.ERROR,
format="%(asctime)s [%(levelname)s] %(message)s"
)
try:
result = 10 / 0
except ZeroDivisionError as e:
logging.error("0으로 나누기 시도됨: %s", e)
이 예제는 예외 발생 시 로그 파일에 오류 정보를 남기며, 메시지에는 예외 객체의 문자열이 포함됩니다.
2) 예외 전파와 재처리 전략
때때로 예외를 잡은 후, 단순히 무시하지 않고 재처리하거나 상위 계층으로 재전파해야 할 수도 있습니다. 이럴 때도 로그를 남기는 습관이 중요합니다.
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
logging.warning("0으로 나눌 수 없습니다. 기본값 0을 반환합니다.")
return 0
또는 예외를 처리한 뒤, 다시 발생시켜 호출자에게 책임을 위임할 수도 있습니다.
def read_file(path):
try:
with open(path, 'r') as file:
return file.read()
except FileNotFoundError as e:
logging.error("파일을 찾을 수 없습니다: %s", path)
raise
raise
키워드를 통해 예외를 재발생시키며, 로깅으로 오류 내역은 남겨놓습니다.
3) 사용자에게는 친절하게, 로그에는 상세하게
에러 메시지를 사용자에게 그대로 노출하는 것은 UX 측면에서 바람직하지 않습니다. 하지만 로그에는 가능한 한 많은 디버깅 정보를 남겨야 합니다. 예:
try:
user_age = int(input("나이를 입력하세요: "))
except ValueError as e:
logging.exception("잘못된 사용자 입력 발생")
print("숫자를 정확히 입력해 주세요.")
logging.exception()
은 traceback까지 함께 기록해주므로 매우 유용합니다. 사용자는 간단한 안내만 받고, 개발자는 로그를 통해 문제의 원인을 파악할 수 있습니다.
이처럼 예외 처리와 로깅은 반드시 함께 설계되어야 하며, 이 전략이 잘 구현되어야만 운영 환경에서도 안정적인 시스템을 유지할 수 있습니다. 다음 장에서는 유지보수성과 확장성을 고려한 고급 전략을 살펴보겠습니다.
8. 유지보수성과 확장성을 고려한 고급 전략
작은 프로젝트에서는 단순한 로깅과 예외 처리로도 충분할 수 있지만, 규모가 커질수록 더 체계적이고 유연한 구조가 요구됩니다. 이 장에서는 장기적인 유지보수와 팀 단위 개발을 고려한 고급 전략을 소개합니다.
1) 커스텀 예외 클래스 정의
비즈니스 로직에 특화된 예외 처리를 위해 자체적인 예외 클래스를 정의하는 것이 좋습니다. 이를 통해 예외의 의미를 명확히 하고, 코드 가독성도 높일 수 있습니다.
class InvalidAgeError(Exception):
def __init__(self, age, message="나이 입력이 유효하지 않습니다."):
self.age = age
self.message = message
super().__init__(f"{message} (입력값: {age})")
def process_age(age):
if age < 0:
raise InvalidAgeError(age)
이렇게 정의한 예외는 except InvalidAgeError as e:
형식으로 처리할 수 있으며, 명확한 예외 종류를 기준으로 로깅할 수 있습니다.
2) 로깅 설정 외부화 (.ini 또는 .yaml)
코드 안에 로깅 설정을 직접 작성하면 유지보수가 어렵습니다. 로깅 설정을 외부 파일로 분리하면 환경에 따라 동적으로 조정할 수 있어 매우 유용합니다.
예를 들어 logging_config.ini
파일을 작성합니다:
[loggers]
keys=root
[handlers]
keys=consoleHandler
[formatters]
keys=simpleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)
[formatter_simpleFormatter]
format=%(asctime)s [%(levelname)s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S
Python 코드에서는 다음과 같이 로드합니다:
import logging
import logging.config
logging.config.fileConfig('logging_config.ini')
logger = logging.getLogger()
logger.debug("외부 설정을 사용한 로깅 시작")
이 접근 방식은 환경에 따라 다른 로그 레벨, 출력 형식, 저장 경로 등을 설정할 수 있어 배포 자동화에도 효과적입니다.
3) 대형 프로젝트에서의 로깅 아키텍처 설계 팁
- 모듈별 로거 사용: 각 모듈마다 개별
logger = logging.getLogger(__name__)
선언 - 공통 로깅 유틸리티 작성: 일관된 포맷과 핸들러 설정을 모듈화
- 로그 수준 환경변수화: 운영/개발 환경에 따라 로그 레벨을 조절
- 로그 회전(Rotation):
RotatingFileHandler
등을 활용해 파일 크기 초과 시 자동 백업
이러한 전략은 프로젝트가 커져도 유지보수성과 확장성을 보장하며, 협업 및 배포 환경에서도 유연하게 대응할 수 있도록 돕습니다.
이제 마지막으로, 본문의 내용을 정리하고 전체 메시지를 압축하는 결론으로 이어가겠습니다.
9. 결론: 예외 없는 코드보다 예외에 강한 코드
프로그래밍에서 예외는 피할 수 없는 현실이며, 이를 어떻게 다루느냐가 소프트웨어의 품질을 결정짓습니다. 단순히 예외를 무시하거나 억지로 감추는 것이 아닌, 명확히 인지하고 체계적으로 처리하며 기록하는 일련의 전략이 중요합니다.
이 글에서는 Python의 try-except
구문을 중심으로, 다양한 예외 처리 패턴과 실전 사례, 그리고 logging
모듈을 활용한 로깅 전략까지 심도 있게 다루었습니다. 또한 유지보수와 확장성을 고려한 고급 기술로 커스텀 예외 클래스, 외부 설정 분리, 모듈화 설계 방식도 소개하였습니다.
우리가 궁극적으로 지향해야 할 것은 완벽한 코드가 아니라, 견고한 코드입니다. 즉, 예상치 못한 상황이 닥쳤을 때도 무너지지 않고, 문제를 기록하며 빠르게 복구할 수 있는 시스템을 만드는 것입니다.
코드는 언젠가 실패할 수 있습니다. 그러나 그 실패를 미리 감지하고, 기록하며, 책임 있게 대처하는 시스템은 쉽게 무너지지 않습니다. 이 글을 통해 여러분의 Python 코드가 조금 더 견고하고, 실무에 강한 형태로 다듬어지기를 바랍니다.