읽기일기

파이썬 클린 코드 – 3

좋은 코드의 일반적인 특징

계약에 의한 디자인

  • 컴포넌트는 기능을 숨겨 캡슐화하고 함수를 사용할 고객에게는 API를 노출해야 한다.
  • 컴포넌트의 함수, 클래스,. 메서드는 유의사항에 따라 동작해야 하며 그렇지 않을 경우 코드가 깨지게 된다.
  • 반대로 클라이언트는 특정 응답을 기대하며 이것과 다른 경우 함수 호출에 실패하게 된다.
  • 계약에 의한 디자인이란, 관계자가 기대하는 바를 암묵적으로 코드에 삽입하는 대신, 양측이 동의하는 계약을 먼저 한 다음, 계약을 어겼을 경우 명시적으로 왜 계속할 수 없는지 예외를 발생시키라는 것이다.
  • 여기서 계약은 소프트웨어 컴포넌트 간의 통신 중에 반드시 지켜져야 할 규칙을 강제하는 것이다.
    • 사전 조건: 코드가 실행되기 전에 체크해야하는 것들이다. 함수가 진행되기 전에 처리되어야 하는 모든 조건을 의미한다.
    • 문제는 유효성 검사를 어디할지인데, 클라이언트가 함수를 호출할 때 할 것인지, 함수가 자체적으로 검사할 것인지에 대한 문제가 있다.
    • 사후 조건: 함수 반환 값의 유효성 검사가 수행된다. 호출자가 기대된 것을 제대로 받았는지 확인한다.
    • 불변식: 함수가 실행되는 동안에 일정하게 유지되는 것을 나타낸다.
    • 부작용: 코드의 부작용을 docstring에 언급하기도 한다.
  • 이상적으로는 이런 부분들을 컴포넌트 계약서에 문서화하는것이 좋지만 사전 조건, 사후 조건은 코드 레벨에서 강제한다.
  • 사전 조건은 클라이언트와 관련이 있고, 사후 조건은 컴포넌트와 연관이 있다. 즉 책임 소재를 신속하게 파악할 수 있다.

방어적(defensive) 프로그래밍

  • 계약에 의한 디자인과는 달리 방어적 프로그래밍은 객체, 함수, 메서드 등 코드의 모든 부분을 유효하지 않은 것들로부터 스스로 보호할 수 있게 하는 것이다.

에러 핸들링

  • 에러 핸들링은 예상되는 에러에 대해 실행을 계속할 수 있을지 아니면 프로그램을 중단할지를 결정하는 것이다.

값 대체

  • 잘못된 값을 생성한 경우, 결과 값을 안전한 다른 값으로 대체할 수 있다.

  • 항상 값 대체가 가능하지는 않기 때문에 견고성과 정확성 간의 트레이드오프에서 결정을 하여야 한다.

예외 처리

  • 어떤 경우에는 호출자에게 실패했음을 알리는 것이 좋은 선택이다.
  • 단 예외 상황을 명확히 알려주고 원래의 로직을 따라 흐름을 유지하여야 한다. 예외를 사용하여 로직을 처리하려고 하면 프로그램의 흐름을 읽기 어려워진다.
  • 예외는 캡슐화를 약화시킨다. 함수에 예외가 많을 수록 호출자가 호출하는 함수에 대해 더 많은 것을 알아야 한다.

  • 에러는 발생한 곳에서 처리하도록 하자.

  • Traceback은 노출하는 것에 유의하자.
  • 비어있는 except 블록을 사용하지 말자

  • 위 코드는 절대 실패하지 않는다.
    • 보다 구체적인 예외를 사용하고, except 블록에서도 실제 오류 처리를 하자.
  • 기본 예외를 새로운 예외로 래핑할 경우, 원본 예외를 포함하도록 할 수 있다.

  • 이렇게 하면 원본 예외는 __cause__ 속성에 할당된다.

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

  • 절대로 일어나지 않아야 할 상황에 사용한다. 결함이 있다는 것을 의미한다.
  • Assertion을 로직과 섞거나 제어 흐름으로 사용하지 않도록 하자.

  • 위 예제는 좋지 않은 생각이다.
  • 또한 조건으로 함수가 사용되고 있는데, 이 또한 수정되어야 한다.

