함수란? 입력과 반환값으로 작업을 재사용하는 방법

함수란? 입력과 반환값으로 작업을 재사용하는 방법#

함수는 하나의 작업에 이름을 붙여 필요할 때 호출할 수 있게 만든 코드 단위입니다. 함수는 작업에 필요한 값을 입력으로 받고, 처리한 결과를 호출한 곳에 돌려줄 수 있습니다.

예를 들어 상품 가격과 수량으로 주문 금액을 계산하는 코드가 여러 곳에 필요하다고 해보겠습니다.

notebook_total = 3_000 * 2
keyboard_total = 25_000 * 1
pen_total = 1_000 * 3

곱셈 자체는 간단합니다. 하지만 나중에 “수량은 1 이상이어야 한다”라는 규칙이 추가되면 모든 계산 위치에서 이를 확인해야 합니다. 상품 금액 계산을 함수로 만들면 계산 방법과 규칙을 한곳에 모을 수 있습니다.

def calculate_item_total(price, quantity):
    if quantity < 1:
        raise ValueError("수량은 1 이상이어야 합니다.")

    return price * quantity
notebook_total = calculate_item_total(3_000, 2)
keyboard_total = calculate_item_total(25_000, 1)

print(notebook_total)  # 6000
print(keyboard_total)  # 25000

이 글에서는 함수의 정의와 호출에서 시작해 매개변수, 인자, 반환값, 변수의 범위, 함수 분리 기준까지 살펴봅니다. 코드는 Python으로 보여주지만 함수의 핵심 개념은 여러 프로그래밍 언어에 공통으로 적용됩니다.

함수가 없으면 어떤 문제가 생길까?#

사용자의 이름을 받아 인사말을 만드는 코드가 여러 곳에 있다고 해보겠습니다.

user_name = "김철수"
message = f"안녕하세요, {user_name}님!"
print(message)

manager_name = "이영희"
manager_message = f"안녕하세요, {manager_name}님!"
print(manager_message)

두 코드는 이름만 다르고 나머지 작업은 같습니다. 인사말을 “환영합니다”로 바꾸려면 두 곳 모두 수정해야 합니다. 같은 형식이 여러 화면과 파일에 복사되어 있다면 빠뜨릴 가능성이 생깁니다.

함수로 묶으면 작업의 이름을 정하고 바뀌는 값만 전달할 수 있습니다.

def create_greeting(name):
    return f"안녕하세요, {name}님!"
print(create_greeting("김철수"))
print(create_greeting("이영희"))

실행 결과:

안녕하세요, 김철수님!
안녕하세요, 이영희님!

함수를 사용하면 다음 일을 할 수 있습니다.

  • 반복되는 작업을 한곳에 정의합니다.
  • 값이 다른 경우에도 같은 처리 규칙을 적용합니다.
  • 긴 작업을 이름 있는 단계로 나누어 읽기 쉽게 만듭니다.
  • 작업의 결과를 다른 계산에 연결합니다.

다만 한 줄짜리 코드를 모두 함수로 감쌀 필요는 없습니다. 함수가 무슨 작업을 하는지 이름으로 설명할 가치가 있고, 다른 곳에서 호출하거나 따로 확인해야 할 때 만드는 편이 좋습니다.

함수는 어떻게 정의하고 호출할까?#

Python에서는 def로 함수를 정의합니다.

def say_hello():
    print("안녕하세요!")

이 코드는 say_hello라는 함수를 정의합니다. 정의한 시점에 함수 내부의 print()가 곧바로 실행되는 것은 아닙니다.

실행하려면 함수 이름 뒤에 괄호를 붙여 호출해야 합니다.

say_hello()

실행 결과:

안녕하세요!

두 동작을 구분해 보세요.

코드 의미
def say_hello(): ... 나중에 사용할 함수를 정의
say_hello() 함수를 지금 호출하여 실행
say_hello 함수 자체를 가리킴

마지막의 say_hello는 괄호가 없으므로 함수를 호출하지 않습니다. 함수를 값처럼 다른 곳에 전달할 때 이 형태를 사용할 수 있으며, 람다식과 고차 함수를 배울 때 다시 만나게 됩니다.

함수 이름은 작업을 드러내야 한다#

def work(x):
    return x * 2

이 함수가 가격을 계산하는지, 점수를 바꾸는지 이름만으로 알기 어렵습니다.

