[ python ] 03. 좋은 코드의 일반적인 특징_(2)

박찬영·2024년 3월 29일

파이썬 클린 코드

목록 보기
9/19
post-thumbnail

파이썬 클린 코드

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

관심사의 분리

관심사 분리는 책임이 다르면 컴포넌트, 계층 또는 모듈로 분리되어야 한다는 것이다. 프로그램의 각 부분은 기능의 일부분(관심사)에 대해서만 책임을 지며 나머지 부분에 대해서는 알 필요가 없다.

소프트웨어 디자인에서 관심사 분리의 목표는 파급 효과를 최소화하여 유지보수성을 향상시키는 것이다. 파급(ripple) 효과는 어느 지점에서의 변화가 전체로 전파되는 것을 의미한다. 이러한 오류나 예외는 다른 예외를 유발하거나 혹은 먼 지점의 결함을 초래한다. 함수 정의를 약간만 변경해도 코드의 여러 부분에 영향을 미쳐 많은 코드를 변경해야 할 수도 있다.

소프트웨어는 쉽게 변경될 수 있어야 한다. 애플리케이션의 나머지 부분에 대한 영향성을 최소화하면서 코드를 수정하거나 리팩토링을 하고 싶다면 적절한 캡슐화가 필요하다.

이 개념은 각 관심사가 계약에 의해 시행될 수 있다는 점에서 DbC 원칙과 비슷하다. 계약에 위배되는 행동으로 예외가 발생하면 프로그램이 어떤 부분이 실패했는지 그리고 어떤 책임을 이행하지 못했는지 알 수 있기 때문이다.

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

응집력과 결합력은 소프트웨어 설계를 위한 중요한 개념이다. 응집력이란 객체가 작고 잘 정의된 목적을 가져야 하며 가능하면 작아야 한다는 것을 의미한다. 객체의 응집력이 높을수록 더 유용하고 재사용성이 높아지므로 더 좋은 디자인이다. (유닉스 명령어가 한 가지 일만 잘 수행하라는 철학을 가진 것과 유사)

결합력이란 두 개 이상의 객체가 서로 어떻게 의존하는지를 나타낸다. 이 종속성은 제한을 의미한다. 객체 또는 메서드의 두 부분이 서로 너무 의존적이라면 다음과 같이 바람직하지 않은 결과를 가져온다.

  • 낮은 재사용성
    만약 어떤 함수가 특정 객체에 지나치게 의존하는 경우 이 함수는 해당 객체에 결합되게 된다. 즉 다른 상황에서는 이 함수를 사용하기가 어렵다는 뜻이다.
  • 파급 효과
    너무 의존적인 경우 한 가지를 변경하면 다른 부분에도 영향을 미친다.
  • 낮은 수준의 추상화
    두 함수가 너무 가깝게 관련되어 있으면 서로 다른 추상화 레벨에서 문제를 해결하기 어렵다.

일반적으로 잘 정의된 소프트웨어는 높은 응집력과 낮은 결합력을 가진다

개발 지침 약어

이번에는 좋은 디자인 아이디어를 주는 몇 가지 원칙을 살펴본다. 요점은 좋은 소프트웨어 관행을 약어를 통해 쉽게 기억하자는 것이다.

DRY(Do not Repeat Yourself)/OAOO(Once and Only Once)

DRY와 OAOO는 밀접한 관련이 있어 함께 다룬다. 한 마디로 중복을 반드시 피하라는 이야기이다.
코드에 있는 지식은 단 한번, 단 한 곳에 정의되어야 한다. 코드를 변경하려고 할 때 수정이 필요한 곳은 단 한군데만 있어야 한다.

중복은 기존 코드의 지식을 무시함으로써 발생한다. 코드의 특정 부분에 의미를 부여함으로써 해당 지식을 식별하고 표시할 수 있다.

만약 연구 센터에서 학생들을 다음과 같은 기준, 시험 통과 11점, 시험 통과 실패 -5점, 1년이 지날 때마다 -2점의 기준으로 평가한다고 가정해보자. 나쁜 코드로 어떻게 코드가 중복될 수 있는지 나타내면 다음과 같다.

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(
        "name:{0}, score:{1}".format(
            student.name,
            (student.passed*11- student.failed*5 - student.years*2),
        )
    )

이 코드에서는 특별히 할당된 이름이 있는 코드 블록도 없고 어떤 의미도 부여하지 않았다. 코드에서 의미를 부여하지 않았기 때문에 순위를 출력할 때 중복이 발생한다.

위 코드에서 중복을 해결하기 위해서 의미를 부여해보면 다음과 같이 작성할 수 있다.

def score_for_student(student):
  return student.passed*11- student.failed*5 - student.years*2

def process_students_list(students):
  ...
  students_ranking = sorted(students, key = score_for_student)
  # 학생별 순위 출력
  for student in students_ranking:
    print(
        "name:{0}, score:{1}".format(
            student.name,
            score_for_student(student)
            )
    )

위 예제에서는 중복을 제거하는 가장 간단한 방법인 함수 생성 기법을 사용했다.

