본문 바로가기
자습

파이썬 클린 코드 CH3. 좋은 코드의 일반적인 특징

by litaro 2019. 8. 17.

목표

견고한 소프트웨어 개념을 이해

작업중 잘못된 데이터를 다루는 방법

새로운 요구사항을 쉽게 받아들이수 있고 유지 보수가 쉬운 확장성

재사용 가능한 설계

개발팀의 생산성을 높이는 효율적 코드 작성

계약에 의한 디자인 (DbC)

컴포넌트와 이를 사용하는 클라이언트간에 동의하는 계약을 먼저 한 다음, 계약을 어겼을 경우는 명시적으로 왜 계속할 수 없는지 예외를 발생시키는 것.

예) 정수를 파라미터로 사용하는 함수에 문자열을 전달하면?

계약은 컴포넌트간의 통신중에 반드시 지켜져야할 몇가지 규칙을 강제한다.

이로 인해 오류 발생시 쉽게 찾아낼수 있다. - 책임 소재 신속히 파악

사전 조건 (precondition)

함수나 메서드가 제대로 동작하기 위해 보장해야 하는 모든것, 즉 입력 값의 유효성을 이야기 한다.

ex) 초기화된 객체, null이 아닌 값 등의 조건

mypy가 하는 타입 체킹 보다는 입력으로 필요로 하는 값이 정확한지 확인하는 것에 가깝다.

문제는 어디서 할것인지

  • 관용적 (tolerant) 접근 방법: 클라이언트가 할지
  • 까다로운 (demanding) 접근 방법: 함수 자체적으로 로직 실행 전에 할것인지.

어떤 방식을 택하든 중복 제거 원칙을 항상 마음속에 간직하도록~!

사전 조건은 런타임 중 확인 가능하기에 사전 조건에 맞지 않다면 실행하지 않아야 한다.

사후 조건 (postcondition)

메서드 또는 함수가 반환된 후의 상태를 강제하는 계약의 일부, 즉 반환값의 유효성을 얘기한다.

불변식 (invariant)

함수가 실행되는 동안 유지되는것 

부작용 (side-effect)

코드의 부작용

파이썬스러운 계약

https://www.python.org/dev/peps/pep-0316/

 

PEP 316 -- Programming by Contract for Python

The official home of the Python Programming Language

www.python.org

PEP316의 내용

파이썬 자체적으로 이를 제공하려는 시도는 있지만 아직 "deferred" 상태

개별적으로 충분히 구현 가능하고, 가능한 방법은 Exception 을 발생시키거나 데코레이터를 사용할 수 있다.

데코레이터를 이용한 사전/사후 조건체크 (https://stackoverflow.com/questions/8563464/using-design-by-contract-in-python)

계약에 의한 디자인 - 결론

디자인 원칙의 주된 가치는

  • 문제가 있는 부분을 효과적으로 식별
  • 프로그램 구조를 명확히 하기 : 명시적으로 함수나 함수나 메서드가 정상 동작하기 위해 기대하는 것이 무엇인지, 무엇을 기대할 수 있는지 정의)

이를 위해 계약에 대한 단위테스트 추가가 필요하다.

주의: 단순 데이터 타입 검사에 대한 계약 정의는 의미 없다. 이것은 Mypy와 같은 도구를 이용한다.

방어적(defensive) 프로그래밍

DbC와 같이 예외 발생시킬 조건을 기술하는것이 아니라, 

객체, 함수, 메서드와 같은 코드의 모든 부분을 스스로 보호할수 있게 하는것이다. (DbC와 보완적인 관계)

에러 핸들링

오류 발생하기 쉬운 상황에서 에러 핸들링 프로시져 사용. 실행을 계속 할수 있을지 or 극복할 수 없는 오류라서 프로그램 중단할지 결정하는것이다.

값 대체

해당 컴포넌트에 오류가 있는 경우 결과 값을 안전한 값으로 대체 : 견고성 vs. 정확성 (trade-off)

예) 덧셈인 경우 0을 반환

