-
Notifications
You must be signed in to change notification settings - Fork 369
#06. 로깅, 에러 핸들링 (Logging & Error Handling)
- 더 나은 소프트웨어를 고민하시는 분
- 소프트웨어를 실제로 운영 환경에서 띄우실 분
- 개발 환경과 다르게 운영 환경에서는 소프트웨어를 안전하고 지속적으로 운영되어야 합니다.
- 만약 운영 중인 소프트웨어가 중단된다면, 운영 주체(기업, 개인 등)에게 큰 손실을 입힐 수 있습니다.
- 혼자서 개발하는 소프트웨어와 다르게, 함께 개발해야 하는 소프트웨어는 예측 가능하게 만들어져야 합니다.
-
소프트웨어의 동작 상태를 운영자가 잘 확인하기 위해서
로그
를 잘 심어두는 습관이 필요합니다. 운영자 입장에서 로그는 소프트웨어의 상태를 확인할 수 있는 주요 수단이 됩니다. 따라서 로그를 얼마나 잘 심냐에 따라 소프트웨어 운영 비용이 결정될 수 있습니다. 장애가 발생했지만 로그가 보이지 않거나, 주요 동작에 대한 로그가 보이지 않는다면 소프트웨어를 운영/유지보수 하는 입장에서는 앞이 깜깜할 것입니다. -
일반적으로 프로그래밍 언어는 기본적인 로그 출력 메서드를 제공합니다. 파이썬에서는
print
을 사용해 기본적인 문자열을 출력합니다. 또한 기본 내장 모듈인logging
을 활용해서 각 로그의 레벨을 설정해주고 커스텀하게 로그를 출력할 수 있습니다.# print print("안녕하새우1") # logging import logging logger = logging.getLogger() logger.setLevel(logging.INFO) logger.info("안녕하새우2")
-
해당 아티클을 통해 로깅을 더 잘하기 위한 방법들을 알아봅시다(핵심 내용들을 가볍게 간추려 봤습니다) 이 중에서 중요하다고 생각하는 부분들은 Bold 처리를 해놓았으니 참고해주시면 됩니다.
- 로그를 확인할 운영자들을 위해 표준 라이브러리 또는 시스템 API 호출을 사용하는 게 좋습니다. 즉 팀, 회사 차원에서 세운 표준을 따라가는 것이 좋습니다.
- 스스로 로깅 모듈을 따로 개발할 필요는 없습니다 (은총알은 없습니다)
- 일반적으로 로그의 중요도에 따라 레벨을 설정할 수 있습니다.
- 각 상황에 맞게 로그에 레벨을 설정해주면, 소프트웨어의 기본 로그 레벨 설정에 따라 출력되는 로그를 제한할 수 있습니다(Info를 기본 레벨로 설정하면 Debug, Trace는 보이지 않습니다.)
- 💡 가장 쉽게 적용해볼 수 있는 것은 개발 환경에서만 보이는 로그는 Debug 레벨, 일반적인 정보를 보여주는 로그는 Info 레벨, 장애를 발생시킬 수 있는 로그는 Error 레벨로 분류해주면 좋습니다.
- 로그를 성격에 맞게 분류해주기 위해 로그 카테고리를 적용해주는 게 좋습니다 (e.g., 모델 학습 로그, 모델 추론 로그를 분류하기)
- 💡 일반적으로 파이썬의 logging은 카테고리를 설정해주는 기능이 따로 없습니다. 따라서 카테고리를 넣으려면
json
포맷 같이 구조화된 로깅(Structured Logging)을 적용하는 걸 추천드립니다.
- 로깅을 할 때 가장 중요한 건 바로 로그를 봤을 때 상황을 정확히 파악할 수 있어야 합니다. 따라서 이해할 수 있도록 로그 메시지를 작성하는 것이 중요합니다.
- 해당 로그에 대한 문맥(Context)를 잘 포함할 수 있도록 작성합니다.
- 다른 로그 메시지와 의존 관계가 생기지 않도록 주의합니다 (비동기, 멀티 스레드 환경에서 문제가 발생할 수 있습니다)
- 영어는 전부 ASCII 코드이기 때문에 문자열 인코딩 관점에서 나은 선택입니다.
- 💡 애매하게 영어로 작성을 하는 것보다, 한국어로 명확하게 로그 메시지를 작성하는 게 더 나은 선택일 때가 많습니다.
- 로그 메시지에는 해당 로그에 대한 문맥(Context)을 포함하도록 하는 것이 좋습니다
# BAD
Transaction failed
User operation succeeds
java.lang.IndexOutOfBoundsException
# Good
Transaction 2346432 failed: cc number checksum incorrect
User 54543 successfully registered e-mail user@domain.com
IndexOutOfBoundsException: index 12 is greater than collection size 10
- Context를 더 쉽게 담을 수 있도록
MDC(Mapped Diagnostic Context)
를 사용하는 것을 추천합니다
class UserRequest {
...
public void execute(int userid) {
MDC.put("user", userid);
// ... all logged message now will display the user=<userid> for this thread context ...
log.info("Successful execution of request");
// user request processing is now finished, no need to log our current user anymore
MDC.remote("user");
}
}
- 사람이 잘 읽을 수 있게 하는 것만큼, 기계가 잘 읽을 수 있도록 하는 것도 중요합니다
# BAD (파싱하기 어렵다. regex를 활용해야 함)
2013-01-12 17:49:37,656 [T1] INFO c.d.g.UserRequest User 1334563 plays 4 of spades in game 23425656
# Good (파싱하기 쉽다. json parsing)
2013-01-12 17:49:37,656 [T1] INFO c.d.g.UserRequest User plays {'user':1334563, 'card':'4 of spade', 'game':23425656}
- 표준 date/time format를 사용합니다 (ISO8601)
- UTC나 현지시간 offset을 더한 Timestamp을 추가합니다.
- 로그 레벨을 잘 설정해줍니다
- 서로 다른 수준의 로그를 서로 다른 대상으로 분할하여 세밀하게 로그를 관리합니다
- Exception을 포함한 로그는 StackTrace를 담도록 합니다
- 멀티 스레드 환경에서는 스레드 이름을 포함하도록 합니다 (MDC)
- 로그를 너무 많이 추가하면, 제대로 된 값을 확인하기 어려울 수 있습니다.
- 로그를 너무 적게 추가하면, 트러블슈팅 시 문제가 발생할 수 있습니다.
- 팁 : 개발 도중에는 가능한 많이 로그를 심어두고, 운영 환경에 올리기 전에 로그를 잘 정리하는 것이 낫습니다
- 해당 로그를 확인하는 주체(ops 팀, qa 팀, 같은 개발팀 등)에 따라 로그의 메시지, 문맥, 카테고리, 레벨 등이 달라질 수 있습니다.
- 로그 메시지는 트러블슈팅 뿐만 아니라 다양한 용도에서 활용할 수 있습니다
- Auditing : 사내에서 시스템 사용자의 행동을 추적하는 용도로 사용 가능합니다
- Profiling : Timestamp가 포함된 로그는 프로그램을 프로파일링(성능 측정, 메트릭 확인 등)을 할 때 용이합니다
- Statistics : 쌓인 로그들은 통계 분석이 가능합니다
- 특정 공급 업체에 락인되지 않도록 합니다
- 비밀번호, 보안정보 등의 정보는 로그 메시지에 포함시키면 안 됩니다.
-
에러를 발생할 가능성이 있는 코드는
에러 핸들링
을 잘 해주어 예상치 못한 상황에서 소프트웨어가 종료되면 안 됩니다. 이번 파트에서는 에러 핸들링 중에서 코드 차원에서 해주어야 할예외 처리
에 더 집중해 보겠습니다. -
파이썬에서는 예외 처리를 잘하기 위해
try/except
문법을 제공해줍니다. 만약 에러가 발생할 수 있는 코드가 있다면 try/except를 적용해줘서 소프트웨어의 종료를 막는 것이 중요합니다.
try:
i_can_occur_error()
...
except ValueError as e:
# 에러 처리 로직
- 해당 아티클을 통해 예외 처리를 더 잘하는 방법에 대해 알아봅시다
오류 코드를 사용하게 되면 상단에 오류인지 확인하는 불필요한 로직이 들어가게 됩니다. 오류의 범주에 들어가지 않은 상태를 나타내는 것이 아니라면, 예외(Exception)로 명시적으로 에러 처리를 표현해주는 게 좋습니다.
-
as-is
from enum import Enum class ErrorCodes(Enum): VALUE_ERROR="VALUE_ERROR" def we_can_raise_error(): ... return ERROR_CODES.VALUE_ERROR def use_ugly_function(): result = we_can_occur_error() if result == ErrorCodes.VALUE_ERROR: # 처리 코드 ...
-
to-be
def we_can_raise_error(): if ... raise ValueError("에러 발생") def use_awesome_function(): try: we_can_occur_error() ... except ValueError as e: # 에러 처리 로직
-
기본 Exception만 쓰기보단 내장된 built in Exception을 잘 활용하면 좋습니다.
-
상황에 맞게 Custom Exception을 만들어 사용하는 것도 좋습니다
class CustomException(Exception): ... class WithParameterCustomException(Exception): def __init__(self, msg, kwargs): self.msg = msg self.kwargs = kwargs def __str__(): return f"message {self.msg} with parameter {self(self.kwargs)}" raise WithParameterCustomException("문제가 있습니다", {"name": "grab"})
-
에러를 포착했다면 잘 핸들링해줘야 합니다.
def we_can_raise_error(): ... raise Exception("Error!") # BAD: 에러가 났는지 확인할 수 없게 됩니다. def use_ugly_function1(): try: we_can_raise_error() ... except: pass # BAD: 로그만 남긴다고 끝이 아닙니다. def use_ugly_function2(): try: we_can_raise_error() ... except Exception as e: print(f"에러 발생{e}") # GOOD def use_awesome_function(): try: we_can_raise_error() ... except Exception as e: logging.error(...) # Error Log 남기기 notify_error(...) # 예측 불가능한 외부 I/O 이슈라면 회사 내 채널에 알리기(이메일, 슬랙 etc) raise OtherException(e) # 만약 이 함수를 호출하는 다른 함수에서 추가로 처리해야 한다면 에러를 전파하기 finally: ... #에러가 발생하더라도 항상 실행되어야 하는 로직이 있다면 finally 문을 넣어주기
-
에러 핸들링을 모을 수 있으면 한곳으로 모읍니다. 보통 같은 수준의 로직을 처리한다면 한 곳으로 모아서 처리하는 게 더 에러를 포착하기 쉽습니다.
-
as-is
def act_1(): try: we_can_raise_error1() ... except: #handling def act_2(): try: we_can_raise_error2() ... except: #handling def act_3(): try: we_can_raise_error3() ... except: #handling # 에러가 날 지점을 한눈에 확인할 수 없습니다. # act_1이 실패하면 act_2가 실행되면 안 된다면? 핸들링하기 어려워집니다. def main(): act_1() act_2() act_3()
-
to-be
def act_1(): we_can_raise_error1() ... def act_2(): we_can_raise_error2() ... def act_3(): we_can_raise_error3() ... # 직관적이며 에러가 날 지점을 확인하고 처리할 수 있습니다. # 트랜잭션같이 한 단위로 묶여야하는 처리에도 유용합니다. def main(): try: act_1() act_2() act_3() except SomeException1 as e1: ... except SomeException2 as e2: ... except SomeException2 as e3 ... finally: ...
-
https://www.dataset.com/blog/the-10-commandments-of-logging/
https://yansfil.github.io/awesome-class-materials/2.clean-code/5. 에러핸들링.html#에러-핸들링-잘하기