[python cleancode] 3. 좋은 코드의 일반적인 특징

햄도·2021년 4월 8일
0

Python Cleancode

목록 보기
3/9

출처

파이썬 클린코드를 읽으며 정리한 내용입니다.

  • 앞장에서 차례대로
    1. 일관적인 코드를 구성하는 것이 왜 중요한지
    2. 간결하고 관용적인 코드를 작성하는 방법
      을 배웠으니, 이제 클린코드가 무엇인지 이해해보자.
  • 궁극적인 목표는 코드를 가능한 견고하게 만들고, 결함을 최소화하고 완전히 자명하도록 하는 것

계약에 의한 디자인(DbC)


  • 애플리케이션의 책임을 나누어 레이어나 컴포넌트로 분리하고, 함수를 사용할 고객에게 API를 노출한 경우 컴포넌트의 함수, 클래스, 메서드는 특별한 유의사항에 따라 동작해야 하며 그렇지 않을 경우 결함이 발생한다.
  • 코드가 정상적으로 동작하기를 위해 기대하는 것과, 호출자가 반환 받기를 기대하는 것은 디자인의 하나가 되어야 한다. -> 계약!
  • 계약에 의한 디자인이란?
    • 양측이 동의하는 계약을 먼저 한다.
    • 계약을 어겼을 경우 명시적으로 왜 계속할 수 없는지 예외를 발생한다.
  • 계약은 주로 사전조건과 사후조건으로 구성되고, 불변식과 부작용은 선택적으로 포함될 수 있다.
  • 이런 디자인을 하는 이유는 책임소재를 신속하게 파악하여 오류를 쉽게 찾아서 수정하고, 잘못된 가정 하에 코드의 핵심 부분이 실행되는 것을 막기 위해서이다.

사전조건(precondition)

  • 함수나 메서드가 제대로 동작하기 위해 보장해야 하는 모든 것
  • 일반적으로는 적절한 데이터를 전달하는 것을 말한다. 타입 체크 외에도 유효성 검사가 필요하다.
  • 유효성을 검사할 수 있는 위치는 다음과 같다.
    • 클라이언트가 함수를 호출하기 전에 검사 -> 관용적 접근법. 함수가 깨진 데이터도 수용한다.
    • 함수가 로직을 실행하기 전에 검사 -> 까다로운 접근법
  • 책에서는 까다로운 접근법을 제안한다. 일반적으로 가장 안전한 방법이며 널리 쓰이고 있다.
  • 유의할 점은, 중복 제거 원칙을 지켜야 한다는 것이다. 즉 검증 로직을 클라이언트나 함수 둘 중 하나에만 두어야 한다.

사후조건(postcondition)

  • 메서드 또는 함수가 반환된 후의 상태를 강제하는 계약의 일부
  • 함수 또는 메서드가 적절한 속성으로 호출되었다면 사후조건은 특정 속성이 보존되도록 보장해야 한다.

파이썬스러운 계약

  • 이러한 디자인을 적용하는 방법으로는 메서드, 함수 및 클래스에 RuntimeError 나 ValueError를 발생시키는 메커니즘을 추가하는 것이다.
  • 문제를 정확하게 특정하기 어려우면 사용자 정의 예외를 만드는 것이 좋다.
  • 사전조건에 대한 검사와 사후조건에 대한 검사, 그리고 핵심 기능 구현은 가능한 한 구분하는 것이 좋다. 이를 위해 더 작은 함수를 생성하거나, 데코레이터를 사용하는 방법도 있다.

결론

  • 이러한 원칙을 따르기 위해서는, 대부분의 디자인 원칙이 그렇듯이 추가 작업이 발생한다. 하지만 이 원칙을 통해 얻은 품질은 장기적으로 보장된다.
  • 무엇을 검증할지만 신중히 검토하여 정하는 것이 좋을 것

