📚 독서를 합시다

CH3. 좋은 코드의 일반적인 특징

반응형

내용은 "유지보수가 쉬운 파이썬 코드를 만드는 비결" 이라는 책을 보고 요약 정리한 내용입니다.

목표

  • 견고한 소프트웨어의 개념
  • 작업 중 잘못된 데이터를 다루는 방법
  • 새로운 요구사항을 쉽게 수용하고 확장할 수 있는 유지보수가 쉬운 SW 설계
  • 재사용 가능한 SW 설계
  • 생산성을 높이는 효율적인 코드 작성

계약에 의한 디자인

  • 디자인 바이 컨튜렉
  • 컴포넌트는 기능을 숨겨 캡슐화하고 이를 사용할 수 있도록 API(인터페이스)를 노출해야 한다.
  • API를 디자인할 때에는 예상되는 입출력과 부작용을 문서화해야 한다. 이때 계약 이라는 개념이 생긴다.
def divide(a: int, b: int) -> float:
    """
        Divides a by b.

    Preconditions:
    - b must not be 0

    Postconditions:
    - returns a value which is the result of a/b
    """
    if b == 0:
        raise ValueError("Divider (b) should not be 0.")
    return a / b

계약에 의한 디자인을 하는 이유?

  • 오류를 쉽게 찾아낼 수 있기 때문이다.
  • 사전조건사후조건 에 대한 계약이 존재하므로, 어떤 곳에서 계약을 위반하고 있는지 쫓아간다면 오류를 더 쉽게 발견할 수 있다.
  • 또한, 코드의 핵심 부분이 실행되는 것을 방지하기 위함이다.
    • 예를들어.. 잘못된 요청이지만 DB까지 도달해서 값을 업데이트 한다거나, 삭제한다거나.. 등등

DbC 결론

  • 이 원칙을 따르면 코드는 견고해진다.
  • 하지만 이 원칙을 따르려면 추가 작업 이 발생한다.
    • 핵심 로직 뿐만 아니라, 계약을 작성하기 위한 문서 등 도 필요하기 때문이다. 또한 이러한 계약에 대한 단위 테스트를 추가해야할 수도 있기 때문이다.
  • 추가 작업 이 존재하는 만큼 품질은 보장된다.

 

방어적 프로그래밍

⇒ 이는 DbC와는 다소 다른 접근 방식을 따른다.
⇒ 다른 디자인 원칙과 서로 보완 관계에 있을 수 있다는 것

def safe_divide(a: int, b: int) -> Union[float, None]:
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero!")
        return None

에러 헨들링

⇒ 주요 목적은 예상되는 에러에 대해 실행을 계속 할 수 있을지 아니면 극복할 수 없는 오류여서 프로그램을 중단할지 결정하는 것

⇒ 에러 핸들링의 방법으로는..

  • 값 대체
    • 결과 값을 안전한 다른 값으로 대체하는 방법
    • 하지만 값 대체를 결정하기 위해서는 견고성정확성 간의 트레이드 오프를 계산해보아야 한다.
      • 우리는 프로그램이 절대 죽어서는 안돼! → 견고성
      • 우리는 민감하고 중요한 정보를 다루기 때문에 부정확한 결과를 그대로 내보내면 안돼! → 정확성
  • 에러 로깅
try:
	self.connect()
	data = event.decode()
	self.send(data)
except ConnectionError as e:
	logger.info("커넥션 오류: %s", e)
	raise
except ValueError as e:
	logger.info("%r 이벤트에 잘못된 데이터 포함: %s", event, e)
	raise
  • 예외 처리
    • DbC에서 보았듯이 사전조건 검증에 실패한 경우
    • 예외 매커니즘을 활용해 예외 상황을 명확하게 알려주고, 원래의 비즈니스 로직의 흐름을 유지하는 것이 중요
    • 예외를 go-to 문 처럼 사용해서는 안 된다. 즉, 호출자가 알아야하는 실질적인 문제에 대하여만 예외를 발생시켜야 함. (위의 예시 코드 참고)
    • 함수가 너무 많은 예외를 발생시킨다는 것은 문맥에서 자유롭지 못한다는 것을 의미한다.
      따라서, 여러 개의 작은 기능으로 나눌 수 있는지 검토해보자~
    • 엔드 유저에게는 Traceback 노출을 금지하도록 하자!
      • 예외가 발생하여 사용자에게 문제를 알리려면, “알 수 없는 문제가 발생했다.” 또는 “페이지를 찾을 수 없습니다.”와 같은 일반적인 메시지를 사용하도록 하자
    • 비어있는 try-except 문은 파이썬스러운 코드가 아니다. 이러한 블록은 지양하도록 하자~

 

관심사의 분리

⇒ 목표는 파급 효과를 최소화하여 유지 보수성을 향상시키는데 있다.

⇒ DbC 원칙과 비슷하지만, 관심사의 분리는 좀 더 큰 내용이 포함되어진다.

응집력과 결합력

# 높은 응집력의 예시
class Calculator:
    def add(self, x, y):
        return x + y

    def subtract(self, x, y):
        return x - y

