반응형
내용은 "유지보수가 쉬운 파이썬 코드를 만드는 비결" 이라는 책을 보고 요약 정리한 내용입니다.
목표
- 견고한 소프트웨어의 개념
- 작업 중 잘못된 데이터를 다루는 방법
- 새로운 요구사항을 쉽게 수용하고 확장할 수 있는 유지보수가 쉬운 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에서 보았듯이
관심사의 분리
⇒ 목표는 파급 효과를 최소화하여 유지 보수성을 향상시키는데 있다.
⇒ 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 |
---|