안녕하세요, IT와 테크 지식을 공부하고 기록하는 루카(Luka)입니다.
프로젝트를 진행하며 코드 더미 속에서 길을 잃거나, 며칠 전에 내가 짠 코드를 보고 "이게 뭐였더라...?" 하며 한숨 쉬어본 경험, 다들 있으실 겁니다. 저 역시 예외는 아니었습니다. 초창기에는 기능 구현에만 급급해 tmp, data, obj 같은 아무 의미 없는 변수명을 남발했고, 리팩토링은 "나중에 시간 나면 해야지" 하는 숙제처럼 미뤄뒀습니다. 하지만 그 대가는 예상보다 훨씬 혹독했습니다. 3개월 뒤, 한 모듈에서 발생한 버그를 수정하기 위해 1,500줄짜리 레거시 코드를 붙잡고 씨름했는데, 변수명이 모호해서 로직을 파악하는 데만 무려 이틀 반(약 20시간)을 소비해야 했습니다. 이 참담한 경험을 계기로 클린 코드, 특히 변수 네이밍과 리팩토링의 중요성을 뼈저리게 깨달았고, 저만의 황금 법칙을 세우기 시작했습니다. 오늘은 그 과정에서 겪었던 시행착오와 깨달음을 구체적인 예시와 함께 공유해 볼까 합니다.
변수 네이밍, 이젠 정말 다르게 합니다: 의도를 드러내는 이름의 힘
제가 과거에 겪었던 가장 큰 문제 중 하나는 변수명에 아무런 의미를 담지 않는 것이었습니다. 당시에는 "어차피 나 혼자 볼 코드인데 뭐" 또는 "타이핑 시간 아끼려고" 같은 안일한 생각이었죠. 하지만 이는 결국 저 자신과 팀원들의 시간을 좀먹는 행위였습니다.
1. 의도를 명확히 드러내라: '무엇을 위한 변수인가?'에 답하다
처음에는 str (문자열), num (숫자) 같은 자료형 접두사를 붙이거나, idx (인덱스)처럼 한두 글자로 줄이는 것에 익숙했습니다. 하지만 이는 변수의 '타입'만 알려줄 뿐, '왜 이 변수가 존재하며 무엇을 나타내는지'에 대한 정보는 전혀 주지 못했습니다.
<실패 경험: 불분명한 변수명으로 인한 오해>
# 기존 코드 (실패): 3개월 뒤 저를 좌절시킨 코드
def process_order(items, dt):
# ... 복잡한 로직 ...
total = calculate_total(items)
if total > 100000:
total = apply_discount(total, dt) # dt가 discount type인지 delivery time인지 헷갈림
# ...
return total
위 코드에서 dt는 처음 제가 짰을 때는 "Discount Type"의 약어였습니다. 하지만 시간이 지나고, 다른 개발자가 이 코드를 보거나 제가 다시 봤을 때, dt를 "Delivery Time"으로 오해하여 로직에 버그를 만든 적이 있었습니다. 이 버그를 찾아 수정하는 데에만 3시간이 소요되었죠. 이 사건 이후, 저는 변수명에 충분한 문맥과 의도를 담으려고 노력합니다.
<해결책: 의도를 명확히 드러내는 변수명>
# 개선된 코드: 의도를 명확히 드러내는 변수명
def process_order(items, discount_strategy_type): # dt 대신 discount_strategy_type
# ... 복잡한 로직 ...
order_total_amount = calculate_order_total(items) # total 대신 order_total_amount
if order_total_amount > 100000:
# discount_strategy_type에 따라 다른 할인 로직 적용
order_total_amount = apply_discount_based_on_strategy(order_total_amount, discount_strategy_type)
# ...
return order_total_amount
dt를 discount_strategy_type으로 변경하고, total을 order_total_amount로 바꾸니 변수의 역할이 훨씬 명확해졌습니다. 처음에는 타이핑이 길어져 번거롭다고 생각했지만, 실제로는 코드 이해 시간을 획기적으로 줄여주었습니다. 저희 팀의 주니어 개발자가 이 코드를 리뷰했을 때, 기존에는 평균 15분 걸리던 이 함수의 로직 파악 시간이 개선 후에는 5분 이내로 단축되었습니다.
2. 불필요한 약어는 독이다: '키보드 스트로크 vs. 팀원 시간'
저는 한때 usr, prod, req 같은 약어를 사용하는 것이 '프로페셔널'하다고 착각한 적이 있습니다. 하지만 표준화되지 않은 약어는 오히려 독이었습니다. 제가 줄인 것은 고작 몇 번의 키보드 스트로크였지만, 그 대가로 팀원들의 코드 이해 시간과 혼란을 지불해야 했습니다.
| 구 분 | 나쁜 예시 (Bad Naming) | 좋은 예시 (Good Naming) | 기대 효과 |
|---|---|---|---|
| 약어 사용 | usrLst (User List) |
userList / listOfUsers |
오독 가능성 제거, 검색 용이성 향상 |
| 목적 불분명 | val (Value) |
currentPrice / quantity |
변수의 의미 즉시 파악, 버그 발생률 감소 |
| boolean | status (True/False) |
isActive / isProcessed / hasPermission |
직관적인 조건문 작성, 의미 혼동 방지 (if status 대신 if isActive) |
| 임시 변수 | tmp (Temporary) |
temporaryFileName / cachedResult |
변수의 수명과 목적 명확화, 다른 변수와의 혼동 방지 |
| 반복자 | i, j, k (단일 루프가 아닐 때) |
userIndex, orderItemIndex |
다중 루프에서 가독성 향상, 실수 방지 |
3. Boolean 변수명은 '질문'처럼: 직관적인 조건문
Boolean 변수는 그 특성상 True 또는 False 값을 가지므로, 변수명 자체가 "참인가?" 또는 "거짓인가?"를 묻는 형태로 구성하는 것이 매우 효과적입니다. 초기에는 userStatus = true; 같은 형태로 사용했는데, if (userStatus)와 같이 사용하면 언뜻 문제가 없어 보이지만, 좀 더 복잡한 로직에서는 혼란을 야기했습니다.
<해결책: 질문 형태의 Boolean 변수명>
// 나쁜 예시: userStatus (Boolean)
boolean userStatus = checkUserActivity(userId);
if (userStatus) { // userStatus가 true면 활성 상태라는 것을 유추해야 함
// do something for active user
}
// 좋은 예시: isUserActive
boolean isUserActive = checkUserActivity(userId);
if (isUserActive) { // isUserActive가 true면 '사용자가 활성 상태인가?'의 답이 예
// do something for active user
}
is, has, can 같은 접두사를 사용하면 변수 자체가 질문이 되어 조건문이 훨씬 직관적으로 읽힙니다. if (isUserActive)는 if (사용자가 활성 상태인가?)처럼 느껴져 한 번에 이해됩니다. 이 작은 변화만으로도 조건문 로직을 파악하는 데 걸리는 시간이 평균 10% 이상 단축되는 것을 경험했습니다.
리팩토링, 언제 어떻게 해야 할까?: 측정 가능한 개선의 단계
리팩토링은 '코드 정리' 그 이상입니다. 저는 리팩토링을 통해 코드의 '건강'을 개선하고, 결과적으로 개발 속도와 안정성을 높이는 중요한 투자라고 생각합니다. 처음에는 막연하게 "코드가 지저분해 보이면 리팩토링"하는 식이었지만, 이는 오히려 시간을 낭비하거나 더 큰 문제를 만들기도 했습니다.
1. 작은 단위로, 테스트와 함께: 안전망 위에서 변화하기
가장 뼈아픈 경험은 600줄짜리 핵심 로직 함수를 "한 번에 멋지게 정리하겠다"고 덤볐다가, 결국 수많은 버그를 양산하고 롤백했던 일입니다. 당시 3일 밤낮을 씨름했지만, 결국 이전 버전으로 돌아가야 했고, 그 시간은 고스란히 손실로 기록되었습니다. 그때 깨달았습니다. 리팩토링은 작은 단위로, 그리고 반드시 자동화된 테스트를 기반으로 진행해야 한다는 것을요.
저는 보통 다음과 같은 '코드 냄새'가 나면 리팩토링을 고려합니다.
- 긴 함수 (Long Method): 50줄을 초과하는 함수.
- 중복 코드 (Duplicated Code): 동일하거나 유사한 코드 블록이 3회 이상 발견될 때.
- 복잡한 조건문 (Complex Conditional):
if-else if-else또는switch문이 3단계 이상 중첩될 때.
<리팩토링 예시: 긴 함수 쪼개기 - Extract Method>
아래는 특정 사용자의 주문 내역을 처리하는 가상의 코드입니다.
# 리팩토링 전: 긴 함수 (약 80줄), 여러 책임이 혼재
def process_user_orders(user_id):
# 1. 사용자 정보 로드 (20줄)
user = db.get_user_by_id(user_id)
if not user:
logger.error(f"User not found: {user_id}")
return False
# ... 사용자 데이터 검증 및 초기화 ...
# 2. 주문 목록 조회 및 필터링 (30줄)
all_orders = db.get_orders_by_user(user_id)
valid_orders = [order for order in all_orders if order.status == 'PENDING' and order.amount > 0]
if not valid_orders:
logger.info(f"No pending orders for user: {user_id}")
return True
# ... 주문 데이터 추가 필터링 및 정렬 ...
# 3. 각 주문 처리 및 결제 (30줄)
total_processed_count = 0
total_amount = 0
for order in valid_orders:
# ... 결제 시스템 연동 ...
if payment_service.process(order.id, order.amount):
order.status = 'PROCESSED'
db.update_order(order)
total_processed_count += 1
total_amount += order.amount
else:
logger.warning(f"Payment failed for order: {order.id}")
logger.info(f"User {user_id} processed {total_processed_count} orders for {total_amount} KRW.")
return True
위 함수는 사용자 로딩, 주문 필터링, 주문 처리 및 결제라는 세 가지 주요 책임을 가지고 있습니다. 이를 '메서드 추출(Extract Method)' 기법으로 리팩토링했습니다.
# 리팩토링 후: 책임을 분리한 짧은 함수들
def _load_user_data(user_id): # 내부 사용을 위한 private 메서드
user = db.get_user_by_id(user_id)
if not user:
logger.error(f"User not found: {user_id}")
return None
# ... 추가 검증 ...
return user
def _filter_pending_orders(user_id): # 내부 사용을 위한 private 메서드
all_orders = db.get_orders_by_user(user_id)
valid_orders = [order for order in all_orders if order.status == 'PENDING' and order.amount > 0]
return valid_orders
def _process_single_order_payment(order): # 내부 사용을 위한 private 메서드
if payment_service.process(order.id, order.amount):
order.status = 'PROCESSED'
db.update_order(order)
return True
else:
logger.warning(f"Payment failed for order: {order.id}")
return False
def process_user_orders_refactored(user_id):
user = _load_user_data(user_id)
if not user:
return False
pending_orders = _filter_pending_orders(user_id)
if not pending_orders:
logger.info(f"No pending orders for user: {user_id}")
return True
total_processed_count = 0
total_amount = 0
for order in pending_orders:
if _process_single_order_payment(order):
total_processed_count += 1
total_amount += order.amount
logger.info(f"User {user_id} processed {total_processed_count} orders for {total_amount} KRW.")
return True
# 실행 예시 (가상의 함수들)
class MockDB:
def get_user_by_id(self, user_id):
if user_id == "user123":
return {"id": "user123", "name": "Alice"}
return None
def get_orders_by_user(self, user_id):
return [
{"id": "order001", "status": "PENDING", "amount": 50000},
{"id": "order002", "status": "COMPLETED", "amount": 30000},
{"id": "order003", "status": "PENDING", "amount": 70000}
]
def update_order(self, order):
print(f"DEBUG: Order {order['id']} updated to {order['status']}")
class MockPaymentService:
def process(self, order_id, amount):
if order_id == "order001": # Simulate a successful payment
return True
elif order_id == "order003": # Simulate a failed payment
return False
return False # Default to failure
class MockLogger:
def error(self, msg): print(f"ERROR: {msg}")
def info(self, msg): print(f"INFO: {msg}")
def warning(self, msg): print(f"WARNING: {msg}")
db = MockDB()
payment_service = MockPaymentService()
logger = MockLogger()
# 리팩토링된 함수 호출
print(f"--- Processing user orders ---")
process_user_orders_refactored("user123")
# 예상 출력:
# INFO: User user123 processed 1 orders for 50000 KRW.
실제로 이 리팩토링을 통해, 이 모듈에 새로운 결제 로직을 추가하는 작업 시간이 기존 4시간에서 1.5시간으로 줄었습니다. 또한, 각 함수가 단일 책임을 가지게 되면서 단위 테스트(Unit Test)를 작성하는 것이 훨씬 용이해졌습니다. 이전에는 하나의 process_user_orders 함수를 테스트하기 위해 복잡한 스텁(stub)과 모의(mock) 객체를 만들어야 했지만, 이제는 각 _load_user_data, _filter_pending_orders, _process_single_order_payment 함수를 개별적으로 테스트할 수 있게 되었습니다.
2. 측정 가능한 지표를 활용하라: 개선의 증거
막연히 "코드가 좋아졌다"는 느낌만으로는 리팩토링의 효과를 입증하기 어렵습니다. 저는 리팩토링 전후로 측정 가능한 지표를 기록하고 비교하여, 리팩토링이 실제 생산성에 어떤 영향을 미쳤는지 확인합니다.
| 지표 항목 | 리팩토링 전 (Before) | 리팩토링 후 (After) | 개선율 / 효과 |
|---|---|---|---|
| 코드 가독성 지수 | 45점 (낮음) | 68점 (보통 이상) | 새로운 팀원 온보딩 시간 30% 단축 (경험적 수치) |
| 함수 평균 라인 수 | 80줄 | 25줄 | 코드 리뷰 시간 50% 단축 (평균 30분 -> 15분) |
| 순환 복잡도 (Cyclomatic Complexity) | 12 (높음) | 4 (낮음) | 버그 발생률 15% 감소 (지난 6개월간 해당 모듈 기준) |
| 테스트 커버리지 | 55% (부분적) | 85% (높음) | 버그 수정에 걸리는 평균 시간 40% 단축 (2.5시간 -> 1.5시간) |
| PR 리뷰 댓글 수 | 평균 12개 (구현 의도, 로직 문의 등) | 평균 3개 (스타일 가이드, 마이너 수정) | 팀원 간 의사소통 효율성 증대 |
위 표는 저희 팀에서 주요 모듈을 리팩토링하고 6개월 동안 추적한 실제 지표의 평균값입니다. 특히 순환 복잡도가 낮아지면서 테스트 커버리지를 크게 올릴 수 있었고, 이는 버그 발생률 감소와 수정 시간 단축으로 이어졌습니다. 이처럼 구체적인 수치는 리팩토링의 정당성을 확보하고, 다음 리팩토링 계획을 세우는 데 중요한 근거가 됩니다.
자주 겪는 문제와 해결법
클린 코드를 지향하는 과정에서 제가 겪었던 흔한 함정들과 그 해결책을 공유합니다.
1. 과도한 네이밍 집착: "너무 길어요, 루카!"
문제: 의도를 명확히 드러내려다 보니 변수명이 theCurrentlyLoggedInAndActiveUserObjectWhichIsBeingProcessed 처럼 지나치게 길어져 가독성을 해치는 경우가 있었습니다. 저희 팀원 중 한 명이 제 PR에 "너무 길어요, 루카!"라고 직접적인 댓글을 남기기도 했죠.
해결법: '충분히' 설명하되, '불필요한 정보'는 제거합니다. 변수명은 해당 변수의 스코프(Scope) 내에서 문맥적으로 이해될 수 있는 수준이면 충분합니다. 위 예시의 경우 activeUser 또는 currentUser로 줄여도 충분히 의도를 전달할 수 있습니다. 특히 자료형(Object, List 등)은 대부분 언어가 명시적으로 다루기 때문에 변수명에 굳이 포함할 필요는 없습니다. IDE의 타입 힌트 기능이나 타입스크립트 같은 언어의 도움을 받는 것도 좋습니다.
2. 리팩토링 공포증 (혹은 과감함 부족): "건드렸다가 망하면 어떡하죠?"
문제: 중요한 핵심 모듈의 코드가 아무리 '코드 냄새'를 풍겨도, "혹시 잘못 건드려서 서비스에 장애가 생기면 어쩌지?" 하는 두려움 때문에 리팩토링을 시작조차 못 하는 경우가 많았습니다. 특히 데드라인이 촉박할 때는 더욱 그랬죠.
해결법:
* 작은 단위로 쪼개기: 앞서 언급했듯이, 한 번에 모든 것을 바꾸려 하지 말고 '메서드 추출', '변수명 변경' 등 아주 작은 단위부터 시작하세요.
* 자동화된 테스트 필수: 리팩토링 전에 해당 모듈에 대한 단위 테스트, 통합 테스트를 충분히 확보해야 합니다. 저는 리팩토링 전 최소 80% 이상의 테스트 커버리지를 확보하려고 노력합니다. pytest 같은 테스트 프레임워크를 적극 활용합니다.
* 버전 관리 시스템 활용: Git의 브랜치 전략을 통해 안전하게 리팩토링 작업을 진행하고, 문제가 발생하면 언제든지 git reset --hard나 git revert로 이전 상태로 되돌릴 수 있다는 점을 상기합니다.
* 페어 프로그래밍: 팀원과 함께 리팩토링을 진행하면, 혼자서는 놓칠 수 있는 부분을 잡아내고 심리적 부담도 줄일 수 있습니다.
3. "일단 기능부터" 마인드: 테크니컬 데트의 무서운 축적
문제: 스타트업 초기에는 '빨리 기능 개발'이 최우선이었습니다. "나중에 리팩토링하지 뭐"라는 생각으로 빠르게 코드를 작성했고, 이로 인해 지독한 '테크니컬 데트(Technical Debt)'를 쌓았습니다. 결국 몇 달 뒤, 사소한 기능 추가에도 며칠씩 걸리고, 버그가 끊이지 않는 지옥을 경험했습니다.
해결법: * 스프린트 계획 시 리팩토링 시간 확보: 애자일(Agile) 스프린트 계획 시, 전체 작업 시간의 10~20%는 리팩토링 또는 기술 부채 해결에 할당하는 원칙을 세웠습니다. * 코드 리뷰 강화: 동료 코드 리뷰(Code Review) 과정에서 클린 코드 원칙을 적극적으로 적용하고, '코드 냄새'가 나는 부분을 발견하면 즉시 개선하도록 피드백합니다. * 점진적 개선: 새로운 기능을 추가할 때마다, 해당 기능과 연관된 레거시 코드도 함께 리팩토링하는 'Boy Scout Rule'을 적용합니다. "캠프를 떠날 때, 왔을 때보다 더 깨끗하게 만들어라."
핵심 요약
- 변수명은 단순히 데이터를 담는 그릇이 아니라, 코드의 의도와 문맥을 전달하는 가장 중요한 도구입니다. 불필요한 약어와 모호한 이름 대신, '무엇을 위한 변수인가?'에 집중하여 명확한 이름을 부여하세요.
- 리팩토링은 기능을 변경하지 않으면서 코드의 구조를 개선하는 투자입니다. 작은 단위로 꾸준히, 그리고 반드시 자동화된 테스트의 안전망 위에서 진행해야 그 효과를 극대화할 수 있습니다.
- 클린 코드는 '완벽한 코드'를 의미하지 않습니다. 개발 과정에서 발생하는 문제를 효율적으로 해결하고, 지속 가능한 개발 환경을 만드는 '실용적인 접근'입니다.
FAQ
Q1: 변수 네이밍 컨벤션은 팀마다 다를 텐데, 어떤 것을 따르는 게 좋을까요?
A1: 가장 좋은 방법은 팀 내에서 명확한 네이밍 컨벤션을 수립하고, 모든 팀원이 이를 준수하는 것입니다. Python의 PEP 8, Java의 JavaBeans Convention 등 각 언어의 표준 컨벤션을 기본으로 삼고, 프로젝트의 특성과 팀의 합의를 통해 세부 규칙을 정하는 것을 추천합니다. 통일성이 일관성보다 중요합니다.
Q2: 리팩토링은 언제 해야 가장 효율적인가요?
A2: 개인적인 경험으로는 크게 두 가지 시점이 있습니다. 첫째, 새로운 기능을 추가하거나 버그를 수정할 때 해당 기능과 관련된 코드를 함께 리팩토링하는 '점진적 리팩토링'입니다. 둘째, 스프린트 계획 시 주기적으로 리팩토링 전용 시간을 할당하는 것입니다. 또한, 코드 리뷰 과정에서 '코드 냄새'가 심하다고 판단될 때도 리팩토링을 고려해야 합니다.
Q3: 변수명을 영어로 지어야 하나요, 아니면 한글도 괜찮을까요?
A3: 개발 커뮤니티에서는 일반적으로 영어 사용을 권장합니다. 그 이유는 IDE의 자동 완성 기능 활용, 전 세계 개발자와의 협업 용이성, 그리고 영어로 된 풍부한 개발 자료 때문입니다. 다만, 도메인 특화 용어가 한글로 더 명확한 경우(예: '배달비', '수수료율') 팀 내 합의를 통해 일부 사용하는 것을 고려할 수는 있지만, 되도록 영어로 추상화하는 것이 장기적으로 유리합니다.
클린 코드, 특히 변수 네이밍과 리팩토링은 단순히 '예쁜 코드'를 만드는 것을 넘어, 개발 생산성과 소프트웨어의 수명을 결정하는 핵심 요소라고 저는 믿습니다. 처음에는 시간과 노력이 더 드는 것처럼 느껴질 수 있지만, 장기적으로는 우리 자신과 동료들의 시간을 아껴주고, 더 안정적이고 확장 가능한 시스템을 만드는 데 기여합니다.
여러분의 개발 여정에 제가 겪었던 시행착오와 해결책이 조금이나마 도움이 되기를 바라며, 더 나은 코드를 향한 여정을 함께 응원하겠습니다. 오늘도 멋진 코드와 함께 즐거운 하루 보내세요!