def double_score(score):
    return score * 2

double_score는 “점수를 두 배로 만든다”는 의도를 보여줍니다. 함수 이름은 호출하는 사람이 내부 코드를 열어보지 않고도 무엇을 기대할 수 있는지 알려주는 약속입니다.

매개변수와 인자: 함수에 값을 전달하는 방법#

함수를 정의할 때 입력을 받아 사용할 이름을 매개변수라고 합니다.

def create_greeting(name):
    return f"안녕하세요, {name}님!"

여기서 name이 매개변수입니다.

함수를 호출하면서 전달하는 실제 값을 인자라고 합니다.

message = create_greeting("김철수")

여기서 "김철수"가 인자입니다. 함수가 호출되면 그 값이 name에 연결되고, 함수 내부에서 사용됩니다.

시점 코드 역할
함수를 정의할 때 def create_greeting(name): name은 매개변수
함수를 호출할 때 create_greeting("김철수") "김철수"는 인자

처음에는 둘 다 “함수에 넣는 값”으로 느껴질 수 있습니다. 매개변수는 함수 쪽에서 정한 입력 이름, 인자는 호출하는 쪽에서 전달한 값이라고 구분하면 됩니다.

매개변수가 여러 개라면?#

def calculate_item_total(price, quantity):
    return price * quantity
total = calculate_item_total(3_000, 2)

print(total)  # 6000

첫 번째 인자 3_000이 price에, 두 번째 인자 2가 quantity에 전달됩니다.

순서를 바꾸면 결과나 의미가 달라질 수 있습니다. 곱셈은 두 값을 바꿔도 숫자 결과가 같지만, 모든 함수가 그런 것은 아닙니다.

def subtract(a, b):
    return a - b


print(subtract(10, 3))  # 7
print(subtract(3, 10))  # -7

키워드 인자로 의미를 드러내기#

Python에서는 매개변수 이름을 지정해 호출할 수 있습니다.

total = calculate_item_total(price=3_000, quantity=2)

print(total)  # 6000

인자가 여러 개라면 이렇게 작성했을 때 각 값의 의미가 분명해집니다.

def create_user(name, age, is_active):
    print(name, age, is_active)


create_user(name="김철수", age=28, is_active=True)

단, 함수의 매개변수 이름을 변경하면 해당 이름으로 호출한 코드도 영향을 받습니다. 공개적으로 사용하는 함수라면 매개변수 이름도 사용 방법의 일부로 생각해야 합니다.

반환값: 계산한 결과를 호출한 곳으로 돌려주기#

함수 안에서 return을 사용하면 결과를 호출한 곳으로 돌려줄 수 있습니다.

def add(a, b):
    return a + b


result = add(10, 20)

print(result)  # 30

실행 흐름은 다음과 같습니다.

  1. add(10, 20)을 호출합니다.
  2. a는 10, b는 20을 가리킵니다.
  3. a + b를 계산해 30을 얻습니다.
  4. return이 30을 호출한 곳으로 돌려줍니다.
  5. 돌려받은 30을 result에 할당합니다.

반환값은 다시 다른 계산에 사용할 수 있습니다.

first_total = add(10, 20)
final_total = add(first_total, 5)

print(final_total)  # 35

이처럼 반환값이 있으면 한 함수의 결과를 다음 작업의 입력으로 연결할 수 있습니다.

return을 만나면 함수 실행이 끝난다#

def classify_score(score):
    if score >= 60:
        return "통과"

    return "재학습"
print(classify_score(85))  # 통과
print(classify_score(40))  # 재학습

85점을 전달하면 첫 번째 return에서 함수가 끝납니다. 그 아래의 "재학습" 반환문은 실행되지 않습니다.

이 특징을 이용하면 특별한 경우를 먼저 처리할 수도 있습니다.

def calculate_average(scores):
    if not scores:
        return None

    return sum(scores) / len(scores)

목록이 비어 있다면 None을 반환하고 함수가 끝납니다. 그렇지 않을 때만 평균 계산으로 내려갑니다.

print()와 return은 어떻게 다를까?#

처음 함수를 배울 때 가장 많이 혼동하는 부분입니다.

  • print()는 내용을 출력합니다.
  • return은 값을 함수를 호출한 곳에 돌려줍니다.

다음 두 함수를 비교해 보세요.

def show_total(price, quantity):
    print(price * quantity)


def calculate_total(price, quantity):
    return price * quantity
