CleanCode_Python

Chapter 5 - FINDING CODE SMELLS [코드 악취 감지]

코드 악취 : 잠재적인 버그를 나타내는 소스 코드 패턴, 코드 냄새가 난다고 해서 반드시 문제가 있다는 의미는 아니지만 프로그램을 조사해야 한다는 의미

나중에 버그를 발견하고 이해하고 수정하는 것보다 버그를 방지하는 데 훨씬 적은 시간과 노력이 듭니다. 모든 프로그래머는 디버깅에만 시간을 소비한 이야기를 가지고 있습니다. 한 줄의 코드 변경과 관련된 수정 사항을 찾았습니다. 이러한 이유로 잠재적인 버그의 조그마한 냄새라도 잠시 멈추어 향후 문제를 일으키지 않는지 다시 확인하라는 메시지를 표시해야 합니다.

물론 코드 냄새가 반드시 문제가 되는 것은 아닙니다. 궁극적으로 코드 냄새를 처리할지 아니면 무시할지 여부는 사용자가 판단해야 합니다.

5.1_ Duplicate Code [중복 코드]

중복 코드 : 프로그램에 다른 코드를 복사하여 붙여넣어 생성할 수 있는 모든 소스 코드

print('Good morning!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good afternoon!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')
print('Good evening!')
print('How are you feeling?')
feeling = input()
print('I am happy to hear that you are feeling ' + feeling + '.')

중복 코드는 코드 변경을 어렵게 만들기 때문에 문제입니다. 중복 코드의 복사본 하나를 변경하면 프로그램의 모든 복사본에 변경 사항이 적용되어야 합니다. 어딘가에서 변경하는 것을 잊어버리거나 다른 복사본을 다르게 변경하면 프로그램에 버그가 생길 수 있습니다.

중복 코드에 대한 해결책은 중복을 제거하는

해결법 1 : 중복 코드를 함수로 이동한 다음 해당 함수를 반복해서 호출

def askFeeling():
    print('How are you feeling?')
    feeling = input()
    print('I am happy to hear that you are feeling ' + feeling + '.')

print('Good morning!')
askFeeling()
print('Good afternoon!')
askFeeling()
print('Good evening!')
askFeeling()

해결법 2 : 중복 코드를 루프로 이동

for timeOfDay in ['morning', 'afternoon', 'evening']:
    print('Good ' + timeOfDay + '!')
    print('How are you feeling?')
    feeling = input()
    print('I am happy to hear that you are feeling ' + feeling + '.')

해결법 3 : 두 기술을 결합하여 함수와 루프를 사용

def askFeeling(timeOfDay):
    print('Good ' + timeOfDay + '!')
    print('How are you feeling?')
    feeling = input()
    print('I am happy to hear that you are feeling ' + feeling + '.')

for timeOfDay in ['morning', 'afternoon', 'evening']:
    askFeeling(timeOfDay)

5.2_ Magic Numbers [매직 넘버]

프로그래밍에 숫자가 포함되는 일이 있지만, 소스 코드에 나타나는 일부 숫자는 다른 프로그래머(또는 작성한 후 몇 주 후에)를 혼란스럽게 할 수 있다

# before
expiration = time.time() + 604800

# after
expiration = time.time() + 604800  # Expire in one week.

다음과 같이 주석을 사용하여 expiration(만료기간)이 일주일 뒤인것을 나타 낼 수 있다

# Set up constants for different time amounts:
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR   = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY    = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK   = 7  * SECONDS_PER_DAY

--snip--

expiration = time.time() + SECONDS_PER_WEEK  # Expire in one week.

다음과 같이 Magic Number를 상수로 대체하는 것이 더 좋은 해결 법이다

NUM_CARDS_IN_DECK = 53
NUM_WEEKS_IN_YEAR = 52

지금은 값이 같더라도 다르게 사용이 될 수 있다면 분리해서 선언해야 된다

- Magic number는 숫자가 아닌 값(문자열, …)등에도 적용 될 수 있다

while True:
    print('Set solar panel direction:')
    direction = input().lower()
    if direction in ('north', 'south', 'east', 'west'):
        break

print('Solar panel heading set to:', direction)
if direction == 'nrth': # <- 오타
    print('Warning: Facing north is inefficient for this panel.')

