본문 바로가기
카테고리 없음

제발 파이썬 함수 이렇게 쓰지마세요: 코드 품질을 높이는 7가지 핵심 팁

2025. 3. 19.
반응형

파이썬은 읽기 쉽고 간결한 코드 작성을 지향하는 프로그래밍 언어입니다. 하지만 많은 개발자들이 파이썬의 함수를 작성할 때 몇 가지 흔한 실수를 범하곤 합니다. 이런 실수들은 코드의 가독성을 떨어뜨리고, 유지보수를 어렵게 만들며, 때로는 예상치 못한 버그를 발생시킵니다.

이 글에서는 파이썬 함수 작성 시 절대 하지 말아야 할 실수들과 함께 더 나은 코드를 작성하기 위한 핵심 팁을 알아보겠습니다. 경험 많은 개발자들도 종종 놓치는 이런 팁들을 통해 여러분의 코드 품질을 한 단계 업그레이드해 보세요!

들어가며: 함수가 중요한 이유

함수는 모든 프로그래밍 언어의 핵심 구성 요소입니다. 특히 파이썬에서 함수는 코드를 구조화하고, 재사용성을 높이며, 복잡한 로직을 추상화하는 강력한 도구입니다. 좋은 함수는 전체 프로그램의 품질을 결정짓는 가장 중요한 요소 중 하나라고 해도 과언이 아닙니다.

하지만 많은 개발자들이 함수를 작성할 때 실수를 범하곤 합니다. 이런 실수들은 코드의 가독성, 유지보수성, 그리고 성능에 부정적인 영향을 미칩니다. 지금부터 이런 실수들을 피하고 더 나은 함수를 작성하는 방법을 알아보겠습니다.

여러분은 자신이 작성한 함수를 6개월 후에 다시 보았을 때, 쉽게 이해할 수 있나요? 다른 개발자가 여러분의 함수를 보았을 때 별다른 설명 없이도 바로 이해할 수 있을까요?

팁 1: 과도하게 긴 함수 피하기

문제점

파이썬 초보자부터 중급자까지 가장 흔히 저지르는 실수 중 하나는 너무 많은 일을 하는 함수를 작성하는 것입니다. 수백 줄에 달하는 함수는:

  • 코드를 이해하기 어렵게 만듭니다
  • 디버깅이 복잡해집니다
  • 테스트하기 어려워집니다
  • 코드 재사용성이 떨어집니다
def process_user_data(user_list):
    """너무 많은 일을 하는 함수의 예"""
    valid_users = []
    invalid_emails = []
    
    # 사용자 데이터 검증 (50줄)
    for user in user_list:
        if '@' not in user['email']:
            invalid_emails.append(user['email'])
            continue
        # 더 많은 검증 로직...
        
    # 데이터베이스 저장 (30줄)
    db_connection = connect_to_database()
    # 복잡한 DB 처리 로직...
    
    # 알림 이메일 발송 (40줄)
    for user in valid_users:
        # 이메일 발송 로직...
        
    # 로그 기록 (20줄)
    # 로깅 로직...
    
    return valid_users

위 함수는 검증, 저장, 알림, 로깅이라는 네 가지의 서로 다른 책임을 가지고 있습니다. 이는 단일 책임 원칙(Single Responsibility Principle)을 위반하는 전형적인 예입니다.

개선 방법

함수는 한 가지 일만 하도록 설계해야 합니다. 이를 위한의 가이드라인:

  • 함수의 길이를 20-30줄 이내로 유지하세요
  • 함수가 하나의 명확한 작업만 수행하도록 하세요
  • 복잡한 함수는 더 작은 함수들로 분리하세요
def validate_user_email(user):
    """사용자 이메일을 검증하는 함수"""
    return '@' in user['email']

def filter_valid_users(user_list):
    """유효한 사용자만 필터링하는 함수"""
    return [user for user in user_list if validate_user_email(user)]

def save_users_to_database(valid_users):
    """사용자 정보를 데이터베이스에 저장하는 함수"""
    db_connection = connect_to_database()
    # DB 저장 로직...
    return True