방어적 프로그래밍


  • DbC와는 다른 접근 방식을 따르는 디자인
  • 객체, 함수 또는 메서드와 같은 코드의 모든 부분을 유효하지 않은 것으로부터 스스로 보호할 수 있게 하는 것

에러 핸들링

  • 예상할 수 있는 시나리오의 오류 처리
  • 예상되는 에러에 대해서 실행을 계속할 수 있을지 아니면 프로그램을 중단해야 할지 결정하는 것이 주 목적이다.
  • 에러 처리 방법의 일부로 세 가지 방법이 있다.
    • 값 대체
    • 에러 로깅
    • 예외 처리

값 대체

  • 소프트웨어가 잘못된 값을 생성하거나 전체가 종료될 위험이 있을 경우 안전한 다른 값으로 대체한다.
  • 대체 값이 실제로 안전한 옵션인 경우에만 신중하게 선택해야 한다. 이것은 견고성과 정확성 간의 트레이드오프이다.
  • 다른 방향으로, 보다 안전한 방법을 택하자면 제공되지 않은 데이터에 기본 값을 사용하는 방법이 있다. 설정되지 않은 환경 변수나 함수의 파라미터, 사전의 값 등에 접근할 때에는 기본값을 설정할 수 있다.
config = {"dbport": 5432}
config.get("dbhost", "localhost")
>>> 'localhost'
  • 일반적으로 이러한 방법은 문제가 없지만 오류가 있는 데이터를 유사한 값으로 대체하는 것은 더 위험하며, 일부 오류를 숨겨버릴 수 있다.

예외 처리

  • 함수는 심각한 오류에 대해 명확하고 분명하게 알려줘서 적절하게 해결할 수 있도록 해야 한다.

  • 예외를 사용하는 경우 프로그램의 흐름을 읽기 어려워진다고, go-to문을 사용하여 예외를 처리하는 것은 상황을 악화시킨다. 호출 스택의 여러 수준에서 사용되면 논리를 캡슐화하지 못할 수 있다.

  • 프로그램이 꼭 처리해야 하는 예외적인 비즈니스 로직을 except 블록과 혼합하여 사용하지 말자. 이렇게 하면 유지보수가 필요한 핵심 논리와 오류를 구별하는 것이 어려워진다.

  • 예외는 캡슐화를 약화시킨다. 함수에 예외가 많을수록 호출자가 함수에 대해 더 많은 것을 알아야 한다. 예외가 너무 많이 발생하면 여러 개의 작은 것으로 나눠야한다는 신호일 수 있다.

    → 많은 예외는 약한 캡슐화의 증상인 것 같다.

파이썬의 예외와 관련된 권장사항들

  • 올바른 수준의 추상화 단계에서 예외 처리: 예외는 오직 한 가지 일을 하는 함수의 한 부분이어야 한다. 함수가 처리하는 예외는 캡슐화된 로직과 일치해야 한다.
class DataTransport: 
    """다른 레벨에서 예외를 처리하는 예"""

    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)
  • 먼저 ConnectionError는 connect 메서드 안에서 처리되어야 한다.
  • 또한 ValueError는 event의 decode 메서드와 관련된 에러이다.
  • 위 예외들을 분리하면, deliver_event에서는 예외를 catch할 필요가 없다.
def connect_with_retry(connector, retry_n_times, retry_threshold=5): 
    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: 
    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
  • 두 가지 에러를 분리한 메서드는 훨씬 더 작고 읽기 쉽다.

Traceback 노출 금지

  • 보안을 위한 고려 사항
  • 특정 문제를 나타내는 예외가 있는 경우 문제 해결을 위해 traceback 정보, 메세지 등을 로그로 남기는 것이 중요하지만, 세부사항은 절대 사용자에게 보여서는 안된다.
  • 문제를 알리려면 무엇이 잘못되었다거나, 페이지를 찾을 수 없다는 등의 일반적인 메시지를 사용해야 한다.

비어있는 except 블록 지양

  • 가장 악마같은 패턴..
try:
    process_data()
