Skip to content

#06. 로깅, 에러 핸들링 (Logging & Error Handling)

Sung Yun Byeon edited this page Nov 7, 2022 · 2 revisions

Target User

  • 더 나은 소프트웨어를 고민하시는 분
  • 소프트웨어를 실제로 운영 환경에서 띄우실 분

Why you need to know

  • 개발 환경과 다르게 운영 환경에서는 소프트웨어를 안전하고 지속적으로 운영되어야 합니다.
  • 만약 운영 중인 소프트웨어가 중단된다면, 운영 주체(기업, 개인 등)에게 큰 손실을 입힐 수 있습니다.
  • 혼자서 개발하는 소프트웨어와 다르게, 함께 개발해야 하는 소프트웨어는 예측 가능하게 만들어져야 합니다.

Contents

1. 로그 잘 심기

  • 소프트웨어의 동작 상태를 운영자가 잘 확인하기 위해서 로그를 잘 심어두는 습관이 필요합니다. 운영자 입장에서 로그는 소프트웨어의 상태를 확인할 수 있는 주요 수단이 됩니다. 따라서 로그를 얼마나 잘 심냐에 따라 소프트웨어 운영 비용이 결정될 수 있습니다. 장애가 발생했지만 로그가 보이지 않거나, 주요 동작에 대한 로그가 보이지 않는다면 소프트웨어를 운영/유지보수 하는 입장에서는 앞이 깜깜할 것입니다.

  • 일반적으로 프로그래밍 언어는 기본적인 로그 출력 메서드를 제공합니다. 파이썬에서는 print을 사용해 기본적인 문자열을 출력합니다. 또한 기본 내장 모듈인 logging 을 활용해서 각 로그의 레벨을 설정해주고 커스텀하게 로그를 출력할 수 있습니다.

    # print
    print("안녕하새우1") 
    
    # logging
    import logging
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    logger.info("안녕하새우2")
  • 해당 아티클을 통해 로깅을 더 잘하기 위한 방법들을 알아봅시다(핵심 내용들을 가볍게 간추려 봤습니다) 이 중에서 중요하다고 생각하는 부분들은 Bold 처리를 해놓았으니 참고해주시면 됩니다.

1-1. Don’t Write Logs by Yourself (AKA Don’t Reinvent the Wheel)

  • 로그를 확인할 운영자들을 위해 표준 라이브러리 또는 시스템 API 호출을 사용하는 게 좋습니다. 즉 팀, 회사 차원에서 세운 표준을 따라가는 것이 좋습니다.
  • 스스로 로깅 모듈을 따로 개발할 필요는 없습니다 (은총알은 없습니다)

1-2. Log at the Proper Level

image

  • 일반적으로 로그의 중요도에 따라 레벨을 설정할 수 있습니다.
  • 각 상황에 맞게 로그에 레벨을 설정해주면, 소프트웨어의 기본 로그 레벨 설정에 따라 출력되는 로그를 제한할 수 있습니다(Info를 기본 레벨로 설정하면 Debug, Trace는 보이지 않습니다.)
  • 💡 가장 쉽게 적용해볼 수 있는 것은 개발 환경에서만 보이는 로그는 Debug 레벨, 일반적인 정보를 보여주는 로그는 Info 레벨, 장애를 발생시킬 수 있는 로그는 Error 레벨로 분류해주면 좋습니다.

1-3. Employ the Proper Log Category

  • 로그를 성격에 맞게 분류해주기 위해 로그 카테고리를 적용해주는 게 좋습니다 (e.g., 모델 학습 로그, 모델 추론 로그를 분류하기)
  • 💡 일반적으로 파이썬의 logging은 카테고리를 설정해주는 기능이 따로 없습니다. 따라서 카테고리를 넣으려면 json 포맷 같이 구조화된 로깅(Structured Logging)을 적용하는 걸 추천드립니다.

1-4. Write Meaningful Log Messages

  • 로깅을 할 때 가장 중요한 건 바로 로그를 봤을 때 상황을 정확히 파악할 수 있어야 합니다. 따라서 이해할 수 있도록 로그 메시지를 작성하는 것이 중요합니다.
  • 해당 로그에 대한 문맥(Context)를 잘 포함할 수 있도록 작성합니다.
  • 다른 로그 메시지와 의존 관계가 생기지 않도록 주의합니다 (비동기, 멀티 스레드 환경에서 문제가 발생할 수 있습니다)