def send_notification_emails(valid_users):
    """사용자에게 알림 이메일을 보내는 함수"""
    for user in valid_users:
        # 이메일 발송 로직...
    return True

def process_user_data(user_list):
    """메인 함수 - 작은 함수들을 조합"""
    valid_users = filter_valid_users(user_list)
    save_users_to_database(valid_users)
    send_notification_emails(valid_users)
    return valid_users

이렇게 작은 함수들로 분리하면:

  • 각 함수의 목적이 명확해집니다
  • 테스트가 용이해집니다
  • 코드 재사용성이 높아집니다
  • 유지보수가 쉬워집니다

실무 팁: 함수 이름만 읽고도 그 함수가 무슨 일을 하는지 알 수 있어야 합니다. 주석을 읽어야만 함수의 목적을 이해할 수 있다면, 함수 이름이 명확하지 않거나 함수가 너무 많은 일을 하고 있는 것입니다.

팁 2: 명확한 함수 이름 사용하기

나쁜 이름의 특징

함수 이름은 그 함수의 역할과 목적을 명확히 드러내야 합니다. 하지만 많은 개발자들이 이런 점을 간과하고 의미가 모호하거나 너무 일반적인 이름을 사용합니다.

# 나쁜 함수 이름 예시
def process(data):
    # 무엇을 처리하는지 알 수 없음
    pass

def handle_stuff(items):
    # 너무 모호함
    pass

def do_it():
    # 전혀 정보를 제공하지 않음
    pass

def get_data():
    # 어떤 데이터를 가져오는지 알 수 없음
    pass

좋은 이름 짓기

좋은 함수 이름은 동사로 시작하고 함수의 목적이나 반환값을 명확히 나타냅니다. 또한 이름이 너무 길지 않으면서도 충분한 정보를 제공해야 합니다.

# 좋은 함수 이름 예시
def calculate_average_score(scores):
    # 점수들의 평균을 계산한다는 것이 명확함
    return sum(scores) / len(scores)

def validate_email_format(email):
    # 이메일 형식을 검증한다는 것이 명확함
    return '@' in email and '.' in email.split('@')[1]

def fetch_active_users_by_region(region):
    # 특정 지역의 활성 사용자를 가져온다는 것이 명확함
    pass

def convert_celsius_to_fahrenheit(celsius):
    # 섭씨를 화씨로 변환한다는 것이 명확함
    return (celsius * 9/5) + 32

여러분이 작성한 함수의 이름을 살펴보세요. 그 이름만 보고도 함수가 무슨 일을 하는지, 어떤 값을 반환하는지 알 수 있나요?

함수 이름을 더 명확하게 만드는 팁:

  • 동사로 시작: 함수는 행동을 나타내므로 동사로 시작하는 것이 좋습니다
  • 구체적으로: 'process_data' 대신 'validate_user_input' 같은 구체적인 이름을 사용하세요
  • 일관성: 프로젝트 전체에서 일관된 명명 규칙을 사용하세요
  • 약어 피하기: 'calc_avg' 보다는 'calculate_average' 같은 완전한 단어를 사용하세요

팁 3: 매개변수 남용 자제하기

매개변수가 많을 때의 문제

함수에 너무 많은 매개변수를 사용하면 여러 문제가 발생합니다:

  • 함수 호출 시 실수하기 쉬워집니다
  • 함수의 의도가 불명확해집니다
  • 테스트해야 할 경우의 수가 기하급수적으로 증가합니다
  • 매개변수 순서를 기억하기 어려워집니다
# 너무 많은 매개변수를 가진 함수
def create_user(first_name, last_name, email, password, 
               birth_date, address, city, country, 
               postal_code, phone, is_active=True, 
               notification_pref=False, language='ko'):
    # 복잡한 사용자 생성 로직...
    pass

# 호출 시 매우 복잡하고 실수하기 쉬움
create_user('홍', '길동', 'hong@example.com', 'pass123', 
           '1990-01-01', '서울시 강남구', '서울', '대한민국', 
           '12345', '010-1234-5678', True, True, 'ko')

개선 전략

