정의할 수 없으므로 측정할 수도 없다.
전문가가 판단하는 영역이다.
코드를 작성하는 것보다 읽는데 훨씬 많은 시간을 소비한다.
클린 코드인지 아닌지는 다른 엔지니어가 코드를 읽고 유지 관리할 수 있는지 여부에 달려 있다.
수십 년 동안 프로그래밍 언어라는 것은 인간의 아이디어를 컴퓨터에 전달하기 위해 사용하느 언어라고 생각해왔다. 그러나 그건 틀린 생각이다. 이러한 생각은 진실이 아니라 진실의 일부이다. 프로그래밍 언어의 진정한 의미는 아이디어를 다른 개발자에게 전달하는 것이다.
자동차 운전 예시
기술부채는 이자를 유발한다.
기술부채에는 두 가지 관점이 존재한다.
장기적이고 근본적인 문제를 내포한다.
당장 시끄러운 경고음을 내지 않는다.
(-> 소액의 이자는 기꺼이 지불하려 한다.)
따라서, 철저한 코드 리뷰와 자동화된 테스트를 사용하자
클린 코드는 PEP-8 코딩 표준, 포매팅, 린팅 도구, 코드 레이아웃 그 이상을 의미한다.
품질 좋은 소프트웨어를 개발하고, 견고하고 유지보수가 쉬운 시스템을 만들고, 기술 부채를 피하는 것을 말한다.
그럼에도 포매팅은 작업의 효율화에 도움이 된다.
코딩 가이드 라인 : 프로젝트에서 따라야만 하는 최소한의 요구사항
현재 프로젝트에서 어떤 코딩 표준도 따르지 않았다면, PEP-8을 사용하자.
일관성이 중요하다.
왜? 일관성이 확보되면 -> 가독성 향상되고 -> 유지 보수 용이
(체스 마스터가 체스판을 바라볼 때의 인지 활동 중 패턴 인식 효과)
검색 효율성
location=current_location,
current_location = get_location()
- 키워드 인자에 값 할당 시, 띄어쓰기 사용하지 않음
- 변수에 값을 할당할 땐, 띄어쓰기 사용
일관성
신규 입사자 교육 시 매우 유용하다.
또한, 신규 개발자나 경험이 많지 않은 개발자 역시 여러 저장소의 코드를 봐도 패턴에 익숙해질 수 있다.
더 나은 오류 처리
try/except 블록 내부의 코드를 최소화하기는 좋은 코딩 스타일 가이드이나, 자동화하기 어렵다.
따라서 코드 리뷰를 하자.
코드 품질
코드를 구조화하여 살펴볼 수 있으므로 한 눈에 코드를 이해하고 버그와 실수를 쉽게 찾을 수 있다.
코드 품질 도구를 사용하면 잠재적인 버그를 찾을 수도 있고, 정적 분석 도구를 사용하면 한 줄당 버그의 개수를 줄일 수도 있다.
코드를 문서화하는 것과 코드의 주석은 다른다.
주석을 추가한다는 것은 코드를 올바르게 작성하지 않았다는 징후다.
문서화를 위해 docstring과 annotation(어노테이션)을 살펴본다.
어떤식으로도 정당화하기 어려운 나쁜 주석도 있다.
주석 처리된 코드이다.
이러한 코드는 무자비하게 삭제되어야 한다.
소스 코드에 포함된 문서(document)다.
>> help(dict.update)
Help on method_descriptor:
update(...)
D.update([E, ]**F) -> None. Update D from dict/iterable E and F.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
In either case, this is followed by: for k in F: D[k] = F[k]
PEP-3107 에서 어노테이션을 소개한다.
기본 아이디어는 코드 사용자에게 함수 인자로 어떤 값이 와야 하는지 힌트를 주자는 것이다.
@dataclass
class Point:
lat: float
long: float
def locate(latitude: float, longitude: float) -> Point:
"""맵에서 좌표에 해당하는 객체를 검색"""
>>> locate.__annotations__
{'latitude': <class 'float'>, 'longitude': <class 'float'>, 'return': <class '__main__.Point'>}
위 정보를 토대로 문서 생성, 유효성 검증, 타입 체크도 할 수 있는 장이 열린 것이다.
v. annotate : 주석을 달다
주석, 어노테이션, docstring 모두 코드에 흔적을 남기는 행위라는 비슷한 의미를 갖는다.
하지만, 용어별 차이점이 있으니, 어떻게 흔적을 남겨야 하는지 그리고 어떤 흔적은 사용을 지양해야 하는지 명확해진다.
어노테이션을 활용하면 좀 더 표현력을 가진 코드를 가질 수 있다.
아래 코드는 아직 불분명하다.
def launch_task(delay_in_seconds):
...
허용 가능한 지연시간은 몇 초일까?
분수를 입력해도 되는 걸까?
아래와 같이 수정해보자.
Seconds = float
def launch_task(delay_in_seconds: Seconds):
...
코드 스스로 자신의 기능에 대해 말을 하고 있다.
Seconds 어노테이션을 사용하여 시간을 어떻게 해석할지에 대해 작은 추상화를 했다.
그렇다면 타입 힌트가 단순히 데이터 타입을 확인해주는 힌트 역할만 하는 것일까?
아니다.
유의미한 이름을 사용하거나 적절한 데이터 타입 추상화를 하도록 도와줄 수도 있다.
def process_clients(clients: list):
...
def process_clients(clients: list[tuple[int, str]]):
...
위 두 방식은 충분한 정보를 제공해주지 못하고 있다.
필요한 데이터 구조를 따로 정의하여 클라이언트가 어떤 데이터 구조를 가지고 있는지
명시적으로 알려주는 것이 좋다.
from typing import Tuple
Client = Tuple[int, str]
def process_clients(clients: list[Client]):
...
지금까지 우리는 어노테이션에 대해 왜 이렇게 장황하게 떠든 것일까?
근본 취지는 유의미한 문법 구조를 통해 코드의 의미나 의도를 명확히 하자는 것이다.
또한, PEP-526, PEP-557 표준을 도입하면 클래스를 보다 간결하게 작성하고 작은 컨테이너 객체를 쉽게 정의할 수 있게 된다.
@dataclass를 통해 __init__
메서드에 변수를 선언하고 할당하는 작업을 할 필요 없이 작은 컨테이너 객체를 쉽게 생성할 수 있게 된다.
아니다.
docstring자체에도 의미가 있다.
def data_from_response(response: dict) -> dict:
if response["status"] != 200:
raise ValueError
return {"data": response["payload"]}
response 객체의 올바른 인스턴스는 어떤 모습일까? 알 수 없다.
def data_from_response(response: dict) -> dict:
"""response의 HTTP status가 200이면 response의 payload 리턴"""
- response 예시::
{
"status": 200, # <int>
"timestamp": "...", # 현재 시간의 ISO 포맷 문자열
"payload": {...} # 리턴할 dict 자료형 데이터
}
- 리턴 dict 값 예시::
{"data" : { .. } }
- 발생 가능한 예외:
- HTTP status가 200이 아닌 경우 ValueError 발생
"""
def data_from_response(response: dict) -> dict:
if response["status"] != 200:
raise ValueError
return {"data": response["payload"]}
좋은 코드인지 나쁜 코드인지 판단할 수 있는 것은 오직 사람이다.
동료의 코드를 살펴볼 때 다음 질문을 해야 한다.
코드 포매팅, 일관된 레이아웃, 적절한 들여쓰기 검사만으로는 클린 코드라고 할 수는 없다.
그렇다고 이것이 중요하지 않다는 것은 아니다.
이러한 반복적인 확인 작업을 줄이기 위해 코드 검사를 자동으로 실행하는 기본 도구를 설정해야 한다.
mypy와 pytype 두 가지가 있다.
pip intall mypy
mypy <file-name>
type_to_ignore = "something" # type: ignore
해당 라인에 대한 검사를 무시하도록 할 수 있다.
from typing import Iterable
def broadcast_notification(
message: str,
relevent_user_emails: Iterable[str],
):
for email in relevent_user_emails:
print(email)
broadcast_notification("welcome", "user1@domain.com")
결과
u
s
e
r
1
@
d
o
m
a
i
n
.
c
o
m
따라서, 명확하게 어노테이션을 작성하는 것이 좋다.
from typing import Union, List, Tuple
def broadcast_notification(
message: str,
relevent_user_emails: Union[
List[str],
Tuple[str],
],
):
for email in relevent_user_emails:
print(email)
broadcast_notification("welcome", "user1@domain.com")
결과
error: Argument 2 to "broadcast_notification" has incompatible type "str"; expected "list[str] | tuple[str]" [arg-type]
Found 1 error in 1 file (checked 1 source file)
오류를 확인하는 시점이 다르다.
from typing import List
def get_list() -> List[str]:
lst = ["PyCon"]
lst.append(2022)
return [str(x) for x in lst]
결과
test.py:20: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "str" [arg-type]
Found 1 error in 1 file (checked 1 source file)
위의 코드는 런타임시에는 아무런 문제가 없는 코드다.
lst.append로 도중에 int형 데이터가 추가되지만, 최종적으로는 str(x)를 통해 List[str]를 만족시키기 때문이다.
pytype에서는 타입 에러를 발생시키지 않으나, mypy에서는 엄격하게 에러로 인지하는 모습이다.
>> pip install pytype
>> pytype test.py
Computing dependencies
Analyzing 1 sources with 0 local dependencies
ninja: Entering directory `.pytype'
[1/1] check test
Leaving directory '.pytype'
Success: no errors found
PEP-8 준수 여부를 검증하는 것 뿐만 아니라 더 복잡한 것에 대한 추가 검사를 제공하는 도구도 있다.
pip install pylint
[DESIGN]
disable=missing-function-docstring
[TIP] 개발팀에서 합의한 코등 표준을 문서화하고, 해당 표준이 자동으로 실행되는 도구의 설정파일에 포함 되도록 하자.
합의한 내용들이 pylintc 등의 설정파일에 구현되도록 하여, 노션에 작성한 합의 내용과 실제 적용되는 방식이 동기화되도록 관리하자!
PR시 불필요한 논쟁을 줄이고 코드의 핵심에 집중할 수 있도록 사전에 팀에서 논의된 코딩 컨벤션을 만들어 두는 것이 좋다.
파이썬 코드를 자동으로 포매팅 하는 다양한 도구가 있다.
flake8
, black
여러 옵션을 수정할 수 있는 유연한 flake8과는 달리 black은 엄격하다.
예를 들어, 따옴표는 항상 큰따옴표만을 사용해야 하고, 파라미터의 순서를 항상 동일한 구조를 따라야 한다.
이렇게 하는 것이 너무 엄격하다고 생각될 수 있지만 코드의 차이를 최소화 하는 유일한 방법이다.
오히려 이것이 black이 존재하는 이유이기도 하다.
PEP-8은 코드의 구조에 대해 가이드 라인을 제시한다.
하지만 그것을 준수하는 방법엔 몇 가지 옵션이 존재한다.
black은 이 점에 대해서 문제를 제기한다.
black은 PEP-8보다 엄격한 하위 집합을 관리함으로써 항상 결정적인 형태의 포맷을 갖게 한다.
아래 두 코드는 모두 PEP-8 을 준수한다.
1) black 미적용
def my_function(name):
"""
>>> my_function('black')
'received Black'
"""
return 'received {0}'.format(name.title())
2) black 적용
def my_function(name):
"""
>>> my_function('black')
'received Black'
"""
return "received {0}".format(name.title())
리눅스 개발환경에서 빌드를 자동화하는 가장 일반적인 방법은 Makefile을 사용하는 것이다.
.PHONY: typehint
typehint:
mypy --ignore-missing-imports /
.PHONY: test
test:
pytest tests/
.PHONY: lint
lint:
pylint src/
.PHONY: checklist
checklist: lint typehint test
.PHONY: black
black:
black -l 79 *.py
.PHONY: clean
clean:
find . -type f -name "*.pyc" | xargs rm -fr
find . -type d -name __pycache__ | xargs rm -fr
Makefile 이점
작업의 표준화
make fotmat이란 표준 명령어로 세부 작업에 대해 추상화를 한다.
나중에 black에서 yapf로 도구를 변경해도 표준 명령어는 변하지 않는다.
여러 작업을 한꺼번에 실행하는 표준화된 방법을 제공한다.
CI에서 하나의 명령어만 호출하지만, Makefile에 정의된 여러 작업들이 실행되는 셈이다.
클린 코드는 코딩 스타일과 레이아웃 그 이상이다.
클린 코드는 기술부채를 최소화하고 가독성과 유지보수성 그리고 다른 개발자들의 이해도를 높이는 효과적인 코드 작성 방법에 관한 것이다.
코딩 스타일과 레이아웃을 준수하는 것 또한 중요하다.
최소한의 요구사항이다. 자동화하는 것이 중요하므로 mypy, pylint, black 등과 같은 도구를 사용하자.