또다른 방법: 제공안된 데이터에 기본값 사용 - 예) Python Dictionary의 get method)

def connect_database(host="localhost", port=5432):
    logger.info("다음 정보로 데이터베이스에 접속: %s:%i", host, port)

예외 처리

어떤 경우는 실행 중 멈추는것이 좋다. 이 경우 호출자에게 실패를 알림

  • 함수 자체의 문제인지 외부 컴포넌트에 의한 문제인지 예외적인 상황을 명확하게 알려줄수 있어야 한다.
  • 사용하여 시나리오나 비즈니스 로직 처리시 프로그램의 흐름을 읽기 어려워 go-to 문 값을 사용하기도 하는데, 호출 스택의 여러 수준에서 사용되면 논리를 캡슐화하지 못할 수 있다.
    • 프로그램이 꼭 처리해야하는 정말 예외적인 비즈니스 로직을 except 블록과 혼합하여 사용하면 안된다.
  • 예외는 캡슐화를 약화 시키기 때문에 신중하게 사용해야한다. 
    • 함수가 예외가 많으면 호출자가 호출하는 함수에 대해 더 많은 것을 알아한다.
    • 함수가 너무 많은 예외를 발생시킴녀 문맥에서 자유롭지 않다는것을 의미한다.
    • 예외가 너무 많이 발생하면 여러개의 작은 것으로 나눠야 한다는 신호일수 있다.

올바른 수준의 추상화 단계에서 예외 처리

아래는 서로 다른 수준의 추상화를 혼합하는 예제. 

class DataTransport:
    """An example of an object badly handling exceptions of different levels."""

    retry_threshold: int = 5
    retry_n_times: int = 3

    def __init__(self, connector):
        self._connector = connector
        self.connection = None

    def deliver_event(self, event):
        try:
            self.connect()
            data = event.decode()
            self.send(data)
        except ConnectionError as e:
            logger.info("connection error detected: %s", e)
            raise
        except ValueError as e:
            logger.error("%r contains incorrect data: %s", event, e)
            raise

    def connect(self):
        for _ in range(self.retry_n_times):
            try:
                self.connection = self._connector.connect()
            except ConnectionError as e:
                logger.info(
                    "%s: attempting new connection in %is",
                    e,
                    self.retry_threshold,
                )
                time.sleep(self.retry_threshold)
            else:
                return self.connection
        raise ConnectionError(
            f"Couldn't connect after {self.retry_n_times} times"
        )

    def send(self, data):
        return self.connection.send(data)

deliver_event 메서드를 보면, ConnectionError, ValueError 를 보면, 별로 관계가 없다.

ConnectionError는 connect 메서드에 속한 에러이고 ValueError 는 event의 decode 메서드에 속한 에러.

분산에 대한 아이디어

  • connect 에서 재시도를 지원한다면 ConnectionError 처리부분을 예외처리가 필요 없다.
  • event 처리는 분리하여 send에서

위를 구현하면 아래와 같다.

def connect_with_retry(connector, retry_n_times, retry_threshold=5):
    """Tries to establish the connection of <connector> retrying
    <retry_n_times>.

    If it can connect, returns the connection object.
    If it's not possible after the retries, raises ConnectionError

    :param connector:           An object with a `.connect()` method.
    :param retry_n_times int:   The number of times to try to call
                                ``connector.connect()``.
    :param retry_threshold int: The time lapse between retry calls.

    """
    for _ in range(retry_n_times):
        try:
            return connector.connect()
        except ConnectionError as e:
            logger.info(
                "%s: attempting new connection in %is", e, retry_threshold
            )
            time.sleep(retry_threshold)
    exc = ConnectionError(f"Couldn't connect after {retry_n_times} times")
    logger.exception(exc)
    raise exc