1-5. Write Log Messages in English

  • 영어는 전부 ASCII 코드이기 때문에 문자열 인코딩 관점에서 나은 선택입니다.
  • 💡 애매하게 영어로 작성을 하는 것보다, 한국어로 명확하게 로그 메시지를 작성하는 게 더 나은 선택일 때가 많습니다.

1-6. Add Context to Your Log Messages

  • 로그 메시지에는 해당 로그에 대한 문맥(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");
    }
}

1-7. Log in Machine Parseable Format

  • 사람이 잘 읽을 수 있게 하는 것만큼, 기계가 잘 읽을 수 있도록 하는 것도 중요합니다
# 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}

1-8. But Make the Logs Human-Readable as Well

  • 표준 date/time format를 사용합니다 (ISO8601)
  • UTC나 현지시간 offset을 더한 Timestamp을 추가합니다.
  • 로그 레벨을 잘 설정해줍니다
  • 서로 다른 수준의 로그를 서로 다른 대상으로 분할하여 세밀하게 로그를 관리합니다
  • Exception을 포함한 로그는 StackTrace를 담도록 합니다
  • 멀티 스레드 환경에서는 스레드 이름을 포함하도록 합니다 (MDC)

1-9. Don’t Log Too Much or Too Little

  • 로그를 너무 많이 추가하면, 제대로 된 값을 확인하기 어려울 수 있습니다.
  • 로그를 너무 적게 추가하면, 트러블슈팅 시 문제가 발생할 수 있습니다.
  • 팁 : 개발 도중에는 가능한 많이 로그를 심어두고, 운영 환경에 올리기 전에 로그를 잘 정리하는 것이 낫습니다

1-10. Think of Your Audience

  • 해당 로그를 확인하는 주체(ops 팀, qa 팀, 같은 개발팀 등)에 따라 로그의 메시지, 문맥, 카테고리, 레벨 등이 달라질 수 있습니다.

1-11. Don’t Log for Troubleshooting Purposes Only

  • 로그 메시지는 트러블슈팅 뿐만 아니라 다양한 용도에서 활용할 수 있습니다
    • Auditing : 사내에서 시스템 사용자의 행동을 추적하는 용도로 사용 가능합니다
    • Profiling : Timestamp가 포함된 로그는 프로그램을 프로파일링(성능 측정, 메트릭 확인 등)을 할 때 용이합니다
    • Statistics : 쌓인 로그들은 통계 분석이 가능합니다

1-12. Avoid Vendor Lock-In

  • 특정 공급 업체에 락인되지 않도록 합니다

1-13. Don’t Log Sensitive Information

  • 비밀번호, 보안정보 등의 정보는 로그 메시지에 포함시키면 안 됩니다.

2. 에러 핸들링 잘하기 (예외 처리 잘하기)

  • 에러를 발생할 가능성이 있는 코드는 에러 핸들링을 잘 해주어 예상치 못한 상황에서 소프트웨어가 종료되면 안 됩니다. 이번 파트에서는 에러 핸들링 중에서 코드 차원에서 해주어야 할 예외 처리 에 더 집중해 보겠습니다.

  • 파이썬에서는 예외 처리를 잘하기 위해 try/except 문법을 제공해줍니다. 만약 에러가 발생할 수 있는 코드가 있다면 try/except를 적용해줘서 소프트웨어의 종료를 막는 것이 중요합니다.

try:
    i_can_occur_error()
    ...
except ValueError as e:
    # 에러 처리 로직
  • 해당 아티클을 통해 예외 처리를 더 잘하는 방법에 대해 알아봅시다

2-1. 오류 코드보다는 예외 사용하기

오류 코드를 사용하게 되면 상단에 오류인지 확인하는 불필요한 로직이 들어가게 됩니다. 오류의 범주에 들어가지 않은 상태를 나타내는 것이 아니라면, 예외(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:
            # 에러 처리 로직				
            

2-2. 예외 클래스 잘 정의하기

2-3. 에러 핸들링 잘하기

  • 에러를 포착했다면 잘 핸들링해줘야 합니다.

    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:
              ...	

References

https://www.dataset.com/blog/the-10-commandments-of-logging/

https://yansfil.github.io/awesome-class-materials/2.clean-code/5. 에러핸들링.html#에러-핸들링-잘하기

Clone this wiki locally