리펙토링의 코드스멜(Code Smell)이해하기
리펙토링의 코드스멜(Code Smell)이해하기
코드 스멜(Code Smell)
소프트웨어 개발 과정에서 모든 코드는 시간이 지나며 복잡해지기 마련입니다. 특히, 처음에는 정상적으로 보이는 코드도 유지보수와 기능 추가를 거치며 점점 비효율적이고 읽기 어려운 상태로 변질될 수 있습니다. 이런 코드를 깨끗하게 정리하기 위해서는 “코드 스멜(Code Smell)”을 감지하고 이를 해결하는 리팩토링 작업이 필요합니다.
코드 스멜이란?
코드 스멜(Code Smell)은 명백한 오류는 아니지만, 코드가 복잡하거나 비효율적으로 작성되어 리팩토링이 필요하다는 신호를 의미합니다. “스멜(Smell)”이라는 용어는 코드를 읽을 때 문제가 있는 부분에서 나쁜 냄새가 난다는 은유적 표현입니다.
코드 스멜은 버그가 아닙니다. 그러나 버그를 유발할 가능성이 크고, 코드의 가독성과 유지보수성을 낮추는 원인이 됩니다.
대표적인 코드 스멜과 해결 방법
1. 중복 코드 (Duplicated Code)
문제점
- 동일하거나 유사한 코드가 여러 곳에 중복 작성되어 있습니다.
- 수정 시 중복된 모든 부분을 수정해야 하므로 오류 가능성이 높아집니다.
해결 방법
- 함수 추출(Extract Method): 중복된 코드를 하나의 함수로 추출.
- 상속이나 구성 사용: 클래스 구조를 재구성하여 중복 제거.
예제 코드
# Before: 중복된 코드
def calculate_discount(price):
if price > 100:
return price * 0.9
return price
def calculate_tax(price):
if price > 100:
= price * 0.9
price return price * 1.1
# After: 중복 코드 제거
def apply_discount(price):
if price > 100:
return price * 0.9
return price
def calculate_discount(price):
return apply_discount(price)
def calculate_tax(price):
= apply_discount(price)
price return price * 1.1
2. 긴 함수 (Long Method)
문제점
- 함수가 너무 길어져 한눈에 파악하기 어렵고, 여러 가지 책임을 동시에 수행.
해결 방법
- 함수 추출(Extract Method): 독립적인 기능을 별도의 함수로 분리.
- 매개변수 객체화: 함수가 너무 많은 매개변수를 받는 경우, 객체로 묶어 전달.
예제 코드
# Before: 긴 함수
def process_order(order):
# 주문 정보 검증
if not order["valid"]:
raise ValueError("Invalid order")
# 총액 계산
= 0
total for item in order["items"]:
+= item["price"]
total # 세금 추가
= total * 1.1
total return total
# After: 함수 분리
def validate_order(order):
if not order["valid"]:
raise ValueError("Invalid order")
def calculate_total(items):
return sum(item["price"] for item in items)
def process_order(order):
validate_order(order)= calculate_total(order["items"])
total return total * 1.1
3. 긴 매개변수 목록 (Long Parameter List)
문제점
- 함수가 너무 많은 매개변수를 요구하면 코드가 복잡하고, 호출이 번거로워집니다.
해결 방법
- 매개변수 객체화(Introduce Parameter Object): 관련 있는 매개변수를 객체로 묶어 전달.
- 빌더 패턴: 복잡한 초기화 과정을 간소화.
예제 코드
# Before: 긴 매개변수 목록
def create_user(first_name, last_name, age, email, address):
return {"first_name": first_name, "last_name": last_name, "age": age, "email": email, "address": address}
# After: 매개변수 객체화
class User:
def __init__(self, first_name, last_name, age, email, address):
self.first_name = first_name
self.last_name = last_name
self.age = age
self.email = email
self.address = address
def create_user(user_data):
return User(**user_data)
4. 매직 넘버 (Magic Numbers)
문제점
- 코드에 특정 숫자가 하드코딩되어 있어 의미를 이해하기 어렵습니다.
해결 방법:
- 상수로 대체(Replace Magic Number with Constant): 숫자를 의미 있는 상수로 치환.
예제 코드
# Before: 매직 넘버 사용
if user_age > 18:
print("Access granted")
# After: 상수로 대체
= 18
MINIMUM_AGE if user_age > MINIMUM_AGE:
print("Access granted")
5. 과도한 주석 (Excessive Comments)
문제점
- 코드 자체가 이해하기 어렵기 때문에 주석으로 보충해야 하는 경우 발생.
- 주석이 많아지면 코드가 장황해지고 가독성이 떨어질 수 있음.
해결 방법
- 자체 문서화된 코드(Self-Documenting Code): 주석 대신 명확한 변수명, 함수명을 사용.
- 중복된 주석 제거: 주석은 코드의 “왜(Why)”에 대해 설명하고, “어떻게(How)”는 코드가 직접 드러내게 작성.
예제 코드
# Before: 과도한 주석
# 이 함수는 숫자의 제곱을 계산합니다.
def calc_square(n):
# 제곱 계산
return n * n
# After: 자체 문서화된 코드
def calculate_square(number):
return number ** 2
6. 데이터 뭉치(Data Clumps)
데이터 뭉치(Data Clumps)는 특정 데이터 그룹이 여러 곳에서 반복적으로 사용되면서도 하나의 단위로 묶이지 않고, 독립적인 변수로만 존재하는 문제를 말합니다.
문제점
- 중복 코드 발생: 데이터 그룹이 여러 곳에서 반복적으로 사용되므로, 중복된 코드가 많아지고 유지보수가 어렵다.
- 확장성 저하: 데이터 그룹에 새로운 필드가 추가되면 모든 관련 코드를 수정해야 한다.
- 의미의 모호성: 관련 데이터가 독립된 변수로 존재하면 해당 데이터가 어떤 의미로 사용되는지 이해하기 어렵다.
해결 방법
- 데이터를 객체로 그룹화: 관련 데이터를 하나의 클래스나 데이터 구조로 묶는다.
- 명확한 표현: 객체를 사용하여 데이터의 의미와 연관성을 더 명확히 표현한다.
- 코드 재사용성 향상: 그룹화된 객체를 통해 동일한 데이터 그룹을 더 쉽게 재사용할 수 있다.
예제 코드
def print_address(street, city, postal_code):
print(f"Address: {street}, {city}, {postal_code}")
def calculate_shipping_cost(street, city, postal_code):
print(f"Calculating shipping cost for {city}, {postal_code}...")
return 5.99 # Mocked cost
# 여러 곳에서 동일한 데이터 그룹 사용
"123 Main St", "Springfield", "12345")
print_address(= calculate_shipping_cost("123 Main St", "Springfield", "12345")
cost print(f"Shipping Cost: {cost}")
# After 데이터 뭉치를 객체로 그룹화
class Address:
def __init__(self, street, city, postal_code):
self.street = street
self.city = city
self.postal_code = postal_code
def __str__(self):
return f"{self.street}, {self.city}, {self.postal_code}"
def print_address(address):
print(f"Address: {address}")
def calculate_shipping_cost(address):
print(f"Calculating shipping cost for {address.city}, {address.postal_code}...")
return 5.99 # Mocked cost
# 객체로 데이터를 묶어 사용
= Address("123 Main St", "Springfield", "12345")
address
print_address(address)= calculate_shipping_cost(address)
cost print(f"Shipping Cost: {cost}")
7. 과도한 의존성(Coupling)
문제점
- 변경의 전파: 한 클래스가 변경되면 이를 의존하는 다른 클래스들도 수정해야 하는 경우가 발생.
- 유지보수 어려움: 클래스 간 결합도가 높으면 전체 코드의 구조를 이해하기 어렵고 수정이 복잡해짐.
- 재사용성 저하: 강하게 결합된 클래스는 독립적으로 재사용하기 어렵다.
- 테스트 어려움: 테스트 시 의존성을 모두 설정해야 하므로 단위 테스트 작성이 힘들어짐.
해결책
- 의존성 주입(Dependency Injection): 필요한 객체를 직접 생성하지 않고 외부에서 주입받아 의존성을 낮춤.
- 인터페이스 사용: 구체적인 클래스 대신 추상화된 인터페이스를 사용하여 유연성을 높임.
- 팩토리 패턴 사용: 객체 생성 로직을 캡슐화하여 의존성을 줄임.
- 서비스 로케이터 패턴: 서비스의 인스턴스를 중앙에서 관리하고 제공.
예제 코드
# **문제점**
# `UserService`가 `DatabaseService`를 직접 생성하여 강하게 결합됨.
# `DatabaseService`가 변경되면 `UserService`도 수정해야 함.
# 테스트 시 `DatabaseService`를 Mock으로 교체하기 어려움.
class DatabaseService:
def connect(self):
print("Connecting to database...")
class UserService:
def __init__(self):
self.db_service = DatabaseService() # UserService가 DatabaseService에 강하게 결합
def get_user(self, user_id):
self.db_service.connect()
print(f"Fetching user with ID {user_id}")
# After: 의존성 주입을 사용하여 결합도 낮추기
# 1. **유지보수성 향상**: `DatabaseService`를 수정해도 `UserService`는 영향을 받지 않음.
# 2. **테스트 가능성 향상**: Mock 객체를 사용하여 의존성을 쉽게 대체 가능.
# 3. **재사용성 증가**: `UserService`가 특정 구현에 의존하지 않아 다양한 데이터베이스 서비스와 함께 재사용 가능.
# 4. **유연성**: 런타임에 적합한 구현체를 주입받아 사용할 수 있음.
class DatabaseService:
def connect(self):
print("Connecting to database...")
class MockDatabaseService:
def connect(self):
print("Mock database connection for testing.")
class UserService:
def __init__(self, db_service): # 의존성 주입
self.db_service = db_service
def get_user(self, user_id):
self.db_service.connect()
print(f"Fetching user with ID {user_id}")
# 실제 서비스에서 사용
= DatabaseService()
real_db_service = UserService(real_db_service)
user_service 42)
user_service.get_user(
# 테스트에서 Mock 서비스 사용
= MockDatabaseService()
mock_db_service = UserService(mock_db_service)
test_user_service 42) test_user_service.get_user(
요약
과도한 의존성은 코드의 유연성과 확장성을 크게 저해할 수 있습니다. 이를 방지하려면 의존성 주입과 추상화를 적극적으로 활용하여 결합도를 낮추고, 각 구성 요소를 독립적으로 관리할 수 있도록 설계해야 합니다
코드 스멜 제거의 이점
- 코드 가독성 향상
- 간결하고 명확한 코드는 팀원 간 협업을 수월하게 합니다.
- 유지보수성 증가
- 중복된 로직이 줄어들어 수정이 간편해지고, 버그 발생 가능성이 감소합니다.
- 성능 최적화 가능
- 간결한 코드는 실행 효율성을 높이고, 필요 시 성능 개선이 쉬워집니다.
- 테스트 용이성 증가
- 단순화된 코드는 유닛 테스트 작성이 훨씬 간단합니다.
결론
코드 스멜은 깨끗한 코드를 방해하는 신호이며, 이를 무시하면 나중에 큰 기술 부채로 이어질 수 있습니다. 스멜을 감지하고 적절히 리팩토링하면 코드 품질을 유지하면서 생산성을 높일 수 있습니다.
오늘부터 여러분의 코드에서 스멜을 찾아내고, 이를 제거하는 연습을 시작해 보세요! 깨끗한 코드를 작성하는 습관은 훌륭한 개발자로 성장하는 데 큰 도움이 될 것입니다.