except:
    pass
  • 위와 같은 코드는 실패해야만 할 때조차도 실패하지 않는다.
  • except를 비워두는 대신, 구체적인 예외를 사용하고 except 블록에서 실제 오류 처리를 해야 한다.

원본 예외 포함

  • 오류 처리 과정에서 다른 오류를 발생시키고 메시지를 변경하는 경우, 원래 예외를 포함하는 것이 좋다.
  • 기본 예외를 사용자 정의 예외로 래핑하고 싶다면, 루트 예외에 대한 정보를 다음과 같이 포함할 수 있다.
class InternalDataError(Exception):
    """
        업무 도메인 데이터의 예외
    """

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

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

  • 어설션은 절대로 일어나지 않아야 하는 상황에 사용되므로 assert문에 사용된 표현식은 불가능한 조건을 의미한다.
  • 이것은 프로그램이 더 큰 피해를 입지 않도록 하는 것이 목적이며, 비즈니스 로직과 섞거나 소프트웨어의 제어 흐름 메커니즘으로 사용해서는 안된다.
  • 어설션에 실패하면 반드시 프로그램을 종료시켜야 하고, 다른 함수를 호출하면 안된다.
  • 다음과 같이 유용한 정보를 추가하는 것이 좋다.
result = condition.holds()
assert result > 0, f"에러 {result}"

관심사의 분리


  • 책임이 다르면 컴포넌트, 계층 또는 모듈로 분리되어야 한다.
  • 관심사를 분리하는 목표는 파급 효과를 최소화하여 유지보수성을 향상시키는 데에 있다.
    • 파급 효과? 어느 지점에서의 변화가 전체로 전파되는 것
    • 작은 수정이 코드의 여러 부분에 영향을 미치면 안된다.
  • 관심사가 계약에 의해 시행될 수 있다는 점에서 DbC 원칙과도 관련이 있지만, 관심사의 분리는 더 큰 내용이다.
  • 함수, 메서드, 클래스 정도 규모에서는 계약과 관심사의 분리를 모두 생각할 수 있지만, 관심사의 분리는 기본적으로 파이썬 모듈, 패키지 그리고 소프트웨어 컴포넌트에 대해 적용되는 개념이다.

응집력과 결합력

  • 훌륭한 소프트웨어 설계를 위한 중요 개념
  • 응집력: 객체가 작고 잘 정의된 목적을 가져야 하며, 가능하면 작아야 한다.
  • 결합력: 두 개 이상의 객체가 서로 얼마나 의존하는지.
    • 객체 또는 메서드의 두 부분이 서로 너무 의존적인 경우, 재사용성이 낮아지고 파급 효과가 커지며, 관심사가 분리되어있다고 보기 어렵다.
  • 잘 정의된 소프트웨어는 높은 응집력과 낮은 결합력을 갖는다.

개발 지침 약어


  • 좋은 디자인 아이디어를 주는 개발 원칙

DRY/OAOO

  • Do not repeat yourself, Once and only once
  • 코드에 있는 지식은 단 한 번, 한 곳에 정의되어야 한다. 코드를 변경할 때 수정이 필요한 곳은 단 한군데만 있어야 한다. 그렇지 않다는 것은 잘못된 시스템의 징조이다.
  • 코드 중복이 발생하는 경우, 오류가 발생하기 쉬워지며 변경하는 데에 비용이 많이 들고 신뢰성이 떨어진다.
  • 중복을 제거하기 위해서는 다음과 같은 방법을 사용할 수 있다.
    • 함수 생성
    • 새로운 객체 생성
    • 컨텍스트 관리자 사용
    • 이터레이터/제너레이터
    • 데코레이터
  • 어떤 방법이 가장 좋은지 알려주는 일반적인 패턴은 없지만, 공부하다 보면 알게될것이다.

