데이터 공부/Python

파이썬 클린코드 3 - 좋은 코드의 일반적인 특징 (1/2)

한소희DE 2023. 1. 31. 23:20
요즘 파이썬 클린코드(마리아노 아나야 지음)라는 책을 읽고 있다.
한 챕터씩, 책을 읽으며 내가 기억에 남는 점 등에 대한 정리를 해보도록 하겠다.

목차

1. 계약에 의한 디자인

2. 방어적 프로그래밍

3. 관심사의 분리

 


 

 

좋은 코드의 특징 중 3가지를 이번 포스팅에서 정리를 해보고자 한다.


1. 계약에 의한 디자인

 

계약이란, 소프트웨어 컴포넌트 간 통신 중 지켜야 할 규칙들을 말한다.

여기서 계약에 의한 디자인이란, 계약을 기반으로 코드를 디자인해야 하는 것을 말한다. 이는 런타임 오류 발생 시 코드의 어떤 부분이 계약 파손 됐는지 명확히 알 수 있도록 돕는다. 이외 장단점을 정리하면 아래와 같다.

 

 

계약에 의한 디자인의 장점

  • 사전조건 검증, 사후조건 검증에서 실패하는 오류 발생 시 오류를 쉽게 찾음
  • 책임의 명확성
  • 장기적 품질을 보장
  • 잘못된 가정 하에 코드 핵심 부분이 실행되는 것을 방지

 

 

계약에 의한 디자인의 단점

  • 계약 작성에 의한 추가 작업 발생
  • 계약에 대한 단위테스트 추가 가능성

 

[계약에 의한 디자인 예시]

만약 내부를 숨겨 캡슐화하고 함수를 사용하려면, API를 노출해야 하는데 이때, API가 정상적으로 잘 호출되도록, 이해관계자 서로 간의 계약을 맺는다. (ex: 정수값을 넣어야 함 ... 기타 등등)
또한 이러한 계약을 문서에 잘 작성해 두는 것이 중요하다.
그래야 이 API에서 오류가 발생했을 때, 코드 단의 잘못된 건지 아니면 클라이언트 단의 오류인지알아내기 수월하다.  

 

이러한 계약은 크게 두 가지 조건 계약으로 나뉜다.

 

 

1-1. 사전조건

함수나 메서드가 제대로 동작하기 위해 보장해야 하는 모든 것을 의미하며, 파라미터에 제공된 유효성을 검사하는 계약이다.

사전조건은 클라이언트의 사용에 대한 결함이며, 런타임 중 알아챌 수 있어야 한다. (=코드가 실행되면 안 된다)

 

1-2. 사후조건

반환된 값의 유효성을 검사하여 반환된 후의 상태를 강제한다. 클라이언트는 사후조건을 검증했을때 통과한 반환 객체를 아무문제 없이 사용할 수 있어야 한다. 만약 사후조건 검증에서 실패했다고 한다면 특정 모듈이나 제공하는 클래스 자체에 문제가 있음을 알 수 있다.

 

 

 

1-3. 파이썬스러운 계약 방법

 

그렇다면 이 검증계약 방법을 어떻게 코드에 녹여야 할까?

파이썬스럽게 하기 위해서는, 메서드/함수/클래스가 조건을 위반할 시 RuntimeError or ValueError를 반환하는 제어 메커니즘을 추가하면 된다. 문제를 정확히 특정하기 어려울 땐 예외 추가도 고려할 수 있다.

또한 사전조건에 대한 검사, 사후조건에 대한 검사, 핵심기능 등에 대한 구현 컴포넌트를 분리하는 게 좋다.

 

 


2. 방어적 프로그래밍

방어적 프로그래밍이란, 시나리오의 오류를 처리하거나 or 반드시 시나리오에서 발생하지 않아야 하는 오류들을 처리하는 방법에 대한 프로그래밍을 말한다.

이처럼 반드시 발생하지 않아야 하는 오류를 그대로 넘기지 않기 위한 프로그래밍을 위해서는, 세 가지의 방법이 있다.

 

2-1. 값 대체

만약 소프트웨어 내에서 잘못된 값을 생성하거나, 잘못된 값이 들어왔을 때 전체가 종료되는 우려가 있을 경우, 값을 대체할 수 있다. 

예를 들어, 함수에 특정 파라미터가 들어오지 않으면 None을 반환하도록 설정한다.