‘north’를 입력 받으면 경고를 출력 하려했지만 조건문에서 오타로 ‘nrth’가 입력 되었다
오타는 여전히 문법적으로 올바르기 때문에 감지하기 어려울 수 있다.

# Set up constants for each cardinal direction:
NORTH = 'north'
SOUTH = 'south'
EAST = 'east'
WEST = 'west'

while True:
    print('Set solar panel direction:')
    direction = input().lower()
    if direction in (NORTH, SOUTH, EAST, WEST):
        break

print('Solar panel heading set to:', direction)
if direction == NRTH: # <- 오타
    print('Warning: Facing north is inefficient for this panel.')
# --------------- output --------------- #
Set solar panel direction:
west
Solar panel heading set to: west
Traceback (most recent call last):
  File "panelset.py", line 14, in <module>
    if direction == NRTH:
NameError: name 'NRTH' is not defined

NameError 이 있는 코드 라인에서 발생한 예외는 이 프로그램을 실행할 때 버그를 명확하게 표시해준다

매직 넘버는 목적을 전달하지 않기 때문에 코드 악취이며, 코드 가독성을 떨어뜨린다. 추가로 업데이트하기 어렵게 만들고 감지할 수 없는 오타가 발생하기 쉽다. 해결 방법은 상수 변수를 대신 사용하는 것이다.

5.3_ Commented-Out Code and Dead Code [주석 코드 & 죽은 코드]

  • 주석 코드

    함수나 코드 설명을 위한 주석을 하는 것은 나쁜것은 아니다. 하지만 흔히 테스트 과정에서 다른 기능을 테스트하고 싶을 때 해당 코드를 주석처리하면 나중에 쉽게 되돌릴 수 있다.
    하지만 주석 처리된 코드가 그대로 남아있는 것은 좋지않다. 그렇게 되면 왜 코드에서 제거됐는지, 나중에 어떤 조건에서 다시 필요해지는 지 모르는 상태가 된다. 사용하지 않는 코드는 지우는 것을 권장한다.

    create_dataframe()
    #preprocess_dataframe()
    create_new_dataframe()
    preprocess_dataframe()
    

    예시 코드지만 극단적으로 표현하자면, preprocess_dataframe()가 왜 두 번 나오는지, 그리고 뒤에 create_new_dataframe()을 호출한 뒤에 preprocess_dataframe()을 호출하는데 왜 앞에서 create_dataframe()일경우에는 preprocess_dataframe()를 호출하지 않는지 와 같은 의문이 생길 수 있다.

  • 죽은 코드

    죽은 코드는 “도달할 수 없거나 논리적으로 실행 될 수 없는 코드”이다.

    def true_or_false(input: bool):
      if input == True:
        return '참입니다'
      else:
        return '거짓입니다'
      return '아무것도 아닙니다' #죽은 코드
    

    return '아무것도 아닙니다'는 논리적으로 실행 될 수 없는 죽은 코드이다.

    - 예외

    stub은 코드 악취 판단에서 예외합니다.
    stub : 아직 구현되지 않은 함수나 클래스처럼 향후 코드가 작성될 위치를 나타내는 플레이스홀더이다.

    # 예시 1
    def preprocess_dataframe(df):
      pass
    # 예시 2
    def preprocess_dataframe(df):
      raise NotImplementedError
    

    2가지 방법으로 표현할 수 있지만, “예시 2”번의 방식으로 사용을 한다면 의도치 않게 호출 되었을 때 에러를 표시하여 실수를 예방할 수 있습니다.

5.4_ Print Debugging [디버깅 출력]

print debugging은 코드에 임시로 print() 호출을 넣어 중간에 값들을 출력하는 것을 의마한다.
처음에는 문제가 없는 것 처럼 보일 수 있지만 print()문이 많아지다 보면 호출 일부를 제거하는 것을 잊는 다면 다시 찾아서 제거를 해야되는 문제가 있다.