관심사의 분리

  • 프로그램의 각 부분은 책임이 다르면 컴포넌트, 계층, 모듈로 분리되어야 한다. 각 요소들은 기능의 일부분에 대해서만 책임을 지며 나머지 부분에 대해서는 알 필요가 없다.
  • 파급 효과를 최소화하기 위함이다.

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

  • 응집력이란 객체가 잘 정의된 목적을 가져야 하고 가능하면 작아야 한다는 것이다.
  • 응집력이 높을 수록 더 유용하고 재사용이 가능하다.
  • 결합력이란 두개 이상의 객체가 서로 어떻게 의존하는지를 나타낸다. 너무 의존적이면 다음과 같은 단점이 있다.
    • 낮은 재사용성: 함수가 특정 객체에 의존성이 큰 경우, 해당 객체와 결합된 상태가 되어서 다른 상황에서는 함수를 사용하기가 어려워진다.
    • 파급 효과: 둘중 하나를 변경하면 다른 부분에도 영향을 미친다.
    • 낮은 수준의 추상화: 서로 다른 추상화 레벨에서 문제를 해결하기 어렵기 때문에 관심사가 분리되어 있다고 보기 어렵다.

개발 지침 약어

DRY/OAOO

  • DRY(Do not Repeat Yourself), OAOO(Once and Only Once)
  • 중복을 반드시 피해야 한다. 코드에 있는 지식은 단 한번, 단 한 곳에 정의도어야 한다. 코드를 변경하려고 할 때 수정이 필요한 곳은 단 한군데만 있어야 한다.
    • 어떤 로직이든 코드 전체에 반복이 있다면, 수정을 할 때 하나라도 빠트리면 버그가 발생할 것이다.
    • 여러 곳에서 정의한 경우 변경하는데 더 많은 시간이 소요된다.
    • 여러 코드를 변경해야 하는 경우 사람이 모든 인스턴스의 위치를 기억해야 한다.