하지만 그렇다고해서, 오류가 있는 데이터를 유사한 값으로 대체해서 프로그래밍되도록 처리하는 건 일부 오류를 숨길 수 있기 때문에 매우 위험하니, 잘 고민해서 값 대체를 써야 한다.

 

2-2. 예외 처리, 에러 로깅

만약 함수 자체가 잘못된거면, 에러처리를 하면서 돌아가는 것보다는 프로그래밍이 종료되는 게 훨씬 낫다. 이는 오류를 숨길 수 있기 때문이다. 하지만 외부 컴포넌트 때문(ex: 커넥션이 네트워크가 잠시 끊겨 안될 경우, 커넥션 재시도가 필요한 경우)에 에러가 나는 등의 이유라면 에러 로깅을 하고 예외 처리를 하는 게 좋다.

 

이때, 에러 로깅이 왜 발생했는지, 무엇 때문에 발생했는지에 대한 원인을 쉽게 볼 수 있도록 에러 로깅을 하고 예외 처리를 해야 한다.

왜냐하면 아무리 예외처리여도, 에러는 결코 조용히 전달돼서는 안 된다는 파이썬의 철학을 잊지 말아야 하기 때문이다. 따라서 문제를 일으킨 원본 예외를 같이 전달하자.

 

따라서 에러 처리를 위해서, except에서 None을 반환하는 것은 위와 같은 이유로 절대 하면 안 되는 행동이다. 

이는 의도하지 않았던 새로운 에러가 발생해도 아무런 액션을 취하지 않기 때문이다. 따라서 except 등을 처리하려면, 위에 언급한 것처럼 문제를 일으킨 원본 예외 등을 같이 로그에 전달하는 습관을 들이자.

또한 이때, 예외 처리 중(except)에 예외가 아닌 에러를 분리해서 출력해야 할 경우, 항상 raise <e> from <original> 구문을 이용하자. (raise는 에러를 직접 발생시킨다.)

 

 

2-3. 파이썬에서 어설션 사용하기

assert는 고정된 코드에 대한 정확성을 보장하기 위해 스스로 체크 및 검증을 하는 데에 쓰인다. 즉 잘못된 시나리오에 도달하면 프로그램이 피해를 입지 않도록 하거나 코드를 중단시키는 용도로 쓰이기 때문이다. (이처럼 용도가 명확한 녀석이기 때문에, 다른 비즈니스 로직과 섞어 쓰거나 함수 내에 넣어 쓰거나 하는 행동은 가급적 삼가는 것이 좋다.)

따라서 assertionError가 나오면 이는 소프트웨어에서 결함이 있음을 의미한다. 따라서 assert로 특정 코드에 대한 검사가 필요하다면, 그리고 assertionError가 나온다면, 구체적인 에러를 파악하는 것이 필요하다.

 

# raise 사용 예시

result = condition.holds()
assert result > 0, f"에러 {result}"

 


 

3. 관심사의 분리

이는 클래스와 모듈, 패키지, 컴포넌트가 각자의 목적에 맞게 분리돼야 한다는 뜻이다.

즉, 잘 정의된 소프트웨어는 높은 응집력과 낮은 결합력을 가져야 한다. (매우 중요!)

 

3-1. 응집력

응집력이란, 객체가 작고 잘 정의된 목적을 가져야 하며 가능하면 작아야 한다는 의미다.

유닉스 명령어가 한 가지 일만 잘 수행하라는 철학을 가진 것과 유사한 철학을 파이썬이 지녔음을 알 수 있다.

 

 

3-2. 결합력

결합력이란, 각 객체가 어떻게 의존하는 지를 보는데, 만약 너무 결합력이 높다면 아래와 같은 부작용을 초래한다. 따라서 결합력이 낮도록 코드를 구성하는 습관을 들이는 것이 좋다.

 

결합력이 높으면 발생되는 단점

  • 낮은 재사용성
  • 하나를 변경하면 나머지 하나에도 큰 영향을 미치는 파급효과
  • 서로 다른 추상화 레벨에서 문제를 해결하기 어려워서, 추상화 수준이 낮아짐

 

 


 

파이썬 클린코드를 어느 정도 읽고 나니까, 내가 짠 코드들에 대해 개선해야 할 점들이 곳곳에 더 잘 보이기 시작했다. 이전보단 좀 더 시야가 넓어진 건가? 하는 뿌듯함도 있지만.. 기존에 짰던 코드에 대한 부끄러움 + 개선을 해야 한다는 막막함 과 같은 감정들을 복합적으로 느끼는 요즘이다.