import logging
logging.basicConfig(filename='log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.debug('This is a log message.')

위와 같이 logging 모듈을 가져와 print() 대신 logging.debug()로 호출하면 텍스트 파일에 정보를 저장할 수 있다.
또한 로그 파일은 프로그램에서 대량의 정보를 기록 할 수 있으므로 이전에 실행된 코드와 비교도 가능하다.

5.5_ Variables with Numeric Suffixes [숫자 접미사가 있는 변수]

예를 들어 오타를 방지하기 위해 비밀번호를 2번 입력하는 경우가 있다고 할 때, password1, password2이런 변수명은 지양하고, password, confirm_password이런 방식으로 이름을 짓는 것을 권장한다.

5.6_ Classes That Should Just Be Functions or Modules [함수 또는 모듈이어야 하는 클래스]

import random
class Dice:
  def __init__(self, sides=6):
    self.sides = sides
  def roll(self):
    return random.randint(1, self.sides)

d = Dice()
print('You rolled a', d.roll())

> You rolled a 1
print('You rolled a', random.randint(1, 6))
> You rolled a 6

같은 기능을 하지만 파이썬은 다른 언어에 비해 코드를 구성하는 것이 좀 더 자유롭다.
파이썬은 함수를 그룹화하기 위해 클래스가 아닌 모듈을 사용하기 때문에 위와 같은 경우는 오히려 파이썬 코드를 복잡하게 만들 수 있다.

5.7_ List Comprehensions Within List Comprehensions [리스트 컴프리헨션 내의 리스트 컴프리헨션]

과도한 리스트 컴프리헨션은 오히려 코드 해석을 복잡하게 만들 수 있다.

nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
flatList = [num for sublist in nestedList for num in sublist]
flatList
> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
flatList = []
for sublist in nestedList:
  for num in sublist:
    flatList.append(num)
flatList
> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

첫번째 코드는 한 리스프 컴르리헨션안에 for 문이 2개가 들어가면서 코드 해석이 더 어렵게 되었다.
컴프리헨션은 구문론적으로 코드를 간결하게 만들 수 있지만, 남용하면 오히려 해석을 어렵게 만든다.

5.8_ Empty except Blocks and Poor Error Messages [빈 예외처리 & 부족한 에러 메세지]

try:
  num = input('Enter a number: ')
  num = int(num)
except ValueError:
  pass

Enter a number: forty two
>>> num
'forty two'

다음과 같이 pass로 구성하면 코드 자체는 계속 실행이 되지만, num이 문자열로 출력이 되어, 의도하지 않은 결과를 가져올 수 있습니다.

try:
  num = input('Enter a number: ')
  num = int(num)
except ValueError:
  print('An incorrect value was passed to int()')

Enter a number: forty two
An incorrect value was passed to int()

이전 코드보다는 나아졌지만, 해당 출력만으로는 사용자가 무슨 일이 일어났고, 어떤 일을 해야할지 파악이 어렵습니다.
에러 메세지는 무슨 일이 일어났는 지, 사용자가 해결을 위해 어떤 조치를 해야하는 지가 포함되는 것을 권장합니다.

마무리

저자는 이 장에 설명된 코드 악취도 프로젝트나 개인의 기본 설정에 따라 위에 정리한 내용을 지킨다고 무조건 좋은 코드가 나오는 것은 아니라고 마무리를 했습니다.
하지만 이 챕터를 공부하면서 느낀것은 클린코드는 다른 사람이 이해하기 좋은 코드라는 의미도 있지만, 코드를 작성하는 과정에서 발생하는 실수를 방지하고 유지 보수를 수월하게 할 수 있도록 하는 과정이라고 느꼈습니다.
그래서 클린코드는 기술 부채를 줄이기 위한 중요한 내용이라고 생각하지만, 예전에 배달의 민족 CEO분의 개발에 대한 인터뷰를 보았을 때가 생각이 났었습니다.
기술 부채는 서비스가 개발이 되었기에 생기는 것이니 우선은 주어진 문제를 해결하는 것도 중요하게 생각을 한다고 했었는데, 이러한 점을 보면 클린코드에 대해 정답을 없는 것 같고 앞으로 경험을 쌓으며 균형을 맞추어야 할 것 같습니다.


Reference


[Beyond the Basic Stuff with Python_Al Sweigart] - https://inventwithpython.com/beyond/
[Chapter 5 - Code Formatting with Black] - https://inventwithpython.com/beyond/chapter5.html
CC License - [CC BY-NC-SA 3.0]

Translator - ChatGPT


댓글남기기