printed_result = show_total(3_000, 2)
returned_result = calculate_total(3_000, 2)

print(printed_result)   # None
print(returned_result)  # 6000

show_total()을 호출할 때 화면에 6000이 출력됩니다. 하지만 함수가 값을 반환하지 않았으므로 printed_result에는 None이 들어갑니다.

반면 calculate_total()은 6000을 반환하므로 다른 계산에 사용할 수 있습니다.

subtotal = calculate_total(3_000, 2)
shipping_fee = 2_500

final_price = subtotal + shipping_fee

print(final_price)  # 8500

계산 결과를 여러 곳에서 사용해야 한다면 함수가 결과를 반환하고, 화면에 보여주는 작업은 호출하는 곳에서 맡는 편이 유연한 경우가 많습니다.

subtotal = calculate_total(3_000, 2)

print(f"상품 금액: {subtotal}원")

return을 쓰지 않으면 무엇을 반환할까?#

Python 함수가 return 없이 끝나면 None을 반환합니다.

def greet(name):
    print(f"안녕하세요, {name}님!")


result = greet("이영희")

print(result)  # None

None은 “반환할 결과값을 별도로 제공하지 않는다”는 상황에서 나타날 수 있는 값입니다. 화면에 이미 글자가 출력되었다고 해서 그 문자열이 함수의 반환값이 되는 것은 아닙니다.

기본값이 있는 매개변수#

인자를 생략했을 때 사용할 기본값을 지정할 수 있습니다.

def create_greeting(name, greeting="안녕하세요"):
    return f"{greeting}, {name}님!"
print(create_greeting("김철수"))
# 안녕하세요, 김철수님!

print(create_greeting("이영희", "반갑습니다"))
# 반갑습니다, 이영희님!

greeting을 전달하지 않으면 "안녕하세요"를 사용하고, 전달하면 그 값을 사용합니다.

기본값은 선택적으로 바꿀 설정이 있을 때 유용합니다. 하지만 선택 항목을 너무 많이 늘리면 함수 호출 방법이 복잡해질 수 있습니다. 자주 함께 바뀌는 설정이 많아졌다면 함수의 역할이나 입력 구조를 다시 살펴봐야 합니다.

변경 가능한 값을 기본값으로 둘 때 주의하기#

다음 코드는 의도와 다른 결과가 나올 수 있습니다.

def add_item(item, cart=[]):
    cart.append(item)
    return cart


print(add_item("사과"))  # ['사과']
print(add_item("우유"))  # ['사과', '우유']

함수를 두 번 호출했는데 두 번째 결과에도 첫 번째 호출의 "사과"가 남습니다. 기본값으로 만든 리스트가 호출마다 새로 생성되는 것이 아니라, 함수 정의 시 준비된 같은 리스트 객체가 이후 호출에도 사용되기 때문입니다.

호출할 때마다 독립된 목록이 필요하다면 None을 기본값으로 사용합니다.

def add_item(item, cart=None):
    if cart is None:
        cart = []

    cart.append(item)
    return cart


print(add_item("사과"))  # ['사과']
print(add_item("우유"))  # ['우유']

함수에 리스트나 딕셔너리를 기본값으로 지정할 때는 이 동작을 반드시 확인해야 합니다.

함수 안의 변수는 어디까지 사용할 수 있을까?#

함수 안에서 만든 변수는 보통 그 함수의 지역 변수입니다.

def calculate_total(price, quantity):
    total = price * quantity
    return total


result = calculate_total(3_000, 2)

print(result)  # 6000

# print(total)
# NameError: 여기에는 total이라는 이름이 없다.

total은 calculate_total() 안에서 사용한 이름입니다. 바깥에서는 함수가 반환한 값을 result라는 이름으로 받습니다.

지역 변수를 사용하면 다른 함수의 내부 변수와 이름이 같아도 서로 간섭하지 않습니다.

def calculate_price():
    total = 10_000
    return total


def calculate_points():
    total = 500
    return total

두 함수가 모두 total이라는 지역 변수 이름을 사용해도 각각의 함수 안에서만 그 이름을 사용합니다.

전역 변수를 직접 바꾸는 방식은 왜 조심해야 할까?#

total = 0


def add_price(price):
    global total
    total += price