매개변수가 많은 함수를 개선하는 방법:

  1. 관련 매개변수를 클래스나 데이터 클래스로 그룹화
  2. 키워드 인자(keyword arguments) 사용 권장
  3. 설정값을 딕셔너리로 전달
# 방법 1: 클래스 사용
class UserData:
    def __init__(self, first_name, last_name, email, password, 
                birth_date, address, city, country, 
                postal_code, phone, is_active=True, 
                notification_pref=False, language='ko'):
        self.first_name = first_name
        # 나머지 필드 초기화...

def create_user(user_data):
    # user_data 객체를 사용하여 사용자 생성
    pass

# 방법 2: 딕셔너리 사용
def create_user(user_info):
    # user_info 딕셔너리에서 필요한 정보 추출
    first_name = user_info.get('first_name')
    # 나머지 필드 추출...
    
# 방법 3: 키워드 인자와 더 작은 함수들로 분리
def create_user(first_name, last_name, email, password, **additional_info):
    # 필수 정보로 기본 사용자 생성
    user = {'first_name': first_name, 'last_name': last_name, 
            'email': email, 'password': hash_password(password)}
    
    # 추가 정보 병합
    user.update(additional_info)
    return user

실무 팁: 파이썬 3.7 이상에서는 dataclasses 모듈을 사용하여 코드를 더 간결하게 만들 수 있습니다. 이는 반복적인 보일러플레이트 코드를 줄이는 데 도움이 됩니다.

팁 4: 기본 매개변수 함정 피하기

가변 객체를 기본값으로 사용할 때의 위험성

파이썬에서 흔히 발생하는 실수 중 하나는 리스트, 딕셔너리, 집합과 같은 가변 객체를 함수의 기본 매개변수로 사용하는 것입니다. 이는 예상치 못한 버그를 발생시킬 수 있습니다.

# 위험한 코드 - 가변 객체를 기본값으로 사용
def add_user(name, user_list=[]):
    user_list.append(name)
    return user_list

# 첫 번째 호출
result1 = add_user("홍길동")
print(result1)  # ['홍길동']

# 두 번째 호출
result2 = add_user("김철수")
print(result2)  # ['홍길동', '김철수'] - 예상과 다름!

# 기본값이 공유됨을 확인
print(result1)  # ['홍길동', '김철수'] - 첫 번째 결과도 변함!

이 코드의 문제점은 기본 매개변수가 함수 정의 시점에 한 번만 평가된다는 것입니다. 따라서 모든 함수 호출이 동일한 리스트 객체를 공유하게 됩니다.

안전한 기본값 설정 방법

이 문제를 해결하는 두 가지 일반적인 방법이 있습니다:

# 방법 1: None을 기본값으로 사용
def add_user(name, user_list=None):
    if user_list is None:
        user_list = []  # 함수 호출마다 새로운 리스트 생성
    user_list.append(name)
    return user_list

# 방법 2: 불변 객체를 기본값으로 사용하고 함수 내에서 변환
def add_user(name, user_tuple=()):
    user_list = list(user_tuple)  # 튜플을 리스트로 변환
    user_list.append(name)
    return user_list

주의: 이 문제는 리스트([]), 딕셔너리({}), 집합(set()) 등 모든 가변 객체에 적용됩니다. 기본값으로는 항상 None이나 불변 객체(숫자, 문자열, 튜플, frozenset 등)를 사용하세요.

팁 5: 문서화 습관 들이기

효과적인 문서화

파이썬에서 함수를 문서화하는 가장 일반적인 방법은 독스트링(docstring)을 사용하는 것입니다. 좋은 독스트링은 함수의 목적, 매개변수, 반환값, 예외 등을 명확하게 설명합니다.

많은 개발자들이 문서화를 번거롭게 여기거나 불필요하다고 생각하지만, 좋은 문서화는 코드 자체만큼이나 중요합니다. 특히 팀 프로젝트나 오픈 소스 프로젝트에서는 더욱 그렇습니다.

문서화 팁과 예시