YAGNI(You Ain't Gonna Need it)

이는 과잉 엔지니어링을 하지 않기 위해 계속 염두에 두어야 하는 원칙이다.
우리의 목표는 프로그램을 쉽게 수정하여 미래 보장성이 높은 코드를 작성하고자 하는 것이다.
그러나 많은 개발자는 미래의 모든 요구사항을 고려하여 매우 복잡한 솔루션을 만들고, 유지보수가 어려운 코들를 작성한다. 이는 미래 보장성이 높은 코드의 의미를 미래 요구사항을 예측하여 미리 만들어 놓는 것이라 생각해서 그렇다. 미래 보장성이 높다는 것은 현재의 요구사항을 잘 해결하기 위한 소프트웨어를 작성하고 나중에 수정하기 쉽도록 작성하는 것이다.

예를 들어 어떤 행동을 캡슐화하는 객체를 만들려고 한다고 해보자. 지금 당장이 요구사항을 구현한 클래스를 만들었지만 이것을 공통으로 하는 다른 클래스가 생길 것으로 예상되어 인터페이스로 만들고, 해당 인터페이스를 구현하도록 변경할 수 있다. 이것은 먼저 지금 당장 필요한 것은 처음에 생성한 클래스이지만, 나중에 필요할지 모르는 기능을 위해 과도하게 일반화를 위한 시간 투자가 좋은 방법이 아니다.
그리고 지금 생성한 클래스는 현재의 요구사항에 편향되었을 가능성이 높으므로 올바른 추상화가 되지 않았을 가능성이 높다.

가장 좋은 방법은 현재 필요한 것만 작성하는 것이고, 나중에 새로운 요구 사항이 발생하면 베이스 클래스를 만들고 일부 메서드를 추상화할 수 있다.

YAGNI는 상세 코드 수준에서 뿐만 아니라 전체적인 소프트웨어 아키텍처 수준에서도 적용되는 아이디어이다.

KIS(Keep It Simple)

이는 이전 YAGNI 원칙과 비슷하다.
소프트웨어 컴포넌트를 설계할 때, 선택한 솔루션이 문제에 적합한 최소한의 솔루션인지 자문해보자
문제를 올바르게 해결하는 최소한의 기능을 구현하고 필요한 것 이상으로 솔루션을 복잡하게 만들지 않도록 해야 한다.

디자인은 단순할 수록 유지 관리가 쉽다.

다음 클래스는 키워드 파라미터에서 제공된 값들을 속성으로 초기화하는데 다소 복잡한 구조로 되어 있다.

class ComplicateNamespace:
  """ 프로퍼티를 복잡한 방식으로 초기화하는 객체 """

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

  @classmethod
  def init_with_data(cls,**data):
    instance = cls()
    for key,value in data.item():
      if key in cls.ACCEPTED_VALUES:
        setattr(instance,key,value)
    return instance

객체를 초기화하기 위해 추가적인 클래스 메서드를 만드는 것은 꼭 필요해 보이지 않는다. 반복을 통해 setattr을 호출하는 것은 상황을 더 이상하게 만든다. 사용자에게 노출된 인터페이스 또한 분명하지 않다.

사용자는 초기화를 위해 init_with_data라는 일반적이지 않은 메서드의 이름을 알아야 하는데, 이것 또한 불편한 부분이다. 파이썬에서 다른 객체를 초기화 할 때는 __init__ 메서드를 사용하는 것이 훨씬 간편할 것이다.

class Namespace:
  """ 키워드 인자를 사용하여 객체를 생성"""
  ACCEPTED_VALUES = ("id_","user","location")

  def __init__(self,**data):
    for attr_name, attr_value in data.item():
      if attr_name in self.ACCEPTED_VALUES:
        setattr(self, attr_name, attr_value)

파이썬 철학을 기억하자 : 단순한 것이 복잡한 것보다 낫다.
또한 코드를 단순하게 유지하기 위해 메타 클래스와 같은 파이썬의 고급 기능을 사용하는 것은 피하는 것이 좋다. 왜냐하면 이런 기능이 필요한 경우는 매우 한정적일 뿐만 아니라 이런 고급 기능을 사용하면 코드를 읽기가 훨씬 어려워지고 유지보수 또한 어려워지기 때문이다.

EAFP(Easier to Ask Forgiveness than Permission) / LBYL(Look Before You Leap)

허락보다는 용서를 구하는 것이 쉽다. (무엇을 하기 위해 미리 허락을 구하는 것 보다는 실행한 뒤에 발생하는 오류에 대해서 용서를 구하는 것이 쉽다는 뜻이다.) 반면에 LBYL은 도약하기 전에 미리 살피라는 뜻이다.

EAFP는 일단 코드를 실행하고 실제 동작하지 않을 경우에 대응한다는 뜻이다. 일반적으로는 EAFP 방식으로 구현하면 일단 코드를 실행하고 발생한 예외를 catch하고 except 블록에서 바로잡는 코드를 실행하게 된다.

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

LBYL은 그 반대이다 도약하기 전에 먼저 무엇을 사용하려고 하는지 확인한다. 예를 들어 파일을 사용하기 전에 먼저 파일을 사용할 수 있는지 확인하는 것이다.

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

특수한 경우에 LBYL 방식이 필요한 경우가 있지만, 대부분의 경우 EAFP 방식이 보다 더 의미를 명확하게 드러낸다. 사전에 검증하는 대신에 예외 처리가 필요한 부분으로 바로 이동하기 때문에 가독성이 높다.

profile
안녕하세요 박찬영입니다.

1개의 댓글

comment-user-thumbnail
2024년 3월 29일

상속 파트도 기대할게요!

답글 달기