class DataTransport:
    """An example of an object that separates the exception handling by
    abstraction levels.
    """

    retry_threshold: int = 5
    retry_n_times: int = 3

    def __init__(self, connector):
        self._connector = connector
        self.connection = None

    def deliver_event(self, event):
        self.connection = connect_with_retry(
            self._connector, self.retry_n_times, self.retry_threshold
        )
        self.send(event)

    def send(self, event):
        try:
            return self.connection.send(event.decode())
        except ValueError as e:
            logger.error("%r contains incorrect data: %s", event, e)
            raise

Tranceback 노출 금지

보안을 위한 고려사항이다.

예외 처리를 위해 효율적 오류 파악을 위해 사용하는 Traceback 정보, 메세지 및 기타 수집 가능한 정보를 로그로 남기는 것은 중요하다. 하지만 세부 정보를 사용자에게 보여서는 안된다.

예외가 전파되도록 하는 경우, 중요한 정보를 공개하지 않도록 주의해야한다.

사용자에게 문제를 알리려면 무엇이 잘못되었다거나 페이지를 찾을 수 없다는 등의 일반적인 메세지를 사용해야한다.

비어있는 except 블록 지양

try:
    process_data()
except:
    pass

이것의 문제는 결코 실패하지 않는다는 것이다. 

파이썬의 철학 (The Zen of Python): Errors should never pass silently.

코드에 결함이 있다면.. 유지보수를 더 어렵게 만든다.

대안

  • 보다 구체적인 예외를 사용하는 것이다. ex) Exception 대신  AttributeError or KeyError
  • except 블록에서 실제 오류를 처리한다.

원본 예외 포함

오류 처리 과정에서 다른 오류를 발생시키고 메세지 변경 할 수 있는데 이때 원본 예외는 포함한다.

class InternalDataError(Exception):
    """An exception with the data of our domain problem."""


def process(data_dictionary, record_id):
    try:
        return data_dictionary[record_id]
    except KeyError as e:
        raise InternalDataError("Record not present") from e

raise <e> from <o> 구문을 사용한다.

파이썬에서 어설션 사용하기

어설션은 절대 일어나지 않아야 하는 상황에서 사용되므로 더 큰 피해를 입지 않도록 반드시 프로그램을 종료시켜야 한다.

try:
    assert condition.holds(), "조건에 맞지 않음"
except AssertionError:
    alternative_procedure()

문제점

  • Assertion 예외를 처리하면 안된다. 
  • Assertion 문장이 함수면 오류 정확한 오류 값을 알 수 없고 값이 다른 결과가 나올 수 도 있다.

관심사의 분리

책임이 다르면 컴포넌트, 계층 또는 모듈로 분리되어야 한다.

파금효과를 최소화하여 유지 보수성을 향상시키기 위함이다.

응집력 (cohesion)과 결합력 (coupling)

응집력: 객체가 작고 잘 정의된 목적을 가져야하고, 가능하면 작아야한다. (응집력이 높을 수록 재사용성이 높아진다)

결합력: 두 개 이상의 객체가 서로 어떻게 의존하는지를 나타낸다.

결합력이 높으면 낮은 재사용성, 파급 효과, 낮은 수준의 추상화와 같은 결과를 가져온다

경험상 잘 정의된 소프트웨어는 높은 응집력과 낮은 결합력을 갖는다.

개발 지침 약어

DRY/OAOO

DRY (Do not Repeat Yourself)

OAOO (Once and Only Once)

코드에 있는 지식은 단 한번, 단 한 곳에 정의되어야 한다.

코드 중복의 문제

  • 오류가 발생하기 쉽다. : 수정할때 모두 수정해야하는데 하나라도 빠지면 버그를 발생
  • 비용이 비싸다 : 변경하는데 드는 개발 및 테스트 시간 비용
  • 신뢰성이 떨어진다. : 문맥상 여러 코드를 변경해야하는 경우 사람이 위치를 기억해야한다. 데이터의 완결성이 떨어진다.

아래와 같이 lambda가 특별한 도메인 지식을 나타내는데 의미를 정의하지 않아 중복이 발생한다.

도메인 문제에 대한 지식이 사용된 경우 의미를 부여해야한다.