def calculate_discount(price, discount_rate, max_discount=100):
    """
    상품 가격에 할인율을 적용하여 최종 가격을 계산합니다.
    
    Args:
        price (float): 원래 상품 가격
        discount_rate (float): 적용할 할인율 (0.0 ~ 1.0 사이의 값)
        max_discount (float, optional): 최대 할인 금액. 기본값은 100.
        
    Returns:
        float: 할인이 적용된 최종 가격
        
    Raises:
        ValueError: discount_rate가 0보다 작거나 1보다 큰 경우
        ValueError: price가 음수인 경우
        
    Examples:
        >>> calculate_discount(100, 0.1)
        90.0
        >>> calculate_discount(200, 0.5, max_discount=50)
        150.0
    """
    if not 0 <= discount_rate <= 1:
        raise ValueError("할인율은 0과 1 사이의 값이어야 합니다")
    if price < 0:
        raise ValueError("가격은 음수가 될 수 없습니다")
        
    discount_amount = price * discount_rate
    discount_amount = min(discount_amount, max_discount)
    
    return price - discount_amount

좋은 독스트링의 특징:

  • 간결하면서도 명확한 설명: 함수가 무엇을 하는지 명확히 설명
  • 매개변수 설명: 각 매개변수의 이름, 타입, 목적을 설명
  • 반환값 설명: 함수가 반환하는 값의 타입과 의미
  • 예외 설명: 함수가 발생시킬 수 있는 예외 상황
  • 예시 코드: 함수 사용 방법을 보여주는 간단한 예시

실무 팁: PyCharm, VS Code 등의 IDE는 문서화된 함수에 대해 자동 완성 및 힌트 기능을 제공합니다. 이는 개발 생산성을 크게 향상시킵니다. 또한 help() 함수를 사용하면 문서화된 내용을 쉽게 확인할 수 있습니다.

팁 6: 부작용 최소화하기

부작용의 위험성

부작용(side effect)이란 함수가 자신의 스코프 외부의 상태를 변경하는 것을 말합니다. 예를 들어, 전역 변수 수정, 파일 시스템 변경, 데이터베이스 수정 등이 있습니다. 이런 부작용이 많은 함수는:

  • 테스트하기 어렵습니다
  • 예측하기 어렵습니다
  • 재사용성이 떨어집니다
  • 디버깅이 복잡해집니다
total_items = 0  # 전역 변수

def process_order(items):
    global total_items
    result = []
    
    for item in items:
        # 로직 처리
        result.append(item)
        total_items += 1  # 부작용: 전역 변수 수정
        
    # 파일에 로그 작성 - 또 다른 부작용
    with open('order_log.txt', 'a') as f:
        f.write(f"Processed {len(items)} items\n")
        
    return result

순수 함수의 장점

순수 함수(pure function)는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 부작용이 없는 함수를 말합니다. 가능한 한 함수를 순수하게 유지하면 코드의 품질이 크게 향상됩니다.

def process_order(items, stats=None):
    """
    주문 항목을 처리하고 결과를 반환합니다.
    
    Args:
        items (list): 처리할 항목 리스트
        stats (dict, optional): 업데이트할 통계 정보
        
    Returns:
        tuple: (처리된 항목 리스트, 업데이트된 통계 정보)
    """
    result = []
    
    # 통계 정보 초기화
    if stats is None:
        stats = {'total_items': 0}
    
    for item in items:
        # 로직 처리
        result.append(item)
        
    # 통계 정보 업데이트 (부작용 없음 - stats는 함수 매개변수)
    stats['total_items'] += len(items)
    
    return result, stats

# 로깅은 별도 함수로 분리
def log_order_processing(items_count, log_file='order_log.txt'):
    """주문 처리 결과를 로그 파일에 기록합니다."""
    with open(log_file, 'a') as f:
        f.write(f"Processed {items_count} items\n")

이렇게 개선된 코드는:

  • 명시적인 입력과 출력: 모든 데이터는 매개변수로 전달되고 반환값으로 돌려받습니다
  • 책임 분리: 로깅과 같은 부작용은 별도의 함수로 분리되었습니다
  • 테스트 용이성: 각 함수는 독립적으로 테스트하기 쉽습니다

