나쁜 코드로 인해 발생하는 문제
- 다른 개발자가 읽기 힘듬
- 리팩토링 힘듬
- 의존성 심함
- 생산성 떨어짐
- 재설계 힘듬
이 문제들을 예방하기 위해 클린코드와 리팩토리에 대해 알아보자.
# Clean Code란?
"깨끗한 코드는 한 가지를 제대로 한다." - 비야네 스트롭스트룹
"깨끗한 코드는 절대로 설계자의 의도를 숨기지 않는다. 단순하고 직접적이다." - 그래디 부치
"코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행하는 코드" - 워드 커닝엄
"중복 줄이기, 표현력 높이기, 초반부터 간단한 추상화 고려하기, 내게는 이 세가지가 깨끗한 코드를 만드는 비결이다." - 론 제프리
"모든 팀원이 이해하기 쉽도록 작성한 코드"
반대로 나쁜 코드란, “대충 짰는데 돌아가는 코드”를 말한다.
코드를 짤 때 “대충 짜고 나중에 고치지 뭐.”라고 생각하고는 한다.
But, 나중은 절대 오지 않는다.
"Later is Never"
- Leblanc’s Law -
# 클린코드가 왜 필요한가?
10 : 1
코드를 읽는 시간 : 코드를 짜는 시간
프로그래밍을 할 때 우리가 코드를 읽는 시간 : 코드를 짜는 시간의 비율이 10 : 1이라고 한다.
이직이 잦은 개발자들은 기존 사람이 남기고 간 코드를 읽고 수정하는 일이 잦다.
즉, 기존 코드를 읽어야 새 코드를 짜기 때문에 읽기 쉽게 만들면 새 코드를 짜기도 쉽다.
# 클린코드의 주요 원칙
- Follow Standart Convention Coding 표준, 아키텍처 표준 및 설계 가이드를 준수하라.
- Keep it simple, Stupid 단순한 것이 효율적이다. 복잡함을 최소화해라.
- Boy Scout Rule 참조되거나 수정되는 코드는 원래보다 clean해야 한다.
- Root Cause Analysis 항상 근본적인 원인을 찾아라. 그렇지 않으면 반복될 것이다.
- Do not multiple language in one source file 하나의 파일은 하나의 언어로 작성하라.
클린코드란, 가독성이 높은 코드를 말한다.
가독성을 높이려면 다음과 같이 구현해야 한다.
- 네이밍이 잘 되어야 함
- 오류가 없어야 함
- 중복이 없어야 함
- 의존성을 최대한 줄여야 함
- 클래스 혹은 메소드가 한가지 일만 처리해야 함
얼마나 코드가 잘 읽히는 지, 코드가 지저분하지 않고 정리된 코드인지를 나타내는 것이 바로 '클린 코드'
def AAA(a,b):
return a+b
def BBB(a,b):
return a-b
두 가지 문제점이 있다.
def sum(a, b):
return a+b
def sub( a, b):
return a-b
첫째는 함수 네이밍이다. 다른 사람들이 봐도 무슨 역할을 하는 함수인 지 알 수 있는 이름을 사용해야 한다.
둘째는 함수와 함수 사이의 간격이다. 여러 함수가 존재할 때 간격을 나누지 않으면 시작과 끝을 구분하는 것이 매우 힘들다.
# Refactoring
기본적인 리팩토링
1. 함수 추출하기
- 목적과 구현을 분리한다.
- 코드를 보았을 때 "어떻게" 보다 "무엇"을 하는지 한 번에 알 수 있도록 함수의 이름을 짓자.
- 하나의 함수는 한 가지 목적을 가지고 한 가지 일만 해야 한다.
- 즉 한 가지 일만 할 수 있도록 함수를 쪼개고 추출하자.
before
def printOwing(invoice):
print_banner()
outstanding = calcaulate_outstanding()
print(f"고객명: {invoice.customer}")
print(f"채무액: {outstanding}")
after
def printOwing(invoice):
def print_details(outstanding):
print(f"고객명: {invoice.customer}")
print(f"채무액: {outstanding}")
print_banner()
outstanding = calcaulate_outstanding()
print_details(outstanding)
- 추가 설명과 팁
- 단 한 줄짜리 함수라도 상관없다. 무엇을 하는지 명확하게 드러나야 한다.
- 함수의 길이는 한눈에 들어와야 한다.
- 두 번 이상 사용될 코드는 함수로 만들자.
- 함수 이름을 당장 짓기가 어려우면, 주석으로 먼저 무슨 일을 하는지 적어두자.
- 반면, 코드 자체로 무엇을 하는지 명확히 보인다면, 굳이 추출하지 않는다.
2. 변수 추출하기
- 복잡한 표현식은 과정을 나누어 표현한다.
- 각 과정을 잘 드러내는 임시 변수를 사용하자.
before
return order.quantity * order.item_price - max(0, order.quantity - 500) \
* order.item_price * 0.05 + min(100, order.quantity * order.item_price * 0.1)
after
base_price = order.quantity * order.item_price
quantity_discount = max(0, order.quantity - 500) * order.item_price * 0.05
shipping = min(100, base_price * 0.1)
return base_price - quantity_discount + shipping
- 추가 설명과 팁
- 변수 이름을 문맥에 맞게 잘 짓자.
- 문맥은 함수 내부, 클래스 내부, 전역 등에 따라 달라지므로, 어떻게 사용될지 잘 생각하고 이름을 지어야 한다.
- 반면, 추출하지 않아도 그 자체로 명확히 보인다면 추출하지 말자. (오히려 더 깔끔하게 압축하자)
3. 매개변수 객체 만들기
- 몰려다니는 데이터 무리를 데이터 구조 하나로 모아주자
- 데이터 구조로 묶으면 데이터 사이의 관계가 아주 명확해진다.
before
def amount_invoiced(start_date, end_date):
pass
def amount_recevived(start_date, end_date):
pass
def amount_overdue(start_date, end_date):
pass
after
def amount_invoiced(date_range):
pass
def amount_recevived(date_range):
pass
def amount_overdue(date_range):
pass
- 추가 설명과 팁
- 객체를 만든다는 것은 어떤 개념을 추상화하는 것이다.
- 변수들을 하나의 객체로 묶음으로써 하나의 개념을 만들어내고, 이는 더 나은 디자인을 만들어 낼 수 있다.
4. 여러 함수를 클래스로 묶기
- 클래스로 묶으면, 함수들이 공유하는 공통 환경과 목적을 명확히 표현할 수 있다.
- 또한 함수 매개변수를 줄여서, 호출을 더 간결하게 만들 수 있다.
- 원하는 함수를 클래스 단위로 빠르게 찾을 수 있다.
before
def base(reading):
pass
def taxableCarge(reading):
pass
def calculate_base_charge(reading):
pass
after
class Reading:
def __init__(self, reading):
self.reading = reading
def base(self):
pass
def taxableCarge(self):
pass
def calculate_base_charge(self):
pass
캡슐화
1. 레코드 캡슐화하기
- 곳곳에 쓰이는 가변 데이터는 레코드가 아니라 객체로 저장하자.
- 데이터 구조를 명확히 표현할 수 있고, 코드 한 곳에서 관리하고 표현할 수 있게 된다.
before
organization = {"name": "YoonDi", "country": "Korea"}
after
class Organization:
def __init__(self, name: str, country; str):
self.name = name
self.country = country
2. 임시 변수를 질의함수로 바꾸기
- 곳곳에 쓰이는 임시변수 메쏘드로 만들어, 굳이 임시변수를 더 만들지 말자.
before
base_price = self._quantity * self._item_price
if base_price > 1000:
return base_price * 0.95
else:
return base_price * 0.98
after
def _get_base_price(self):
return self._quantity * self._item_price
if self._get_base_price() > 1000:
return self._get_base_price() * 0.95
else:
return self._get_base_price() * 0.98
- 막상 책의 예제를 옮겨보니... before 가 더 가독성이 좋아 보인다.
- 별로 올바른 예인 거 같지는 않으니, 이 리팩토링의 의도(임시 변수를 줄이려는) 만 기억하자.
3. 클래스 추출하기
- 개발 과정에서 점점 비대해지는 클래스를 적절히 분리한다.
- 단일 책임 원칙 (SRP) 를 잊지 말자.
before
class Person:
def __init__(self, ..., office_area_code, office_number):
...
self.office_area_code = office_area_code
self.office_number = office_number
after
class Person:
def __init__(self, ..., office_area_code, office_number):
...
self.TelephoneNumber(office_area_code, office_number)
class TelephoneNumber:
def __init__(self, office_area_code, office_number):
self.office_area_code = office_area_code
self.office_number = office_number
- 추가 설명과 팁
- 일부 데이터와 메쏘드를 따로 묶을 수 있다면 어서 분리하라는 신호다.
- 함께 변경되는 일이 많거나, 의존하는 데이터들도 분리한다.
- 개발 중, 일부 기능만을 사용하기 위해 서브 클래스를 만들어야 한다면 클래스를 나눠야 한다는 신호다.
- 반대로, 리팩터링을 거치면서 쓸모 없어진 클래스는 이 과정을 반대로 한다.
합친 뒤에, 다시 살펴보면 새로운 클래스를 추출할 수도 있기 때문이다.
기능 이동
1. 문장 슬라이드 하기
- 관련된 코드들이 가까이 모여있다면 이해하기 더 쉽다.
- 데이터 구조를 이용하는 문장들은 한데 모여 있어야 그 쓰임을 정확히 알 수 있다.
before
pricing_plan = receive_pricing_plan()
order = receive_order()
charge = None
charge_per_unit = pricing_plan.unit
after
pricing_plan = receive_pricing_plan()
charge_per_unit = pricing_plan.unit
order = receive_order()
charge = None
- 추가 설명과 팁
- 함수 첫머리에 변수를 몽땅 선언하기보다, 처음 사용할 때 선언하자.
- 관련된 것들은 한데 모아두아야, 추가 리팩토링(함수 추출하기 등)을 시행하기 편하다.
2. 반복문 쪼개기
- 하나의 반복문은 하나의 일만 해야 이해하기도, 관리하기도 쉽다.
- 한 반복문에 두 가지 일을 하면, 두 가지 일 모두 이해해야 하고, 수정할 때도 신경 써야 한다.
before
average_age = 0
total_salary = 0
for person in people:
average_age += person.age
total_salary += person.salary
average_age = average_age / len(people)
after
total_salary = 0
for person in people:
total_salary += person.salary
average_age = 0
for person in people:
average_age += person.age
average_age = average_age / len(people)
- 추가 설명과 팁
- 성능 최적화는 당장 고려하지 않는다. (사실 대부분 성능에 그렇게 영향을 주지 않는다.)
- 성능적으로 문제가 있다는 게 밝혀지면, 그때 다시 합치면 된다.
- 코드 분리는 또 다른 최적화나 디자인 패턴의 길을 열어주기도 한다.
조건부 로직 간소화
1. 조건문 분해하기
- 긴 조건문은 의도를 드러낼 수 있는 함수로 추출하여, 로직을 명확히 하자.
before
if date.is_before(plan.summer_start) and not date.is_after(plan.summer_end):
charge = quantity * plan.summer_rate
else:
charge = quantity * plan.regular_rate + plan.regular_service_charge
after
if is_summer():
charge = summerCharge()
else:
charge = regularCharge()
2. 조건식 통합하기
- 하나로 합칠 수 있는 조건식은 합친 뒤, 의도를 드러낼 수 있는 함수로 추출하자.
before
if employee.seniority < 2: return 0
if employee.month_disabled > 12: return 0
if employee.is_part_time: return 0
after
def is_not_eligible_for_disability():
return (employee.seniority < 2 or
employee.month_disabled > 12 or
employee.is_part_time)
if is_not_eligible_for_disability(): return 0
3. 특이 케이스 추가하기
- 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하면 관리하기 편하다.
before
if customer == "미확인 고객":
customer_name = "거주자"
after
class UnknownCustomer:
def __init__(self):
self.name = "거주자"
- 추가 설명과 팁
- 일반적으로, 특정 값에 대해 똑같이 반응하는 코드가 여러 곳이면, 그 반응을 한 데로 모으는 게 효율적이다.
- 모으는 것은 리터럴 객체나 따로 정의한 클래스에 모을 수 있는데,
- 데이터를 담기만 하는 경우 리터럴 객체(dict 와 같은...)를 쓰면 되고, 어떤 동작을 수행해야 하면 클래스로 추출하면 된다.
- 널 객체 패턴이라고도 한다.
4. 어서션 추가하기
- 어서션은 어떤 상태임을 가정한 채 실행되는지 다른 개발자에게 알려주는 소통 도구다.
before
if self.discount_rate:
base -= self.discount_rate * base
after
assert self.discount_rate >= 0
if self.discount_rate:
base -= self.discount_rate * base
- 추가 설명과 팁
- 어서션이 있고 없고가 프로그램의 정상 동작에 아무런 영향을 주지 않도록 작성되어야 한다.
- 즉, 어서션은 실패해서는 안된다. 실패한다면 어딘가 잘못 구현한 코드가 있는 것이다.
- 어서션은 개발자 간의 커뮤니케이션 도구임을 잊지 말자.
클린코드와 리팩토링의 차이?
리팩토링이 더 큰 의미를 가진 것 같다. 클린 코드는 단순히 가독성을 높이기 위한 작업으로 이루어져 있다면, 리팩토링은 클린 코드를 포함한 유지보수를 위한 코드 개선이 이루어진다.
클린코드와 같은 부분은 설계부터 잘 이루어져 있는 것이 중요하고, 리팩토링은 결과물이 나온 이후 수정이나 추가 작업이 진행될 때 개선해나가는 것이 올바른 방향이다.
출처-
https://dailyheumsi.tistory.com/219
'1일1CS' 카테고리의 다른 글
45. TDD(Test Driven Development)란? (2) | 2023.01.31 |
---|---|
44 . DB index (0) | 2023.01.30 |
42. NoSQL이랑 RDBMS의 특징과 차이점 (0) | 2023.01.20 |
41. 앱의 종류: 네이티브 앱 vs 웹 앱 vs 하이브리드 앱 (0) | 2023.01.19 |
40. [운영체제] 프로세스(Process)와 스레드(thread) (0) | 2023.01.18 |
댓글