YAGNI

  • You ain't gonna need it
  • 미래의 모든 요구사항을 고려하여 복잡한 솔루션을 만들지 말자.
  • 자칫하면 미래의 요구사항도 현재의 요구사항도 제대로 처리하지 못하게 될 수 있다.
  • 유지보수가 가능한 소프트웨어를 만든다는 것은 오직 현재의 요구사항을 잘 해결하기 위한 소프트웨어를 나중에 수정하기 쉽도록 작성하는 것이다.

KIS

  • Keep it simple
  • 문제를 해결하는 최소한의 기능을 구현하고, 필요한 것 이상으로 솔루션을 복잡하게 만들지 말자.
  • 디자인이 단순할수록 유지 관리가 쉽다.
  • 코드부터 컴포넌트까지 모든 추상화 수준에서 염두에 두어야 하는 원칙

EAFP/LBYL

  • Easier to Ask Forgiveness than Permission, Look before you leap
  • EAFP: 일단 코드를 실행하고 실제 동작하는 경우에 대응한다. 일반적으로 코드를 실행하고 예외를 catch하는 형태
  • LBYL: 도약하기 전에 먼저 무엇을 사용하려고 하는지 확인하자.
  • 파이썬은 EAFP 방식으로 만들어졌으며, 그렇게 할 것을 권한다.

컴포지션과 상속


  • 객체 지향 소프트웨어를 디자인할 때 가장 일반적으로 사용되는 개념
  • 강력하지만 부모 클래스를 확장하여 새로운 클래스를 만들 때마다 부모와 강력하게 결합된 새로운 클래스가 생긴다는 위험이 있다.
  • 단지 부모 클래스에 있는 메서드를 공짜로 쓰기 위해 상속을 하는 것은 좋지 않은 생각이다.

상속이 좋은 선택인 경우

  • 하위 클래스를 만들 때 클래스가 올바르게 정의되었는지 확인하려면, 상속된 모든 메서드를 실제로 사용할 것인지 생각해보는 것이 좋다.
  • 대부분의 메서드를 필요로 하지 않거나, 재정의해야 한다면 설계상의 실수라고 할 수 있다.
  • public 메서드와 속성 인터페이스를 정의한 컴포넌트가 있고, 이 클래스의 기능을 물려받으며 추가 기능을 더하거나 특정 기능을 수정하는 경우는 상속을 잘 사용한 좋은 예다.
  • 인터페이스 정의 또한 상속의 좋은 예이다. 어떤 객체에 인터페이스 방식을 강제하기 위해, 구현하지 않은 기본 추상 클래스를 만들고 이 클래스를 상속받아 하위 클래스에서 구현하도록 할 수 있다.

상속 안티패턴

  • 코드 재사용만을 목적으로 상속을 사용하면 안된다.
  • 상속된 메서드는 새로운 클래스의 일부가 되기 때문에, 클래스의 public 메서드는 부모 클래스가 정의하는 것과 일치해야 한다.
class TransactionalPolicy(collections.UserDict):
    def change_in_policy(self, customer_id, **new_policy_data):
        self[customer_id].update(**new_policy_data)
  • 위 예시는 단순히 고객 id로 필요한 정보에 접근하기 위해 UserDict를 상속했고, 영영 사용하지 않은 불필요한 메소드까지 상속받게 된다.
  • 이것이 구현 객체를 도메인 객체와 혼합할 때 발생하는 문제이다. TransactionalPolicy는 특정 도메인의 정보를 나타내는 것이므로, 사전 객체는 오로지 정보를 저장하기 위해서만 사용되어야 한다.
  • 해결책은 컴포지션을 사용하여, 사전을 private 속성에 저장하고 __getitem__으로 사전의 프록시를 만든 후 나머지 메소드를 추가로 구현하는 것이다.
class TransactionalPolicy:
    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라는 알고리즘을 사용하여 메소드 충돌이 발생하지 않도록 한다.
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):
    pass
    
class ConcreteModuleB23(BaseModule2, BaseModule3):
    pass
