Tiny Finger Point Hand With Heart
본문 바로가기
1일1CS

43. 클린코드 와 리팩토링

by yoondii 2023. 1. 25.
728x90
반응형

나쁜 코드로 인해 발생하는 문제

  • 다른 개발자가 읽기 힘듬
  • 리팩토링 힘듬
  • 의존성 심함
  • 생산성 떨어짐
  • 재설계 힘듬

 

이 문제들을 예방하기 위해 클린코드와 리팩토리에 대해 알아보자.

 


# Clean Code란?

 

"깨끗한 코드는 한 가지를 제대로 한다." - 비야네 스트롭스트룹

"깨끗한 코드는 절대로 설계자의 의도를 숨기지 않는다. 단순하고 직접적이다." - 그래디 부치

"코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행하는 코드" - 워드 커닝엄
"중복 줄이기, 표현력 높이기, 초반부터 간단한 추상화 고려하기, 내게는 이 세가지가 깨끗한 코드를 만드는 비결이다." - 론 제프리

 

"모든 팀원이 이해하기 쉽도록 작성한 코드"

 

반대로 나쁜 코드란, “대충 짰는데 돌아가는 코드”를 말한다.

코드를 짤 때 “대충 짜고 나중에 고치지 뭐.”라고 생각하고는 한다.

But, 나중은 절대 오지 않는다.

 

"Later is Never"
- Leblanc’s Law -

 

# 클린코드가 왜 필요한가?

10 : 1
  코드를 읽는 시간 : 코드를 짜는 시간

 

프로그래밍을 할 때 우리가 코드를 읽는 시간 : 코드를 짜는 시간의 비율이 10 : 1이라고 한다.

이직이 잦은 개발자들은 기존 사람이 남기고 간 코드를 읽고 수정하는 일이 잦다.

즉, 기존 코드를 읽어야 새 코드를 짜기 때문에 읽기 쉽게 만들면 새 코드를 짜기도 쉽다.

 

 

# 클린코드의 주요 원칙

  1. Follow Standart Convention  Coding 표준, 아키텍처 표준 및 설계 가이드를 준수하라.
  2. Keep it simple, Stupid  단순한 것이 효율적이다. 복잡함을 최소화해라.
  3. Boy Scout Rule  참조되거나 수정되는 코드는 원래보다 clean해야 한다.
  4. Root Cause Analysis  항상 근본적인 원인을 찾아라. 그렇지 않으면 반복될 것이다.
  5. 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

https://gyoogle.dev/blog/computer-science/

https://sujinhope.github.io/

728x90
반응형

댓글