파이썬 에러 방지하기 - 코드 악취에 대해서

Heebeom·2023년 3월 4일
1
post-thumbnail

혹시 몇 시간에 걸쳐 에러를 디버깅했는데, 알고보니 사소한 것이 원인이었던 적이 있는가? 프로그래머도 사람이기 때문에 이런 실수는 언제나 할 수 있고, 이를 100% 방지할 수는 없을 것이다.

하지만 이런 상황을 최소화시킬 수는 있다. 냄새로 가스 누출을 알아채는 것처럼, 프로그램에서도 버그의 냄새를 맡을 수 있는데, 이를 코드 악취(Code Smell)라고 한다.

오늘 포스팅에서는 이러한 코드 악취의 대표적인 사례를 알아보겠다.

본 포스팅은 Al Sweigart의 저서인 『클린 코드, 이제는 파이썬이다』의 일부를 기반으로 작성되었습니다.


1. 중복된 코드

중복된 코드는 변경하기 어렵다

중복된 코드(duplicated code)란 아래와 같이 같은 로직을 여러번 복사해서 붙여넣는 것을 말한다.

print('좋은 아침입니다!')
print('오늘 기분은 어떠세요?)
feeling = input()
print('오늘 ' + feeling + '을 느끼신다니 기쁘네요.)

print('좋은 점심입니다!')
print('오늘 기분은 어떠세요?)
feeling = input()
print('오늘 ' + feeling + '을 느끼신다니 기쁘네요.)

print('좋은 저녁입니다!')
print('오늘 기분은 어떠세요?)
feeling = input()
print('오늘 ' + feeling + '을 느끼신다니 기쁘네요.)

이 코드의 가장 큰 문제점은 코드를 변경하기 까다롭다는 것이다. 즉, 중복된 코드의 복사본 하나를 변경하면 그 프로그램 내의 모든 복사본도 함께 변경해야 한다.

만약 위 코드를 무엇을 먹었는지 물어보게 바꿔 보자. 당신은 복사본을 모두 바꾸다가 깜빡 잊어버리는 부분이 있을 것이고, 결국 그 프로그램은 버그 투성이가 될 것이다.

해결 방법은 중복 자체를 없애는 것

중복된 코드를 해결하는 방법은 중복 자체를 없애는 것이다. 즉, 코드를 함수나 반복문을 사용해 프로그램 내에서 한 번만 나타나게 해야 한다.

def ask_feeling(time_of_day):
	print('좋은 ' + {time_of_day} + ' 입니다!'
    print('오늘 기분은 어떠세요?')
    feeling = input()
    print('오늘 ' + feeling + '을 느끼신다니 기쁘네요.)
 
 
 for time_of_day in ['아침', '점심', '저녁']:
 	ask_feeling(time_of_day)

위 코드는 함수와 반복문을 사용해 중복된 코드를 해결한 예이다.

  1. 기분을 물어보는 로직을 따로 ask_feeling 함수로 분리하였다.
  2. '아침/점심/저녁'을 묻는 부분을 ask_feeling 함수에 파라미터로 전달하였다.

이제 프로그램을 무엇을 먹었는지 물어보게 바꾸기 위해, 당신은 ask_feeling 함수를 ask_meal 함수로 변경하기만 하면 된다!

2. 매직 넘버

매직 넘버는 의미가 불분명하다

expiration = time.time() + 604800

위 코드를 분석해 보자. time.time()은 현재 시각을 반환하고, expiration 변수는 약 604,800초 이후를 나타낼 것이다.

그런데 604,800이라는 숫자는 무엇을 의미하는가? 계산해보면 1주일인 것을 알 수 있지만, 언뜻 보기에는 이해할 수 없다.

매직 넘버를 상수로 대체하자

SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECOND_PER_MINUTE
SECONDS_PER_DAY = 24 * SECOND_PER_HOUR
SECONDS_PER_WEEK = 7 * SECOND_PER_DAY

expiration = time.time() + SECONDS_PER_WEEK

이를 해결하기 위해 주석을 달 수도 있지만, 상수(Constant Number)를 사용하는 것이 좋다. 위 코드는 한눈에 보기에도 '1주일 뒤의 시간을 expiration 변수에 담는다'인 것을 명확하게 알 수 있다.

같은 값이라도 다른 용도라면 상수를 분리하자

카드 한 세트와 2년을 주로 환산한 결과를 출력하는 프로그램을 작성해 보자.

카드 한 세트는 52장의 카드가 있고, 1년은 52주가 있다. 둘 다 52이라는 값은 같지만, 용도가 다르기 때문에 아래와 같이 작성해야 한다.

NUM_CARDS_IN_DECK = 52
NUM_WEES_IN_YEAR = 52

print('덱에 카드가 ' + NUM_CARDS_IN_DECK + '장 있습니다.')
print('2년은 ' + (2 * NUM_WEES_IN_YEAR) + '주 입니다.')

위 코드와 같이 상수를 분리한다면, 나중에 독립적으로 상수의 값을 변경할 수 있다. 예를 들어 카드 한 세트에 조커 카드를 추가한다면, NUM_CARD_IN_DECK 상수만 53으로 변경하면 된다.

상수를 사용하면 디버깅에 도움이 된다

매직 넘버의 오타는 발견하기 어렵다. 에를 들어 사용자의 방향을 입력받고, 값이 'north'면 경고를 출력하는 프로그램을 작성해 보자.

while True:
    print('태양열 패널의 방향을 설정하세요.')
    direction = input().lower()
    if direction in ('north', 'south', 'west', 'east'):
  		break

# 오타 발생!
if direction == 'nrth':
	print('경고: 이 패널은 북쪽을 바라보면 충전 효율이 떨어집니다.')

위 코드는 의도대로 작동하지 않지만, 문법은 맞기 때문에 프로그램이 정상적으로 실행되는데다 경고 메세지도 출력되지 않는다.

하지만 NORTH라는 상수를 따로 만들면 어떨까? 그렇다면 파이썬은 NRCH라는 변수가 없기 때문에 NameError를 발생할 것이고, 쉽게 오타를 발견할 수 있을 것이다.

3. 주석 처리된 코드와 죽은 코드

이러한 코드는 궁금증을 남긴다

테스트 과정에서 주석 기능을 이용하는 경우가 많다. 하지만 주석 처리된 코드가 그대로 남아 있으면 왜 코드가 제거되었는지, 또 필요한 코드인지 전혀 모르는 상태가 된다.

또한 죽은 코드(dead code)도 마찬가지다. 죽은 코드는 논리적으로 도달할 수 없는 코드란 뜻으로, return문 뒤에 있거나 항상 False인 조건문 안에 있는 코드를 말한다.

import random

def flip_coin():
	if random.randint(0, 1)
    	return '앞면'
    else:
    	return '뒷면'
    return '동전이 세로로 섬'

위 코드에선 if와 esle 블록에서 함수가 끝나기 때문에, return '동전이 세로로 섬'은 죽은 코드이다. 하지만 언뜻 봐서는 '앞면' 혹은 '뒷면' 혹은 '동전이 세로로 섬'의 3개의 값 중에 하나를 반환하는 함수로 보일 수 있다.

이처럼 주석된 코드와 죽은 코드는 다른 개발자 입장에서 어떤 의도로 궁금중을 남기고, 다른 의도로 프로그램을 해석할 수 있다. 이를 해결하기 위해 깃(Git) 같은 형상 관리 프로그램을 사용하자.

4. 숫자 접미사가 붙은 변수

숫자 접미사보다 고유한 이름을 사용하자

프로그램을 개발할 때 동일한 종류의 데이터가 여러 개 필요할 수 있다. 이럴 때 password1, password2 같이 숫자를 사용할 수도 있는데, 이런 경우는 변수가 무엇을 포함하고 어떤 차이가 있는지 알 수 없다.

이런 경우 password와 confirm_password 같이 고유한 이름을 사용해야 한다. 이 경우 변수의 뜻이 모호하지 않으며, confirm_password가 비밀번호 확인을 위한 변수인 것을 단번에 일 수 있다.

3개 이상일 경우는 리스트(list)를 사용하자

3개 이상의 값을 사용하는 경우 리스트(list)나 집합(set)을 사용하는 것이 좋다. 50개의 동물 이름을 입력받을 때, pet1Name, pet2Name ... 처럼 50개의 변수를 사용하는 것 보다 petName라는 리스트 하나에 저장하는 것이 훨씬 효율적이다.

5. 중첩된 리스트 컴프리헨션

리스트 컴프리핸션은 간결하며, 코드의 가독성을 높힌다.

리스트 컴프리핸션(list comprehension)은 파이썬만의 강력한 기능이다. 이 기능을 적절히 사용하면 아래와 같이 코드를 매우 간결하게 만들 수 있다.

# 0부터 100까지 5의 배수를 제외한 숫자 list 생성하기
# 반복문과 조건문을 사용한 경우
arr = []
for number in range(100):
	if number % 5 != 0:
    	arr.append(number)

# 리스트 컴프리핸션을 사용한 경우
arr = [str(number) for number in range(100) if number % 5 != 0]

너무 과하면 코드를 읽기 어렵게 만든다.

리스트 컴프리핸션을 과하게 사용하면 적은 양의 코드에 많은 로직을 쑤셔넣게 되고, 되려 읽기 어렵게 만든다. 중첩된 리스트 컴프리핸션의 예제를 보자.

# 2차원 배열을 1차원 배열로 평탄화하기
nested_list = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
flat_list = [num for sub_list in nested_list for num in sub_list]

위 코드를 보면 리스트 컴프리핸션에 두 개의 반복문을 포함하고 있는데, 이는 숙련된 개발자들도 한 번에 이해해가 어려울 것이다. 이럴 경우는 두 개의 반복문을 사용하는 것이 읽기가 훨씬 쉽다.


마치며

이번 포스팅에서 코드 악취와 대표적인 예시 5가지를 알아보았다. 코드 악취는 직접적으로 오류를 발생하지 않지만, 개발자로 하여금 실수하게 만드는 나쁜 버릇이다.

아무리 뛰어난 개발자라고 해도, 컴퓨터가 아니기 떄문에 언젠가는 실수하기 마련이다. 이 때, 악취가 없는 코드는 최소한의 시간으로 디버그하는데 도움을 줄 것이다 :)

profile
이도저도 아닌 개발자

0개의 댓글