str(ConcreteModuleA12('test'))
>>> 'module-1:test'
[cls.__name__ for cls in ConcreteModuleA12.mro()]
>>> ['ConcreteModuleA12', 'BaseModule1', 'BaseModule2', 'BaseModule', 'object']
  • 위와 같이 mro() 를 통해 메소드 참조 순서를 알아볼 수 있다.

믹스인

  • 코드를 재사용하기 위해 일반적인 행동을 캡슐화해놓은 기본 클래스
  • 보통 다른 클래스와 함께 믹스인 클래스를 다중 상속하여 믹스인에 있는 메소드나 속성을 사용한다.
class BaseTokenizer:
    def __init__(self, str_token):
        self.str_token = str_token
    
    def __iter__(self):
        yield from self.str_token.split('-')
  • 위와 같이 문자열을 받아 쪼개주는 함수에 값을 대문자로 변환하는 로직을 추가하려면 어떻게 하는 것이 좋을까?
  • 기본 클래스를 변경하는 경우 그 클래스를 상속한 클래스들의 기능도 모두 변경된다.
  • 이 경우 믹스인을 이용하면 좋다.
class UpperIterableMixin:
    def __iter__(self):
        return map(str.upper, super().__iter__())
    
class Tokenizer(UpperIterableMixin, BaseTokenizer):
    pass
Tokenizer.mro()
>>> 
[__main__.Tokenizer,
 __main__.UpperIterableMixin,
 __main__.BaseTokenizer,
 object]

함수와 메서드의 인자


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

  • 파이썬이 파라미터를 처리하는 과정을 먼저 이해해보자.

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

  • 파이썬의 모든 인자는 값에 의해 전달된다.
  • 만약 변형 가능한(mutable) 객체를 전달하고 함수에서 값을 변경하면 함수 반환 시 실제 값이 변경될 수 있다.
def function(arg):
    arg += " in function"
    print(arg)
>>> immutable = "hello"
function(immutable)
>>> hello in function
immutable
>>> 'hello'
  • string 객체는 불변형이므로, 값을 수정하려고 하면 새로운 객체를 만들어서 다시 arg에 할당한다.
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']
  • 변형 객체인 리스트를 전달하면, 리스트 객체에 대한 참조를 보유하고 있는 변수를 통해 값을 직접 수정한다.
  • 따라서 부작용을 피하기 위해서는 함수 인자를 변경하지 않는 것이 좋다.
  • 또한 파이썬 인자는 위치 기반으로 호출하거나 키워드 기반으로 호출할 수 있다.
  • 키워드 인자로 호출한다면 이후의 파라미터도 모두 키워드 인자 방식으로 호출해야 한다.

가변인자

  • 파이썬에서는 패킹/언패킹을 이용해 가변인자를 사용할 수 있다.
  • 패킹할 변수의 이름 앞에 *를 사용하면 된다.
