Skip to content

9주차 · 함수 리팩토링과 테스트

이 장을 마치면 다음을 할 수 있습니다.

  • W5에서 배운 함수를 “더 잘 쓰는” 방법을 단계별로 설명하기
  • 긴 스크립트를 입력/계산/출력 역할 함수로 분해하기
  • 함수 이름을 읽는 즉시 역할이 보이게 짓기
  • 지역 변수와 전역 변수의 범위 차이를 코드로 설명하기
  • 부작용(side effect)이 디버깅을 어렵게 하는 이유를 이해하기
  • assert로 리팩토링 전후 동작을 보호하기
  • Bad/Good 예시로 책임 혼합 코드와 분리된 코드의 차이를 비교하기

이번 주 이야기: “코드가 동작하는데, 왜 다시 쓰나요?”

Section titled “이번 주 이야기: “코드가 동작하는데, 왜 다시 쓰나요?””

W5에서 함수를 처음 배웠고, W6(자료구조)·W7(문자열 파싱)에서 함수를 활용했습니다. 이제 질문이 생깁니다.

“코드가 잘 돌아가는데, 뭘 더 고쳐야 하나요?”

두 가지 상황을 비교해 보겠습니다.

# 상황 A: 동작하지만 읽기 어려운 코드
scores = [70, 80, 90]
total = 0
for x in scores:
total += x
avg = total / len(scores)
if avg >= 80: print("B")
else: print("C")
# 상황 B: 동작하고 읽기도 쉬운 코드
def calculate_average(scores):
return sum(scores) / len(scores)
def convert_to_grade(avg):
return "B" if avg >= 80 else "C"
scores = [70, 80, 90]
print(convert_to_grade(calculate_average(scores)))

둘 다 같은 결과를 냅니다. 하지만 상황 B는 나중에 점수 목록이 바뀌어도, 등급 기준이 바뀌어도 고칠 곳이 딱 한 군데입니다. 이것이 리팩토링의 목표입니다.


리팩토링 quickstart: 어디서부터 자를까요?

Section titled “리팩토링 quickstart: 어디서부터 자를까요?”

가장 쉬운 출발점은 입력 / 계산 / 판정 / 출력 4칸으로 나누는 것입니다.

def read_input():
pass # TODO: your code
def compute_value(data):
pass # TODO: your code
def classify_result(value):
pass # TODO: your code
def main():
data = read_input()
value = compute_value(data)
label = classify_result(value)
print(label)

처음 리팩토링할 때 체크할 4가지

Section titled “처음 리팩토링할 때 체크할 4가지”
  1. 한 함수는 한 가지 책임만 맡긴다.
  2. 함수 이름을 보면 “무엇을 하는지” 바로 보여야 한다.
  3. 함수 밖 상태를 몰래 바꾸지 말고, 가능하면 return으로 돌려준다.
  4. 리팩토링 전후 결과가 같은지 assert나 테스트로 확인한다.

초보자 코드는 보통 “한 파일 + 긴 순차 코드”로 시작합니다. 처음에는 빠르지만, 기능이 늘어나면 문제가 생깁니다.

  • 같은 계산을 여러 번 복사하게 됨
  • 한 줄 수정이 다른 곳에서 버그를 만듦
  • 테스트 지점이 없어 “맞는지” 확인이 어려움

함수는 “한 가지 책임”을 이름 붙여 묶는 도구입니다.

구조 변화: 큰 덩어리 코드 → 작은 단위 함수

Section titled “구조 변화: 큰 덩어리 코드 → 작은 단위 함수”
리팩토링 전/후 구조
flowchart TB
  A[giant_script.py<br/>입력+계산+판정+출력 혼합] --> B[책임별 분해]
  B --> C[read_input 함수<br/>입력 담당]
  B --> D[calc_* 함수<br/>계산 담당]
  B --> E[classify_* 함수<br/>판정 담당]
  B --> F[main 함수<br/>조합 담당]

문제를 4단계로 나누세요.

  1. 입력 받기
  2. 계산/변환
  3. 검증
  4. 출력

각 단계를 함수 1개 이상으로 만듭니다.

  • 좋은 이름: calculate_average, parse_score_line, build_report
  • 나쁜 이름: doit, x, temp2

규칙: 동사 + 대상(verb + object)

  • 지역 변수(local variable): 함수 안에서만 존재
  • 전역 변수(global variable): 파일 전체에서 접근 가능

초보 단계에서는 global 사용을 최소화하면 버그가 크게 줄어듭니다.

함수 밖 상태를 바꾸면 부작용(side effect)이 생깁니다.

  • 전역 변수 변경
  • 파일 쓰기
  • 외부 시스템 호출

부작용이 있는 함수는 테스트가 어렵기 때문에 경계를 명확히 두는 것이 좋습니다.

assert calculate_average([10, 20, 30]) == 20

리팩토링 중에는 assert가 안전벨트입니다. 함수 동작이 바뀌지 않았음을 자동으로 확인해 줍니다.


C와 비교: 긴 절차 코드에서 함수 분리하는 감각

Section titled “C와 비교: 긴 절차 코드에서 함수 분리하는 감각”
항목CPython기억할 점
함수 선언타입 + 이름 필요def로 시작Python은 문법이 짧아 분리가 더 쉽다
블록 구분{ ... }들여쓰기함수 경계가 공백으로 보인다
전역 상태 의존흔함가능하지만 권장하지 않음초보 단계에서는 입력/반환 중심이 안전
리팩토링 확인수동 출력 비교assert + 작은 함수 테스트Python이 빠른 검증에 유리
/* C 스타일 긴 절차 코드 */
for (int i = 0; i < n; i++) {
total += scores[i];
}
avg = total / n;
def calculate_average(scores):
return sum(scores) / len(scores)

