ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 루트 Exception을 정의해서 API로부터 호출자를 보호
    언어/파이썬 & 장고 2017. 1. 15. 19:22

    모듈의 API를 정의할 때 사용자가 던지는 예외는 인터페이스의 일부로 정의한 함수와 클래스 만입니다.

    파이썬은 언어와 표준 라이브러리용 내장 예외 계층을 갖추고 있습니다. 오류를 보고할 때 자신만의 새 타입을 정의하는 대신 내장 예외 타입을 사용할 가능성이 큽니다. 예를 들어 함수에 올바르지 않은 파라미터가 넘어오면 valueError예외를 일으 킬 수 있습니다.

    def determine_weight(volume, density):
        if density <= 0:
            raise ValueError('Density must be positive')
        # ...


    몇몇 경우에는 ValueError를 사용하는 것을 이해할 수 있지만 API용으로는 자신만의 예외 계층을 정의하는 방법이 더 강력합니다. 예외 계층을 정의하려면 모듈 내에서 루트 Exception을 제공하면 됩니다. 그런 다음 해당 모듈에서 일어나는 다른 예외가 모두 루트 예외로부터 상속받게 합니다.

    # my_module.py
    class Error(Exception):
        """Base-class for all exceptions raised by this module"""
        
    
    class InvalidDensityError(Error):
        """There was a problem with a provided density value"""
     
     
    def determine_weight(volume, density):
        if density <= 0:
            raise ValueError('Density must be positive')


    모듈에 루트 예외를 두면 API 사용자들이 목적을 두고 일으킨 모든 예외를 잡아낼 수 있습니다. 예를 들어 API 사용자는 루트 예외를 잡아내는 try/except문으로 함수를 호출할 수 있습니다. 

    import my_module
    import logging
    
    
    try:
        weight = my_module.determine_weight(1, -1)
    except my_module.Error as e:
        logging.error('Unexpected error: %s', e)


    이 try/except는 API의 예외가 너무 멀리 퍼져나가서 호출하는 프로그램을 중단하는 일을 막습니다. 이 구문은 호출하는 코드를 API로부터 보호합니다. 이런 보호는 세 가지 유용한 효과를 냅니다.

    첫 번째로 루트 예외가 있으면 호출자가 사용자의 API를 사용할 때 문제점을 이해할 수 있습니다. 호출자가 API를 올바르게 사용한다면 여러분이 의도적으로 일으킨 다양한 예외를 잡아낼 수 있어야 합니다. 그러한 예외를 처리할 수 없다면 여러분이 작성한 모듈의 루트 예외를 잡아서 보호하는 except 블록까지 전파됩니다. 이 except 블록은 API 사용자가 예외를 주목하게 하여 해당 예외 타입을 적절히 처리하는 코드를 추가하게 만듭니다.

    import my_module
    import logging
    
    
    try:
        weight = my_module.determine_weight(1, -1)
    except my_module.InvalidDensityError:
        weight = 0
    except my_module.Error as e:
        logging.error('Bug in the calling code: %s', e)


    루트 예외를 사용하는 두 번째 이점은 API 모듈의 코드에 있는 버그를 찾는데 도움이 된다는 것입니다. 코드에서 모듈 계층 안에 정의한 예외만 의도적으로 일으킨다면, 해당 모듈에서 일어난 다른 타입의 예외는 모두 의도하지 않은 것이 틀림없습니다. 이런 예외는 API 코드에 있는 버그입니다.

    try/except문을 사용한다고 해서 API 모듈의 코드에 있는 버그로부터 API사용자들을 보호하지는 못합니다. API 사용자를 보호하려면 호출자가 파이썬의 Exception 기반 클래스를 잡아내는 다른 except 블록을 추가해야 합니다. 이렇게 하면 API 사용자가 API 모듈의 구현에 수정해야 할 버그가 있다는 사실을 알 수 있습니다.

    import my_module
    import logging
    
    
    try:
        weight = my_module.determine_weight(1, -1)
    except my_module.InvalidDensityError:
        weight = 0
    except my_module.Error as e:
        logging.error('Bug in the calling code: %s', e)
    except Exception as e:
        logging.error('Bug in the API code: %s', e)
        raise 


    루트 예외를 사용할 때의 세 번째 효과는 API의 미래를 대비할 수 있다는 점입니다. 시간이 지나 특정 환경에서 더 구체적인 예외를 제공하려고 API를 확장할 수도 있습니다. 예를 들어 밀도를 음수로 넘기는 오류 상황을 알리는 Exception 서브클래스를 추가할 수 있습니다.

    # my_module.py
    
    
    class Error(Exception):
        """Base-class for all exceptions raised by this module"""
    
    
    class InvalidDensityError(Error):
        """There was a problem with a provided density value"""
    
    
    class NegativeDensityError(InvalidDensityError):
        """A provided density value was negative"""
    
    
    def determine_weight(volume, density):
        if density <= 0:
            raise NegativeDensityError


    호출하는 코드는 이미 InvalidDensityError 예외(NegativeDensityError의 부모 클래스)를 잡아내므로 이전과 똑같이 동작합니다. 나중에 호출자가 새 예외 타입을 특별한 경우로 처리하도록 결정하고 그에 따라 동작을 변경할 수 있습니다.

    import my_module
    import logging
    
    
    try:
        weight = my_module.determine_weight(1, -1)
        
    except my_module.NegativeDensityError as e:
        raise ValueError('Must supply non-negative density') from e
    except my_module.InvalidDensityError:
        weight = 0
    except my_module.Error as e:
        logging.error('Bug in the calling code: %s', e)
    except Exception as e:
        logging.error('Bug in the API code: %s', e)
        raise


    루트 예외 바로 아래에 더 많은 예외를 제공하여 API의 미래를 대비할 수 있습니다. 예를 들어 무게 계산, 부피 계산, 밀도 계산과 관련이 있는 오류 집합이 있다고 가정합니다.

    # my_module.py
    
    
    class Error(Exception):
        """Base-class for all exceptions raised by this module"""
    
    
    class WeightError(Error):
        """Base-class for weight calculation errors"""
        
        
    class VolumeError(Error):
        """Base-class for volume calculation errors"""
        
        
    class DensityError(Error):
        """Base-class for density calculation errors"""


    구체적인 예외는 이런 일반적인 예외로부터 상속해서 만듭니다. 각 중간 예외는 루트 예외처럼 동작합니다. 이 방법을 이용하면 많은 기능을 기반으로 한 API 코드로부터 호출하는 코드를 쉽게 분리할 수 있습니다. 이 방법이 모든 호출자가 매우 구체적인 Exception 서브클래스를 각각 캐치하는 방법보다 훨씬 낫습니다.

    요약

    작성 중인 모듈에 루트 예외를 정의하면 API로부터 API 사용자를 보호할 수 있음

    루트 예외를 잡으면 API를 사용하는 코드에 숨은 버그를 찾는 데 도움이 될 수 있음

    파이썬 Exception 기반 클래스를 잡으면 API 구현에 있는 버그를 찾는 데 도움이 될 수 있음

    중간 루트 예외를 이용하면 API를 사용하는 코드에 영향을 주지 않고 나중에 더 구체적인 예외를 추가할 수 있음


    댓글