def process_students_list(students):
    # 중간 생략 ...

    students_ranking = sorted(
        students, key=lambda s: s.passed * 11 - s.failed * 5 - s.years * 2
    )

    # 학생별 순위 출력
    for student in students_ranking:
        print(
            "이름: {0}, 점수: {1}".format(
                student.name,
                (student.passed * 11 - student.failed * 5 - student.years * 2)
            )
        )
def score_for_students(student):
    return student.passed * 11 - student.failed * 5 - student.years * 2


def process_students_list(students):
    # 중간 생략 ...

    students_ranking = sorted(students, key=score_for_students)

    # 학생별 순위 출력
    for student in students_ranking:
        print(
            "이름: {0}, 점수: {1}".format(
                student.name, score_for_students(student)
            )
        )

YAGNI

YAGNI(You Ain't Gonna Need it) 

과잉 엔지니어링을 피해야 한다.

우린 미래학자가 아니다!

오직 현재의 요구사항을 잘 해결하기 위한 소프트웨어를 작성하고 가능한 나중에 수정하기 쉽도록 작성한다.

KIS

KIS(Keep It Simple) 

선택한 솔루션이 문제에 적합한 최소한의 솔루션인지 자문해보자

일반적으로 단순함이란 문제에 맞는 가장 작은 데이터 구조를 사용하는 것이다. (표준 라이브러리를 참고)

class ComplicatedNamespace:
    """An convoluted example of initializing an object with some properties."""

    ACCEPTED_VALUES = ("id_", "user", "location")

    @classmethod
    def init_with_data(cls, **data):
        instance = cls()
        for key, value in data.items():
            if key in cls.ACCEPTED_VALUES:
                setattr(instance, key, value)
        return instance
>>> cn = ComplicatedNamespace.init_with_data(id_=42, user="root", location="127.0.0.1", extra="excluded")
>>> cn.id_, cn.user, cn.location
(42, 'root', '127.0.0.1')
>>> hasattr(cn, 'extra')
False

객체를 초기화 하기 위해 추가 클래스 메서드를 만드는것은 꼭 필요해 보이지 않는다.

사용자는 초기화를 위해 init_with_data 라는 일반적이지 않은 메서드의 이름을 알아야하는 불편함도 있다.

Python에서 제공하는 초기화 메서드를 사용하자

class Namespace:
    """Create an object from keyword arguments."""

    ACCEPTED_VALUES = ("id_", "user", "location")

    def __init__(self, **data):
        accepted_data = {
            k: v for k, v in data.items() if k in self.ACCEPTED_VALUES
        }
        self.__dict__.update(accepted_data)
>>> cn = Namespace(id_=42, user="root", location="127.0.0.1", extra="excluded")
>>> cn.id_, cn.user, cn.location
(42, 'root', '127.0.0.1')
>>> hasattr(cn, "extra")
False

파이썬의 철학을 기억하자 : 단순한 것이 복잡한 것 보다 낫다

EAFP/LBYL

LBYL(Look Before You Leap)는 도약하기전에 확인하라

if os.path.exists(filename):
    with open(filename) as f:
        ...

EAFP(Easier to Ask Forgiveness than Permission) 일단 코드를 실행하고 실제 동작하지 않을 경우에 대응한다.

try:
    with open(filename) as f:
        ...
except FileNotFoundError as e:
    logger.error(e)

파이썬은 EAFP 방식으로 만들어졌으면 이 방법을 권한다.

컴포지션과 상속

상속이 좋은 선택인 경우

  • public 메서드와 속성 인터페이스를 정의한 컴포넌트가 있고 이 기능을 그대로 물려받으면서 추가 기능을 더하려는 경우 또는 수정하려는 경우
  • 인터페이스 정의 : 특정 인터페이스 방식을 강제하고자 할때. 추상 클래스 만들어 상속한 하위 클래스는 구현
  • 예외 : Exception 에서 파생되어 except Exception: 으로 모든 에러 catch 가능하다

상속 안티패턴

코드 재사용만을 목적으로 상속을 사용하려고 하는 경우가 많다.

다음 예제는 전형적인 안티패턴 예제 : 구현 객체를 도메인 객체와 혼합할때 발생하는 문제

  • 여러 고객에게 정책을 적용하는 시스템, 요구 사항은 새 고객 정보를 기록하고 정책을 변경하거나 데이터 편집이다.
  • 고객마다 다른 정책을 관리하기 위해 특정 고객에게 상수시간에 접근하려면 policy_transation[customer_id]처럼 구현하는 것을 선택하게 된다.
class TransactionalPolicy(collections.UserDict):
    """Example of an incorrect use of inheritance."""

    def change_in_policy(self, customer_id, **new_policy_data):
        self[customer_id].update(**new_policy_data)
>>> policy = TransactionalPolicy(
            {
                "client001": {
                    "fee": 1000.0,
                    "expiration_date": datetime(2020, 1, 3),
                }
            }
        )
>>> policy["client001"]
{'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 3, 0, 0)}
>>> policy.change_in_policy(
            "client001", expiration_date=datetime(2020, 1, 4)
        )
>>> policy["client001"]
{'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 4, 0, 0)}
  • 동작은 잘 되지만 비용 측면으로는 불필요한 수많은 메서드가 포함되게 된다.
dir(policy)
[...'change_in_policy', 'clear', 'copy', 'data', 'fromkeys', 'get', 'items', 'keys', 
'pop', 'popitem', 'setdefault', 'update', 'values']

문제점

  • TransactionPolicy라는 이름만 보고 어떻게 사전 타입이라는것을 알 수 있을까?
  • TransactionPolicy는 이제 사전의 모든 메서드를 포함한다. 이중 필요하지 않는 메서드가 사용자에게 노출되어 부작용을 만들수 있다.

해결

  • 컴포지션을 사용
    • 사전을 private 속성에 저장
    • __getitem__()으로 사전 프록시 만들고
    • 필요한 public 메서드 추가
class TransactionalPolicy:
    """Example refactored to use composition."""

    def __init__(self, policy_data, **extra_data):
        self._data = {**policy_data, **extra_data}

    def change_in_policy(self, customer_id, **new_policy_data):
        self._data[customer_id].update(**new_policy_data)

    def __getitem__(self, customer_id):
        return self._data[customer_id]

    def __len__(self):
        return len(self._data)

파이썬의 다중상속

메서드 결정 순서(MRO)

  • 다중 상속의 다이아몬드 문제: 손자 클래스 입장에서 두개의 부모 클레스가 같은 이름의 메서드를 가진 경우 어떤 메서드를 사용할지
  • 파이썬은 MRO 알고리즘을 사용해서 해결

참고예제

class BaseModule:
    module_name = "top"

    def __init__(self, module_name):
        self.name = module_name

    def __str__(self):
        return f"{self.module_name}:{self.name}"


class BaseModule1(BaseModule):
    module_name = "module-1"


class BaseModule2(BaseModule):
    module_name = "module-2"


class BaseModule3(BaseModule):
    module_name = "module-3"


class ConcreteModuleA12(BaseModule1, BaseModule2):
    """Extend 1 & 2"""


class ConcreteModuleB23(BaseModule2, BaseModule3):
    """Extend 2 & 3"""  

mro() 함수로 해당 클래스의 결정 순서를 알수 있다.

>>> str(ConcreteModuleA12('name'))
'module-1:name'
>>> str(ConcreteModuleB23("test"))
'module-2:test'

>>> [cls.__name__ for cls in ConcreteModuleB23.mro()]
['ConcreteModuleB23', 'BaseModule2', 'BaseModule3', 'BaseModule', 'object']

믹스인 (mixin)

  • 믹스인은 코드를 재사용하기 위해 일반적인 행동을 캡슐화해놓은 기본 클래스.
  • 그 자체로 유용하지 않고 다른 클래스와 함께 믹스인을 다중상속하여 사용한다.

문자열을 받아 (-)으로 구분된 값을 반환하는 파서

class BaseTokenizer:
    def __init__(self, str_token):
        self.str_token = str_token
    def __iter__(self):
        yield from self.str_token.split("-")
>>> tk = BaseTokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0")
>>> list(tk)
['28a2320b', 'fd3f', '4627', '9792', 'a2b38e3c46b0']

믹스인을 사용해서 파싱된 값을 대문자로 변경할 수 있다.

class UpperIterableMixin:
    def __iter__(self):
        return map(str.upper, super().__iter__())

class Tokenizer(UpperIterableMixin, BaseTokenizer):
	pass
>>> tk = Tokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0")
>>> list(tk)
['28A2320B', 'FD3F', '4627', '9792', 'A2B38E3C46B0']

>>> [cls.__name__ for cls in Tokenizer.mro()]
['Tokenizer', 'UpperIterableMixin', 'BaseTokenizer', 'object']

함수와 메서드의 인자

파이썬의 함수 인자 동작 방식

인자는 함수에 어떻게 복사되는가

파이썬은 모든 인자가 값에 의해 전달된다. 

그러나 인자의 값을 변경하는 함수의 경우, 인자가 변형가능한 mutable 객체이면 부작용이 생길 수 있다.

def function(arg):
    arg += " in function"
    print(arg)
    
>>> immutable = "hello"
>>> function(immutable)
hello in function
>>> immutable
'hello'
>>> mutable = list("hello")
>>> function(mutable)
['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']
>>> mutable
['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']

list의 경우, 변형 가능하고 해당 문장은 extend() 호출하는것 동일하게 동작하고 extend 연산자가 객체 참조하고 있는 변수를 수정하기에 결국 함수 외부에서 실제 값을 수정한다.

함수 인자를 변경하지 않아야 한다. 최대한 함수에서 발생할 수 있는 부작용을 회피하라

가변 인자

파이썬에서는 인자를 패킹할 변수의 이름 앞에 별표(*)를 사용한다.

또한 반대로 언패킹 (부분적인것도)도 가능하다.

def f(first, second, third):
    print(first)
    print(second)
    print(third)
    
>>> l = [1,2,3]
>>> f(*l)
1
2
3

>>> a, b, c = [1,2,3]
>>> a
1
>>> b
2
>>> c
3

def show(e, rest):
    print("요소: {0} - 나머지: {1}".format(e, rest))
    
>>> first, *rest = [1,2,3,4,5]
>>> show(first, rest)
요소: 1 - 나머지: [2, 3, 4, 5]
>>> *rest , last = range(6)
>>> show(last, rest)
요소: 5 - 나머지: [0, 1, 2, 3, 4]
>>> first, *middle, last = range(6)
>>> first
0
>>> middle
[1, 2, 3, 4]
>>> last
5
>>> first, last, *empty = (1,2)
>>> first
1
>>> last
2
>>> empty
[]

변수 언패킹의 가장 좋은 사용 예는 반복

>>> USERS = [(i, f"first_name_{i}", f"last_name_{i}") for i in range(1_000)]
>>> USERS
[(0, 'first_name_0', 'last_name_0'), 
(1, 'first_name_1', 'last_name_1'), 
(2, 'first_name_2', 'last_name_2'),
...
(999, 'first_name_999', 'last_name_999')]
class User:
    def __init__(self, user_id, first_name, last_name):
        self.user_id = user_id
        self.first_name = first_name
        self.last_name = last_name

    def __repr__(self):
        return f"{self.__class__.__name__}({self.user_id!r}, {self.first_name!r}, {self.last_name!r})"


def bad_users_from_rows(dbrows) -> list:
    """A bad case (non-pythonic) of creating ``User``s from DB rows."""
    return [User(row[0], row[1], row[2]) for row in dbrows]


def users_from_rows(dbrows) -> list:
    """Create ``User``s from DB rows."""
    return [
        User(user_id, first_name, last_name)
        for (user_id, first_name, last_name) in dbrows
    ]
>>> users_from_rows(USERS)
[User(0, 'first_name_0', 'last_name_0'), 
User(1, 'first_name_1', 'last_name_1'), 
...
User(999, 'first_name_999', 'last_name_999')]

bad_users_from_rows의 경우 동일하지만 row[0], row[1], row[2] 이 어떤 의미인지 알수 없다.

반면 users_from_rows 는 user_id, first_name, last_name 은 자명한 이름이라 읽기 좋다.

이중 별표 (**) 를 활용하면 사전의 값을 활용할 수 있다.

def user_info(user_id, first_name, last_name):
    print('사용자 ID : ', user_id)
    print('이름은 : ', first_name)
    print('성은 :', last_name)
    
>>> user1 = {"user_id": "1", "first_name": "sam", "last_name": "Kim"}
>>> user_info(**user1)
사용자 ID :  1
이름은 :  sam
성은 : Kim

함수 인자의 개수

너무 많은 인자는 Code Smell 의 징후이다.

해결 방법

  • 구체화하는것, 새로운 객체를 만드는 것이다.
  • 가변 인자나 키워드 인자 사용하여 동적 서명을 가진 함수를 만든다
    • 파이썬스러운 방법일지 모르지만 남용하지 않아야.. 동적이어서 유지 보수가 어렵기 때문

함수 인자와 결합력

함수 서명의 인수가 많을수록 호출자 함수와 밀접하게 결합될 가능성이 커진다.

함수가 제대로 동작하기 위해 너무 많은 파라미터가 필요한 경우 코드의 나쁜 냄새라고 생각하면 된다.

많은 인자를 취하는 작은 함수의 서명

track_request(request.headers, request.ip_addr, request.request_id)

모든 파라미터가 request와 관련이 있으니 request를 파라미터를 전달하는 것이 좋다.

track_request(request)

소프트웨어 디자인 우수 사례 결론

최종 권장사항 몇가지를 추가한다.

소프트웨어의 독립성 (orthogonality)

직교는 두 요소가 독립적이라는 것을 의미. 

  • 모듈, 클래스 또는 함수를 변경하면 수정한 컴포넌트가 외부 세계에 영향을 미치지 않아야한다.
  • 런타임 구조적인 측면에서는 변경또는 부작용을 내부 문제로 만드는것이다.
  • 단위테스트 측면에서도 독립적이기 때문에 각각의 테스트를 통과하면 전체 테스트 없이 확신할수 있다.
  • 기능면에서도 두 기능이 완전히 독립적이라면 간단한 테스트 후에 배포 가능하다.
def calculate_price(base_price: float, tax: float, discount: float) -> float:
    return (base_price * (1 + tax)) * (1 - discount)


def show_price(price: float) -> str:
    return "$ {0:,.2f}".format(price)


def str_final_price(
    base_price: float, tax: float, discount: float, fmt_function=str) -> str:
    return fmt_function(calculate_price(base_price, tax, discount))
>>> str_final_price(10, 0.2, 0.5)
'6.0'
>>> str_final_price(1000, 0.2, 0)
'1200.0'
>>> str_final_price(1000, 0.2, 0.1, fmt_function=show_price)
'$ 1,080.00'

calculate_price와 show_price는 독립성을 갖는다. 그리고 str_final_price를 사용해도 서로 변경사항이 영향을 미치지 않는다.

코드 구조

큰 파일을 만들지 말고 유사한 컴포넌트끼리 정리하여 구조화해야한다. 

  • 코드를 구조화하는 방법은 팀의 작업 효율성과 유지 보수성에 영향을 미친다.
  • 모듈을 임포트할때 구문을 분석하고 메모리에 로드할 객체가 줄어든다.
  • 의존성이 줄었기 때문에 더 적은 모듈만 가져오면 된다.

'자습' 카테고리의 다른 글

click을 이용한 python CLI  (0) 2019.09.08
pre-commit 활용하기  (0) 2019.08.31
AWS Lambda@Edge  (0) 2019.08.10
Python Logging  (0) 2019.07.26
AWS S3-Select  (0) 2019.07.20