핵심은 Python에서는 짧은 helper 함수를 부담 없이 만들고, 반환값으로 연결하는 습관을 들이는 것입니다.


3) 출력 예측 연습: 이 코드는 무엇을 출력할까요?

Section titled “3) 출력 예측 연습: 이 코드는 무엇을 출력할까요?”

리팩토링 실력을 키우려면 코드를 머릿속에서 실행하는 훈련이 필요합니다.

x = 10
def add_five(x):
x = x + 5
return x
result = add_five(x)
print(x) # (1) 무엇이 출력될까요?
print(result) # (2) 무엇이 출력될까요?
counter = 0
def increment():
global counter
counter += 1
increment()
increment()
print(counter) # 무엇이 출력될까요?

아래 코드에서 버그를 찾아보세요.

def calculate_average(scores):
total = sum(scores)
avg = total / len(scores)
print(avg) # ← 여기가 문제
result = calculate_average([70, 80, 90])
print(result * 2) # TypeError!

문제: print(avg)만 하고 return이 없습니다. 함수가 None을 돌려줘서 곱셈에서 오류가 납니다.

수정: print(avg)return avg

버그 B: 전역 변수 의존으로 결과가 달라진다

Section titled “버그 B: 전역 변수 의존으로 결과가 달라진다”
total = 100
def add_score(score):
global total
total += score
add_score(20)
add_score(20)
print(total) # 140 — 기대값이 달라질 수 있음

문제: 함수를 몇 번 호출했는지에 따라 결과가 달라집니다. total의 초기값이 무엇이었는지 추적해야 합니다.

수정: 전역 변수를 없애고 인자로 전달하세요.

def add_score(current_total, score):
return current_total + score

scores = [71, 83, 95, 64, 88]
total = 0
for x in scores:
total += x
avg = total / len(scores)
print("avg", avg)
if avg >= 90:
print("A")
elif avg >= 80:
print("B")
elif avg >= 70:
print("C")
else:
print("D")
def calculate_average(scores):
return sum(scores) / len(scores)
def convert_to_grade(score):
if score >= 90:
return "A"
if score >= 80:
return "B"
if score >= 70:
return "C"
return "D"
scores = [71, 83, 95, 64, 88]
avg = calculate_average(scores)
grade = convert_to_grade(avg)
print("avg", round(avg, 2)) # round(값, 소수점자리수): 소수점 2자리로 반올림
print("grade", grade)

실수왜 생기나위험대안
100줄 함수 1개빨리 완성하고 싶어서수정 시 연쇄 버그책임별 10~20줄 함수로 분리
함수가 print만 함눈으로 확인 습관재사용/테스트 어려움계산 함수는 return 우선
전역 변수 의존편의성 착각실행 순서 따라 결과 변경인자 전달 방식 사용
이름이 tmp, data2네이밍 투자 부족협업 시 오해동사+대상 규칙
assert 없음”돌아가면 됨” 사고리팩토링 회귀 버그핵심 규칙마다 assert 1개 이상
기능추가+리팩토링 동시 진행단계 분리 미흡원인 추적 실패먼저 보호(assert), 그다음 구조 개선

예제 1 · 스크립트를 함수로 나누기 Runs in-browser with Pyodide
Ready
  1. calculate_average는 입력 리스트를 평균으로 바꾸는 단일 책임 함수입니다.
  2. convert_to_grade는 점수 규칙만 다룹니다(계산/출력과 분리).
  3. summarize_scores는 작은 함수들을 조합하는 오케스트레이터 역할입니다.
  4. 실행 파트는 함수 정의와 분리하여 “입력만 바꿔 재사용”이 가능하게 만듭니다.
  5. round(값, 2)로 소수점 2자리까지만 출력해 가독성을 높입니다.
  6. assert 3개는 평균, 등급, 경계값(70점)을 동시에 보호합니다.
  7. 함수명을 일부러 x처럼 바꿔본 뒤 다시 복구해 가독성 차이를 체감해 보세요.

예제 2 · 전역 부작용 줄이기 Runs in-browser with Pyodide
Ready

아래 표는 코드를 순서대로 실행할 때 각 변수가 어떻게 바뀌는지 보여줍니다.

실행 순서호출counter (전역)pure_counter출력
시작0start counter: 0
increase_local(counter)지역 복사본만 변경0 (그대로)local call: 1
after local 출력0 (그대로)after local: 0
increase_global() 1번전역 counter 변경1global call 1: 1
increase_global() 2번전역 counter 변경2global call 2: 2
final counter 출력2final counter: 2
increase_pure(0)새 값 반환1
increase_pure(1)새 값 반환2

핵심 관찰: increase_local은 전역 counter를 바꾸지 않습니다. increase_global은 호출할 때마다 전역을 누적 변경합니다. increase_pure는 전역을 전혀 건드리지 않아 가장 안전합니다.

  1. 처음 출력에서 after local이 0인지 확인해 local 변경 범위를 이해합니다.
  2. increase_global 호출 뒤 값이 누적되는 이유를 “외부 상태 변경”으로 설명해봅니다.
  3. 전역 상태 패턴은 작은 예제에서는 동작해도, 규모가 커지면 추적 비용이 커집니다.
  4. increase_pure처럼 입력→출력만 있는 함수로 바꾸면 테스트가 쉬워집니다.
  5. 리팩토링 후에도 assert pure_counter == 2로 동일 동작을 증명합니다.

문제: 중복된 평균 계산 코드를 함수 1개로 추출하세요.

실습 문제 1 · 중복 로직 분리 Runs in-browser with Pyodide
Ready