YAGNI

  • YAGNI(You Ain't Gonna Need It)
  • 미래의 모든 요구사항을 고려하여 매우 과잉 엔지니어링을 하지만, 요구사항이 동작하지 않거나 나타나더라도 예상과는 다르게 동작한다.
  • 하지만 이미 과잉 엔지니어링된 코드는 리팩토링이 어렵고 확장하는 것이 어려워진다.
  • 현재의 요구사항만을 잘 해결하기 위한 코드를 작성하자.

KIS

  • KIS(Keep It Simple)
  • 필요이상 솔루션을 복잡하게 만들지 않도록 하자. 단순할 수록 유지관리가 쉽다.

  • 이러한 코드는 사용자가 ini_with_data라는 일반적이지 않은 메서드 이름을 알아야 한다. __init__ 메서드를 사용하는 것이 간편하다.

EAFP/LBYL

  • EAFP(Easier to Ask Forgiveness than Permission)
    • 일단 코드를 실행하고 예외가 발생하면 cath해서 except 블록에서 바로 잡자.
  • LBYL(Look Before You Leap)
    • 실행하기 전에 먼저 무엇을 사용하려는지 확인하라는 것이다.
  • 파이썬은 LBYL에 가깝다. 암묵적인 것보다 명시적인 것이 좋다.

보다는

쪽에 낫다.

컴포지션과 상속

  • 상속은 강력한 개념이지만 위험도 있다. 부모와 강력하게 결합된 새로운 클래스가 생긴다는 점이다.
  • 가장 좋은 이득은 코드 재사용이지만, 단지 부모 클래스에 있는 메서드를 이용하기 위해서 상속을 하는 것은 좋지 않은 생각이다. 재사용을 위해서 응집력 높은 객체를 사용하는 방법도 있다.

상속이 좋은 선택인 경우

  • 만약 부모의 대부분의 메서드를 필요로 하지 않고 재정의하거나 대체해야 한다면 설계상의 실수라고 할 수 있다.
    • 상위 클래스는 너무 막연한 정의와 너무 많은 책임을 가졌다.
    • 하위 클래스는 상위 클래스의 적절한 세분화가 아니다.
  • 잘 사용한 좋은 예는, 기존의 컴포넌트의 기능을 그대로 물려 받으면서 추가 기능을 더하려는 경우, 또는 특정 기능을 수정하려는 경우

  • 인터페이스 정의 또한 상속의 좋은 예이다. 어떤 객체에 인터페이스 방식을 강제하고자 할 때 사용한다.
  • 새로운 예외를 만드는 것 또한 상속의 좋은 예이다.

상속 안티패턴

  • 부모 클래스는 파생 클래스의 일부를 공통적인 정의로 갖는다. 따라서 public 메서드는 부모 클래스가 정의하는 것과 일치해야한다.
  • 아래는 예이다.

  • 적절히 구현된 것 같지만 불필요한 수많은 메서드가 정의되어있다.
  • 문제점
    • 확장을 한다면 개념적으로 확장되고 세부적인 것이라는 의미해야하는데, 그렇지 않으며 이름만 보고 사전 타입의 확장이라는 것을 알기가 힘들다.
    • 새로운 클래스 TransactionalPolicy는 사전의 모든 메서드를 갖는데, 이런 메서드는 사용되지 않으며 오히려 부작용을 불러올 수 있다.
  • 따라서 기본 클래스에 추가되는, 그리고 보다 특화된 것을 구현할 때에만 확장을 해야 한다.
  • 올바른 해결책은 컴포지션을 사용하는 것이다. TransactionalPolicy 자체가 사전이 되는 것이 아니라 사전을 활용하는 것이다.

  • 이렇게 하면 추후 변경하려고 해도 인터페이스만 유지하면 사용자는 영향을 받지 않는다.

다중 상속

  • 다중 상속은 잘못 사용하면 더 큰 문제가 생길 수 있다. 따라서 9장에서 볼 새로운 패턴과 믹스인을 사용하도록 하고 있다.
  • 그 전에 복잡한 계층구조에서 메서드는 어떻게 결정되는지 살펴본다.

메서드 결정 순서

  • 최상위 클래스에서 두개 이상의 클래스를 확장하고, 확장된 클래스를 모두 상속받아 하나의 클래스를 확장할 경우, 어느 메서드가 호출되는지가 모호하다.
  • 다음과 같이 확인해보자

  • 파이썬에서는 C3 linearization 또는 MRO라는 알고리즘에 따라 메서드 호출 순서를 결정한다.
  • 혹은 예제 코드처럼 순서를 직접 출력해볼 수도 있다.

믹스인(mixin)

  • 믹스인은 코드를 재사용하기 위해 일반적인 행동을 캡슐화해놓은 기본 클래스이다.
  • 믹스인 클래스는 그 자체를 확장해서는 동작하지 않고, 다른 클래스와 함께 믹스인 클래스를 다중 상속하여 믹스인에 있는 메서드나 속성을 사용한다.

  • 믹스인에서 __iter__를 호출하고 다시 super() 호출을 통해 BaseTokenizer에 위윔하게 된다.

함수와 메서드와 인자

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

  • 모든 인자는 값에 의해 전달 (passed by a value) 된다.
  • 인자에 변경을 가하는 경우, 객체 타입에 따라 다르다. mutable 객체인 경우 반환 시 실제 값이 변형된다.
  • 따라서 예상치 못한 부작용을 막기 위해 가급적 함수 내에서 변형은 피하는게 낫다.

가변 인자

  • 가변 인자를 사용하려면 인자를 패킹할 변수에 이름 앞에 별표를 사용한다.

  • 부분적인 언패킹도 가능하다.

  • 언패킹의 좋은 예는 다음과 같다.

  • 사전에 이중 별표를 사용하여 인자로 전달하면 파라미터 이름으로 키를 사용하고 파라미터의 값으로 사전의 값을 사용한다 아래 두 코드는 같은 의미를 지닌다.

  • 반대로 함수 정의에 이중 별표를 사용하면 반대의 동작을 한다. 키워드로 제공된 인자들이 사전으로 패킹된다.

함수 인자의 개수

  • 너무 많은 인자를 사용하는 함수나 메서드는 나쁜 디자인의 징후이다.
  • 첫번째 대안은 여러 인자를 포함하는 새로운 객체를 만드는 것이다.
  • 두번째는 가변 인자나 키워드 인자를 사용하는 것인데, 이는 매우 동적이어서 유지보수하기가 어렵기 때문이다.
  • 만약 파라미터의 값에 대응하여 너무 많은 것들을 함수에서 처리하고 있다면 여러 작은 함수로 분리를 하라는 신호이다.

함수 인자와 결합력

  • 함수의 파라미터가 많을 수록 호출자 함수와 밀접하게 결합되어있을 가능성이 높다.
  • f1, f2가 있다고 하고 f2는 5개의 파라미터를 가진다고 하자.
  • 더 많은 파라미터를 사용하는 f2는 정상 동작을 위한 정보를 수집하는 것이 어려울 것이다.
  • f1가 f2를 호출하기 위한 정보를 다 가지고 있다고 한다면, f1는 이미 f2가 어떤 동작을 하는지 알고 있어 자체적으로 수행할 수도 있는 상태일 것이다. 즉 추상화가 부족한 상황이다.
  • 또한 f2는 다른 환경에서 사용하기가 어려워 f1에서만 사용되고, 재사용성이 떨어진다.

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

  • 너무 많은 파라미터를 사용하는 함수를 찾았을 때 어떻게 리팩토링 할 것인가?
  • 만약 공통 객체에 파라미터 대부분이 포함되어있다면 그냥 그 객체를 전달하는 것이 나을 수도 있다.

  • 주의할 점은 전달 받은 객체를 변경해서는 안된다. 객체를 바꾸고 싶다면 값을 복사하여 수정본을 반환하는 것이 낫다.
  • 한 객체에 담겨 있지 않은 경우 파라미터 그룹핑을 하는 것도 한 대안이다.
  • 함수의 서명을 변경하는 것은 최후의 수단이지만, *args, **kwargs 등으로 사용하는 것은 이해하기 어렵게 만드니 조심해야 한다.

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

소프트웨어 독립성

  • 모듈, 클래스, 함수를 변경하면 수정한 컴포넌트가 외부 세게에 영향을 미치지 않아야만 하다.
  • 런타임 관점에서 독립성은 변경을 내부의 문제로 만드는 것이다.

  • 위의 두개 함수는 서로 독립성을 가져 하나를 변경해도 다른 하나는 변경되지 않는다.
  • 아래 함수 또한 show_price함수에 영향이 없다.
  • 하나를 변경해도 나머지 함수가 그대로라는 것을 알면 변경하기가 편하다.
  • 코드의 두 부분이 독립적이라는 것은 다른 부분에 영향을 주지 않고 변경할 수 있다는 뜻이다.
  • 이는 변경된 부분의 단위 테스트가 나머지 부분의 테스트와 독립적이라는 것을 의미한다.

코드 구조

  • 여러 정의가 들어있는 큰 파일을 만드는 것은 좋지 않다.
  • 코드 여러 부분이 어떤 한 파일의 정의에 종속되어 있다면 패키지를와 패키지 내의 __init__.py 파일을 이용해보자.


Add a Comment Trackback