# 낮은 결합력의 예시
class TaxCalculator:
    def __init__(self, tax_rate):
        self.tax_rate = tax_rate

    def apply_tax(self, amount):
        return amount + (amount * self.tax_rate)
  • 응집력
    • 객체가 작고 잘 정의된 목적을 가져야 함
    • 가능하면 작아야 함
    • 마치 유닉스 명령어처럼 한 가지 일만 잘 수행하도록
  • 결합력
    • 두 개 이상의 객체가 서로 어떻게 의존하는지
    • 너무 의존적이라면?
      • 낮은 재사용성 초래
      • 파급 효과를 불러일으킴
      • 낮은 수준의 추상화

 

개발 지침 약어

DRY / OAOO

# 반복되는 코드
def area_square(side_length):
    return side_length * side_length

def area_rectangle(width, height):
    return width * height

# DRY 원칙 적용
def area_rectangle(width, height):
    return width * height

def area_square(side_length):
    return area_rectangle(side_length, side_length)
  • 두 낫 리핏 유어셆 / 원스 앤 온리 원스
  • 중복을 반드시 피하자
  • 코드를 변경하려고 할 때 수정이 필요한 곳은 단 한군데만 있어야 한다.

 

YAGNI

# 불필요하게 복잡한 함수
def add(x, y, z=None):
    if z:
        return x + y + z
    return x + y

# 간단하게 변경
def add(x, y):
    return x + y
  • 유 에인트 가나 니 딧
  • 과잉 엔지니어링을 하지 않기 위해 계속 염두하자.
  • 우리는 미래학자가 아니기 때문에 미래의 모든 요구사항을 고려하여 이해하기 어려운 코드를 만들진 말자
  • 굳이 필요 없는 기능을 개발하지는 말라는 뜻.

 

KIS

# 복잡한 구현
def add(x, y):
    if isinstance(x, int) and isinstance(y, int):
        result = 0
        for _ in range(x):
            result += 1
        for _ in range(y):
            result += 1
        return result

# 간단한 구현
def add(x, y):
    return x + y
  • 디자인이 단순할수록 유지 관리가 쉽다.
  • 이 모듈은 정말 “지금 당장” 확장이 가능해야할까?
  • 일반적으로 파이썬에서 코드를 추상화하는 방법은 데코레이터를 사용하는 것

 

EAFP / LBYL

  • EAFP 수정 전
def _load_message_template(self, template_code: str) -> dict[Any, Any]:
    """템플릿 코드로 발송할 메시지 데이터를 로드"""

    file_path = os.path.join(
        settings.BASE_DIR, "utils", "gateway", "clients", "bizmsg_templates.yaml"
    )
    with open(file_path, "r", encoding="utf-8") as file:
        templates: Dict[str, Any] = yaml.safe_load(file)
    return templates.get(template_code, {})
  • 접근 방법 ⇒ EAFP (Easier to Ask Forgiveness than Permission)
  • 특징 ⇒ 파일을 직접 열려고 시도하나, 발생 가능한 예외들에 대한 명시적인 처리가 없음

 

  • EAFP 수정 후
import sentry_sdk

def _load_message_template(self, template_code: str) -> dict[Any, Any]:
    """템플릿 코드로 발송할 메시지 데이터를 로드"""

    file_path = os.path.join(
        settings.BASE_DIR, "utils", "gateway", "clients", "bizmsg_templates.yaml"
    )

    try:
        with open(file_path, "r", encoding="utf-8") as file:
            templates: Dict[str, Any] = yaml.safe_load(file)
        return templates.get(template_code, {})
    except FileNotFoundError:
        sentry_sdk.capture_exception()
        return {}
    except yaml.YAMLError:
        sentry_sdk.capture_exception()
        return {}
  • 접근 방법: EAFP (Easier to Ask Forgiveness than Permission)
  • 특징: 파일을 직접 열려고 시도하며, 발생 가능한 예외들 (FileNotFoundError, yaml.YAMLError)에 대한 명시적인 처리. 예외 발생 시 Sentry로 로깅

 

  • LBYL 버전?
import sentry_sdk

def _load_message_template(self, template_code: str) -> dict[Any, Any]:
    """템플릿 코드로 발송할 메시지 데이터를 로드"""

    file_path = os.path.join(
        settings.BASE_DIR, "utils", "gateway", "clients", "bizmsg_templates.yaml"
    )

    if not os.path.exists(file_path):
        sentry_sdk.capture_message(f"File {file_path} does not exist.")
        return {}

    try:
        with open(file_path, "r", encoding="utf-8") as file:
            templates: Dict[str, Any] = yaml.safe_load(file)
            return templates.get(template_code, {})
    except yaml.YAMLError:
        sentry_sdk.capture_exception()
        return {}
  • os.path.exists(file_path)를 사용하여 파일의 존재 여부를 먼저 검사
  • 파일이 존재하지 않을 경우, Sentry를 통해 메시지를 로깅하고 빈 딕셔너리 {}를 반환
  • 파일이 존재할 경우에만 파일을 열고, 이후 YAML 파싱 중 발생할 수 있는 예외(yaml.YAMLError)에 대한 처리도 포함.
반응형

'📚 독서를 합시다' 카테고리의 다른 글

"소프트웨어 장인"을 읽고 느낀점  (0) 2022.08.30