add_price()는 값을 반환하는 대신 함수 바깥의 total을 변경합니다. 프로그램의 여러 곳에서 이 값을 바꾸기 시작하면 어떤 호출이 최종 결과를 만들었는지 추적하기 어려워집니다.

입력을 받아 결과를 반환하면 데이터의 흐름이 더 분명합니다.

def add_price(current_total, price):
    return current_total + price


total = 0
total = add_price(total, 3_000)
total = add_price(total, 5_000)

print(total)  # 8000

모든 전역 변수 사용이 금지된다는 뜻은 아닙니다. 초보 단계에서는 함수에 필요한 값을 인자로 전달하고 결과를 반환하는 방식을 기본으로 익히면 함수 간 관계를 이해하기 쉽습니다.

리스트를 함수에 전달하면 어떤 일이 일어날까?#

함수에 리스트를 전달할 때도 변수와 객체의 관계를 알아야 합니다. 함수가 전달받은 리스트 자체를 수정하면 호출한 쪽에서도 그 변경을 볼 수 있습니다.

def add_product(cart, product):
    cart.append(product)


shopping_cart = ["사과"]
add_product(shopping_cart, "우유")

print(shopping_cart)
# ['사과', '우유']

add_product()는 새 리스트를 반환하지 않았지만, 기존 리스트에 항목을 추가했습니다. 함수의 반환값과 별개로 입력받은 변경 가능한 객체의 상태가 바뀐 것입니다.

이런 함수가 언제나 잘못된 것은 아닙니다. 장바구니를 직접 변경하는 동작이 목적이라면 자연스럽습니다. 다만 호출하는 사람이 입력 목록이 변경된다는 사실을 알아야 합니다.

원본을 유지하고 새 목록을 반환하고 싶다면 다음처럼 작성할 수 있습니다.

def with_added_product(cart, product):
    new_cart = cart.copy()
    new_cart.append(product)
    return new_cart


original_cart = ["사과"]
updated_cart = with_added_product(original_cart, "우유")

print(original_cart)  # ['사과']
print(updated_cart)   # ['사과', '우유']

이 예제는 문자열 목록을 다루므로 얕은 복사인 .copy()로 목적을 달성합니다. 리스트 안에 변경 가능한 딕셔너리나 리스트가 들어 있다면 안쪽 객체까지 독립적으로 복사된 것은 아닙니다.

함수를 만들 때는 다음 중 어느 동작을 의도하는지 정해야 합니다.

  • 기존 객체를 변경한다.
  • 기존 객체는 유지하고 새 결과를 반환한다.

이 선택은 이후 객체 지향과 함수형 프로그래밍의 차이를 이해할 때도 중요합니다.

함수를 어느 크기로 나누면 좋을까?#

“함수는 반드시 짧아야 한다”는 길이 규칙만으로는 좋은 설계를 판단하기 어렵습니다. 한 함수가 설명할 수 있는 하나의 역할을 맡고 있는지 살펴보세요.

주문 처리 코드를 예로 들어 보겠습니다.

def calculate_subtotal(items):
    total = 0

    for item in items:
        total += item["price"] * item["quantity"]

    return total


def calculate_discount(subtotal):
    if subtotal >= 30_000:
        return subtotal * 10 // 100

    return 0


def calculate_final_price(items):
    subtotal = calculate_subtotal(items)
    discount = calculate_discount(subtotal)

    return subtotal - discount

이 코드는 세 단계로 읽힙니다.

  1. 상품의 금액을 합산합니다.
  2. 합계에 따라 할인액을 계산합니다.
  3. 합계에서 할인액을 뺍니다.
items = [
    {"name": "노트", "price": 3_000, "quantity": 2},
    {"name": "키보드", "price": 25_000, "quantity": 1},
]

print(calculate_final_price(items))  # 27900

상품 합계는 31,000원이고 10% 할인액은 3,100원이므로 결과는 27,900원입니다.

함수를 나누면 할인 기준이 바뀌었을 때 calculate_discount()를 중심으로 살펴볼 수 있습니다. 하지만 모든 계산을 의미 없는 한 줄짜리 함수로 잘게 쪼개면 오히려 여러 곳을 이동하며 읽어야 합니다.

함수를 만들 때 확인할 세 가지#

  1. 이름: 함수 이름만 보고 무엇을 하는지 알 수 있는가?
  2. 입력: 작업에 필요한 값이 인자로 분명히 전달되는가?
  3. 출력: 결과를 반환하는지, 기존 데이터를 변경하는지 알 수 있는가?