def show(e, rest):
    print(f'요소: {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
>>> []
  • 언패킹할 부분이 없다면 결과는 비어있게 된다.
  • 일련의 요소를 반복해야 하고, 각 요소가 차례대로 있다면 요소를 반복할 때 언패킹하는 것이 좋다.
  • 비슷한 표기법으로, 사전에 이중 별표를 사용하여 함수에 전달하면 파라미터의 이름으로 키를 사용하고, 파라미터의 값으로 사전의 값을 사용한다.
def func(key):
    print(key)
d = {'key':'value'}
func(**d)
>>> value
  • 반대로 이중 별표로 시작하는 파라미터를 함수에 사용하면 키워드 제공 인자들이 사전으로 패킹된다.
def func(**key):
    print(key)
func(key='value')
>>> {'key': 'value'}

함수 인자의 개수

  • 너무 많은 인자를 사용하는 함수나 메서드가 왜 나쁜 디자인인지 살펴보고, 이 문제를 해결할 방법을 알아보자.

함수 인자와 결합력

  • 함수 서명의 인수가 많을수록 호출자 함수와 밀접하게 결합될 가능성이 커진다.
  • 이와 같은 메서드가 있다는 것은 일반적으로 새로운 상위 레벨의 추상화 객체를 필요로 하거나, 누락된 객체가 있음을 의미한다.

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

  • 너무 많은 인자를 사용하는 함수를 찾았다면, 어떻게 리팩토링하는 것이 좋을까?
    • 공통 객체에 파라미터 대부분이 포함되어있는 경우 공통 객체를 파라미터로 전달한다.
    • 파라미터 그룹핑: 하나의 객체에 파라미터들이 담겨있지 않다면 새로운 객체를 만들어 담아주자.
    • 가변인자나 키워드 인자를 사용하여 동적 서명을 가진 함수를 만든다. 가변인자를 사용하는 경우 다양한 인자를 허용하되 착오가 없도록 docstring을 잘 만들어둔다.

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


  • 좋은 소프트웨어를 디자인하기 위한 최종 권장사항

소프트웨어의 독립성(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))
  • 위 예시에서 위쪽 두개의 함수는 똑같이 가격을 처리하지만 독립성을 갖는다. 하나는 가격을 계산하고, 하나는 가격을 표현한다. 둘 중 하나를 변경해도 다른 함수에는 영향을 주지 않는다.
  • 마지막 함수는 문자열 변환을 기본 표현 함수로 사용하고, 사용자 정의 함수를 전달하면 그 함수를 이용해 문자열을 포맷한다. 이 함수 또한 어떤 가격 계산 로직도 포함하지 않으면서 다른 포맷을 추가할 수 있으므로 잘 분리되어 있다고 할 수 있다.
  • 이처럼 코드의 각 부분들을 독립적으로 관리하면, 수정뿐만 아니라 테스트도 쉬워진다. 변경된 부분의 단위 테스트는 나머지 단위 테스트와도 독립적이다. 따라서 기능 수정 후에도 간단한 테스트 후 배포할 수 있다.

코드 구조

  • 여러 정의가 들어있는 큰 파일을 만드는 것은 좋지 않다. 좋은 코드라면 유사한 컴포넌트끼리 정리하여 구조화해야 한다.
  • 이를 위해 __init__.py 파일을 이용하여 패키지를 만들 수 있다. 이 파일과 함께 정의를 포함하는 여러 파일을 생성하면, 각각의 기준에 맞춰 파일 당 적은 클래스와 함수를 가지게 된다.
  • 파이썬 3부터는 __init__.py를 정의하지 않아도 폴더 구조는 자동으로 패키지로 인식되지만, 호환을 위해 사용하는게 좋다.
  • 이렇게 패키지를 쪼개면 모듈을 임포트할 때 구문을 분석하고 메모리에 로드할 객체가 줄어들며, 더 적은 모듈을 가져올 수 있다.
  • 또한 상수 값을 모아 저장할 특정한 파일을 만들어 임포트하는 등 프로젝트 컨벤션에도 도움이 된다.

요약


  • 컴포넌트뿐만 아니라 코드도 디자인의 일부이다.
  • 계약에 의한 디자인을 적용하면 주어진 조건 하에 동작이 보장되는 컴포넌트를 만들 수 있으며, 디버깅에도 유리하다.
  • 방어적 프로그래밍을 이용하면 잘못된 입력으로부터 스스로를 보호할 수 있다.
  • 어떤 디자인 원칙을 사용하든 어설션을 올바르게 사용해야 한다. 어설션은 프로그램의 흐름을 제어하는 용도로 사용하면 안된다.
  • 예외는 언제 어떻게 사용해야 하는지 알아야 한다.
  • 객체 지향 디자인에서는 상속 또는 컴포지션을 사용할 수 있다. 또한 코드 재사용만을 목적으로 상속을 이용하는 등 안티패턴을 피해야 한다.
  • 가변 인자 파라미터에는 적절한 문서화가 필요하며, 너무 많은 인자는 피하는 것이 좋다.
profile
developer hamdoe

0개의 댓글