Skip to content

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

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

  • W5에서 배운 함수를 “더 잘 쓰는” 방법을 단계별로 설명하기
  • 긴 스크립트를 입력/계산/판정/출력 역할 함수로 분해하기
  • 함수 이름을 읽는 즉시 역할이 보이게 짓기
  • 지역 변수와 전역 변수의 범위 차이를 코드로 설명하기
  • 부작용이 디버깅을 어렵게 하는 이유를 이해하기
  • 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는 나중에 점수 목록이 바뀌어도, 등급 기준이 바뀌어도 고칠 곳이 딱 한 군데입니다. 이것이 리팩토링의 목표입니다.


이번 주의 핵심은 새 문법을 많이 외우는 것이 아니라, 이미 배운 함수를 안전한 순서로 쓰는 것입니다.

  1. 현재 동작을 관찰합니다. 지금 코드가 어떤 값을 출력하거나 반환하는지 먼저 확인합니다.
  2. 중요한 동작을 assert로 잠급니다. “이 함수는 반드시 이 값을 돌려줘야 한다”는 조건을 코드로 적습니다.
  3. 작은 함수 하나만 추출합니다. 계산, 판정, 출력 중 한 역할만 먼저 분리합니다.
  4. 다시 실행합니다. assert가 통과하면 다음 역할을 분리하고, 실패하면 방금 바꾼 부분부터 확인합니다.

assert 조건은 “이 조건은 반드시 참이어야 한다”는 뜻입니다. 조건이 참이면 아무 메시지도 내지 않고 다음 줄로 넘어갑니다. 조건이 거짓이면 AssertionError가 발생하고 프로그램이 멈춥니다.

def add(a, b):
return a + b
assert add(2, 3) == 5
print("첫 번째 점검 완료")
assert add(2, 3) == 6, "2 + 3은 5여야 합니다"
print("이 줄은 실행되지 않습니다")

위 코드에서 첫 번째 assert는 통과합니다. 그래서 "첫 번째 점검 완료"가 출력됩니다. 두 번째 assert는 실패합니다. 그래서 AssertionError가 나오고 마지막 print()는 실행되지 않습니다.


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

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

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

역할스스로에게 물어볼 질문
입력데이터는 어디에서 오는가?리스트, 문자열, 사용자 입력
계산/변환숫자나 값을 어떻게 바꾸는가?평균 계산, 문자열을 숫자로 변환
판정조건에 따라 어떤 라벨을 붙이는가?A/B/C/D 등급 판정
출력사람에게 무엇을 보여주는가?print(), 리포트 문자열
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 키워드 사용을 최소화하면 버그가 크게 줄어듭니다.

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

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

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

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

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

def calculate_average(scores):
return sum(scores) / len(scores)
assert calculate_average([10, 20, 30]) == 20
assert calculate_average([70, 80, 90]) == 80
print("평균 함수 점검 완료")

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에서는 짧은 보조 함수를 부담 없이 만들고, 반환값으로 연결하는 습관을 들이는 것입니다.


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

아래 코드는 한 번에 “나쁜 코드 → 좋은 코드”로 바꾸는 것이 목표가 아닙니다. 실제 리팩토링은 다음 순서로 진행합니다.

  1. 먼저 Bad 코드의 현재 출력이 무엇인지 확인합니다.
  2. 그 출력이 유지되어야 한다는 조건을 assert로 적습니다.
  3. 평균 계산 부분만 함수로 뽑고 다시 실행합니다.
  4. 등급 판정 부분만 함수로 뽑고 다시 실행합니다.
  5. 모든 assert가 통과하면 출력 코드를 정리합니다.
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)

리팩토링 후에도 같은 동작인지 확인하기

Section titled “리팩토링 후에도 같은 동작인지 확인하기”
assert round(avg, 2) == 80.2
assert grade == "B"
assert convert_to_grade(70) == "C"
print("리팩토링 후 점검 완료")

첫 번째 assert는 평균 계산을, 두 번째 assert는 전체 결과를, 세 번째 assert는 등급 경계값을 보호합니다. 이렇게 해 두면 함수 이름이나 내부 구조를 바꾸더라도 동작이 달라졌는지 바로 알 수 있습니다.


6) 자주 나오는 실수/안티패턴 표

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

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

예제 2 · 전역 부작용 줄이기 코드를 실행하고 출력 결과를 확인하세요.
Ready

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

실행 순서호출counter (전역)safe_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 safe_counter == 2로 결과가 같은지 확인합니다.

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

목표: 두 반의 평균을 같은 방식으로 계산하므로, 평균 계산을 calculate_average(scores) 함수 하나로 분리합니다.

  • 입력: section_a = [70, 80, 90], section_b = [60, 75, 95]
  • 작성할 함수: calculate_average(scores) — 숫자 리스트를 받아 평균(float)을 반환합니다.
  • 완료 조건: 함수 안에서는 print()를 쓰지 않고, 출력은 함수 밖에서 합니다.
  • 필수 점검: assert calculate_average(section_a) == 80.0
  • 예상 출력: average A: 80.0, average B: 76.7
실습 문제 1 · 중복 로직 분리 코드를 실행하고 출력 결과를 확인하세요.
Ready

실습을 제출하거나 다음 문제로 넘어가기 전에 아래 항목을 확인하세요.

  • 계산 함수 안에 print()가 없는가?
  • 함수가 필요한 값을 return하는가?
  • 전역 변수를 직접 바꾸지 않는가?
  • 대표값과 경계값을 포함해 최소 2개 이상의 assert가 있는가?
  • 리팩토링 전후 출력이 같은가?
  • 실패한 assert가 있다면, 마지막으로 바꾼 함수 하나만 먼저 확인했는가?