여러분의 코드에서 가장 디버깅하기 어려운 부분은 어디인가요? 혹시 그 부분에 부작용이 많은 함수가 있지는 않나요?

팁 7: 예외 처리 제대로 하기

너무 넓은 예외 처리의 문제

파이썬에서 예외 처리는 중요한 부분이지만, 많은 개발자들이 너무 광범위한 예외를 잡는 실수를 범합니다.

# 나쁜 예외 처리 예시
def get_user_data(user_id):
    try:
        # 데이터베이스 조회
        # 파일 읽기
        # API 호출
        # 여러 가지 작업...
        return user_data
    except Exception as e:  # 너무 광범위한 예외 처리
        # 모든 오류를 동일하게 처리
        print(f"Error: {e}")
        return None

이 코드의 문제점:

  • 모든 유형의 예외를 동일하게 처리합니다
  • 실제 문제를 감추어 디버깅을 어렵게 만듭니다
  • 예상치 못한 오류까지 억제할 수 있습니다

효과적인 예외 처리 방법

좋은 예외 처리 방법:

def get_user_data(user_id):
    try:
        connection = connect_to_database()
        try:
            user_data = fetch_user_from_db(connection, user_id)
            return user_data
        except DatabaseError as e:
            # 데이터베이스 관련 오류만 처리
            logger.error(f"데이터베이스 조회 오류: {e}")
            raise UserDataError(f"사용자 데이터를 가져올 수 없습니다: {e}") from e
        finally:
            # 리소스 정리 보장
            connection.close()
    except ConnectionError as e:
        # 연결 관련 오류 처리
        logger.error(f"데이터베이스 연결 오류: {e}")
        raise UserDataError("데이터베이스에 연결할 수 없습니다") from e

효과적인 예외 처리 원칙:

  • 구체적인 예외 처리: 가능한 한 구체적인 예외 유형을 지정하세요
  • 예외 전환: 저수준 예외를 의미 있는 고수준 예외로 변환하세요
  • 리소스 정리: finally 블록이나 컨텍스트 매니저(with 문)를 사용하여 리소스를 정리하세요
  • 로깅: 예외 정보를 적절히 로깅하세요

실무 팁: except Exception은 마지막 수단으로만 사용하고, 가능하면 항상 더 구체적인 예외 유형을 지정하세요. 또한 raise ... from e 구문을 사용하면 원인 예외를 연결할 수 있어 디버깅에 도움이 됩니다.

결론: 한 단계 더 나아가기

지금까지 파이썬 함수 작성 시 피해야 할 일반적인 실수와 개선 방법에 대해 알아보았습니다. 이러한 팁들을 적용하면 더 나은 코드를 작성할 수 있습니다:

  • 한 가지 일만 하는 작은 함수를 작성하세요
  • 함수 이름을 명확하고 의미 있게 지으세요
  • 매개변수를 너무 많이 사용하지 마세요
  • 가변 객체를 기본 매개변수로 사용하지 마세요
  • 독스트링으로 함수를 문서화하세요
  • 부작용을 최소화하고 순수 함수를 지향하세요
  • 예외를 구체적으로 처리하세요

이러한 원칙들은 단순히 "좋은 관행"이 아니라, 실제로 코드의 품질, 유지보수성, 가독성을 크게 향상시키는 핵심 요소입니다. 특히 팀 프로젝트나 장기적으로 유지보수해야 하는 코드에서는 더욱 중요합니다.

여러분이 작성한 함수를 다시 한번 살펴보고, 이 글에서 소개한 팁들을 적용해 보세요. 처음에는 약간의 추가 노력이 필요하지만, 시간이 지날수록 이러한 습관이 자연스럽게 몸에 배게 될 것입니다. 결과적으로 여러분은 더 효율적이고 유지보수하기 쉬운 코드를 작성하는 개발자가 될 수 있습니다.

이 글에서 가장 유용하다고 생각되는 팁은 무엇인가요? 다음 코드를 작성할 때 어떤 부분을 개선해 보고 싶나요? 댓글로 여러분의 생각을 공유해 주세요!

반응형

댓글