이 세 가지가 분명하면 함수를 호출하는 사람은 내부 구현을 모두 읽지 않아도 사용할 수 있습니다.

실무에서 자주 하는 실수#

함수 이름과 실제 동작이 다르다#

def calculate_total(items):
    print(sum(items))

calculate_total이라는 이름은 합계 값을 받을 수 있을 것처럼 보이지만 실제로는 출력만 합니다.

result = calculate_total([1, 2, 3])

print(result)  # None

계산한 값을 사용하려면 반환해야 합니다.

def calculate_total(items):
    return sum(items)

모든 상황에서 사용할 수 있다고 가정한다#

def calculate_average(scores):
    return sum(scores) / len(scores)

빈 목록을 전달하면 0으로 나눕니다. 먼저 빈 목록의 의미와 처리 방법을 정해야 합니다.

def calculate_average(scores):
    if not scores:
        return None

    return sum(scores) / len(scores)

None을 반환하기로 했다면 호출한 쪽도 이를 처리해야 합니다.

average = calculate_average([])

if average is None:
    print("평균을 계산할 점수가 없습니다.")
else:
    print(f"평균: {average:.1f}점")

호출하는 코드가 함수 내부의 변수에 의존한다#

함수 안에서 discount라는 변수를 썼다고 해서 바깥에서 그 이름을 그대로 사용할 수 있는 것은 아닙니다. 필요한 결과는 return으로 전달받아야 합니다.

def calculate_discount(subtotal):
    discount = subtotal * 10 // 100
    return discount


discount_amount = calculate_discount(30_000)

print(discount_amount)  # 3000

한 함수가 너무 많은 일을 한다#

하나의 함수가 사용자 입력, 가격 계산, 파일 저장, 화면 출력까지 전부 맡으면 어느 부분이 실패했는지 조사하기 어렵습니다. 계산 자체를 별도 함수로 분리하면 입력·저장 없이도 계산 규칙을 확인할 수 있습니다.

def calculate_item_total(price, quantity):
    return price * quantity

이 함수는 입력한 두 값만으로 결과를 확인할 수 있습니다.

직접 확인해 보기#

다음 코드가 어떤 값을 출력할지 먼저 예상해 보세요.

def calculate_discounted_price(price, discount_percent):
    discount = price * discount_percent // 100
    return price - discount


original_price = 20_000
sale_price = calculate_discounted_price(original_price, 15)

print(original_price)
print(sale_price)

실행 결과:

20000
17000

함수는 원래 가격 20,000원과 할인율 15를 입력으로 받습니다. 할인 금액 3,000원을 계산해 뺀 결과인 17,000원을 반환합니다. 함수 안에서 original_price를 바꾸지 않았으므로 원래 변수의 값은 그대로입니다.

다음 연습도 해보세요.

  1. 할인율이 0이면 어떤 값을 반환할까요?
  2. 할인율이 100이면 어떤 값을 반환할까요?
  3. 할인율이 150이면 무엇이 문제일까요?
  4. 이 함수가 허용할 할인율의 범위를 어디에서 검사해야 할까요?

특히 3번은 코드가 오류 없이 실행되더라도 업무적으로 잘못된 값이 나올 수 있다는 점을 보여줍니다. 입력의 의미와 규칙까지 생각하는 것이 함수 설계의 일부입니다.

정리#

함수는 작업에 이름을 붙여 호출하고, 입력을 받아 결과를 반환할 수 있게 하는 코드 단위입니다.

  • 함수를 정의할 때 적는 입력 이름은 매개변수입니다.
  • 함수를 호출하며 전달하는 실제 값은 인자입니다.
  • return은 결과를 호출한 곳에 돌려주고 함수 실행을 끝냅니다.
  • print()는 내용을 출력하지만 자동으로 반환값이 되지는 않습니다.
  • 함수 안의 지역 변수는 바깥에서 직접 사용할 수 없습니다.
  • 리스트처럼 변경 가능한 객체를 전달했다면 함수가 원본을 수정하는지 확인해야 합니다.

좋은 함수를 만들기 위한 출발점은 “코드를 몇 줄 줄일 수 있나?”가 아닙니다. 이 작업은 무엇을 입력받고, 무엇을 책임지며, 호출한 곳에 어떤 결과를 보장하는가? 이 질문에 분명하게 답하는 것입니다.