Devin.KR

함수와 호출 흐름

90분 안팎

학습 목표

생성·조회 함수를 분리하고 각 함수의 입력과 반환값을 설명합니다.

개념

함수 경계로 읽을 범위를 줄입니다

생성부터 조회까지 한 블록에 적으면 어느 값이 다음 단계로 넘어가는지 찾기 어렵습니다. AI가 긴 코드를 주었을 때도 함수 이름만 보고 믿지 않고 입력, 반환값, 변경한 값을 확인합니다. 이번 레슨에서는 유효한 제목을 받는 create_task와 항목 목록을 반환하는 list_tasks를 분리합니다. 제목 검증은 앞 레슨에서 다뤘고 여기서는 함수 호출의 이동을 읽는 연습에 집중합니다.

함수는 def로 정의한 이름 있는 처리 단위입니다. 정의문을 읽었다고 몸통의 모든 줄을 실행하는 것은 아닙니다. create_task(1, 제목)를 호출하면 첫 인자는 task_id에, 둘째 인자는 title에 연결되고 들여쓴 몸통이 실행됩니다. return으로 항목 딕셔너리를 돌려주면 호출한 줄로 돌아옵니다. 함수 이름, 입력 두 개, 출력 한 개를 작은 표로 적으면 호출 관계를 그릴 수 있습니다.

create_task는 id, title, done이라는 세 필드를 가진 새 딕셔너리를 반환합니다. 이번 계약에서는 새 항목의 done이 False입니다. 함수는 제목을 화면에 출력하지 않고 항목을 반환합니다. 출력은 실행기 마지막의 json.dumps와 print에서 담당합니다. 내부 함수에 print만 두면 화면에는 제목이 보여도 호출자는 항목을 받지 못합니다. 그 함수의 반환값은 None이어서 목록에 잘못된 값이 들어갈 수 있습니다.

list_tasks는 목록의 각 항목을 dict(task)로 복사해서 새 리스트로 반환합니다. 대괄호 안의 for task in tasks는 리스트 컴프리헨션입니다. 입력 리스트의 항목을 순서대로 하나씩 읽고 각 항목으로 새 딕셔너리를 만들어 결과에 넣습니다. 익숙하지 않으면 빈 result를 만들고 for 안에서 result.append(dict(task))를 실행한 뒤 return하는 반복문과 대응시켜 읽습니다.

상태를 인자로 전달하고 결과를 받습니다

상태는 현재 가진 모든 할 일 목록입니다. 이번 실습의 initial은 1부터 연속된 번호와 생성 순서로 정렬된 정상 목록입니다. titles는 이미 앞 레슨의 규칙을 만족한 정리된 제목들의 배열입니다. 생성 함수가 제목 형식을 재검사하지 않는 이유는 이 레슨이 호출 경계만 분리한 부분 연습이기 때문입니다. 미션에서는 normalize_title을 다시 연결하여 잘못된 제목도 처리합니다.

다음 번호는 len(state) + 1로 계산합니다. 명세에 삭제가 없고 번호가 1부터 연속이라는 전제가 있으므로 가능한 방식입니다. 삭제가 있는 목록이나 번호 7만 있는 목록에 이 규칙을 가져가면 번호 충돌이 날 수 있습니다. 코드가 실행된다는 사실과 전제가 맞는다는 사실을 따로 확인합니다. 조건이 다른 앱에 함수를 복사할 때는 번호 규칙부터 다시 검토합니다.

state = state + [새 항목]은 기존 항목들과 새 항목을 포함하는 새 리스트에 state라는 이름을 연결합니다. 이전 리스트에 append하는 표현과 달리 그 리스트 자체를 늘리지 않습니다. 그러나 기존 내부 딕셔너리가 저절로 깊이 복사되는 것은 아닙니다. 이번 실행기는 처음에 list_tasks로 항목을 복사하며 이후 기존 항목의 필드를 수정하지 않습니다. 후속 완료 기능에서는 내부 항목 복사도 확인합니다.

지역 변수는 해당 함수 호출 안에서 사용하는 이름입니다. create_task 안의 title과 호출자 쪽의 title은 같은 문자열을 가리킬 수 있어도 이름이 쓰이는 범위는 다릅니다. 함수 안에서 다른 title에 재대입한다고 호출자 변수가 자동으로 바뀌지는 않습니다. 반면 리스트나 딕셔너리의 내용을 직접 바꾸면 공유된 객체를 통해 호출자에게 영향이 갑니다. 이름 재대입과 객체 수정을 구분합니다.

함수에 상태를 명시적인 인자로 주면 같은 입력으로 같은 결과를 다시 확인하기 쉽습니다. 전역 tasks 목록을 숨겨서 사용하면 앞 호출의 결과가 다음 호출에 남아 독립 사례를 재현하기 어렵습니다. 기본 인자에 빈 리스트를 넣는 방식도 호출 사이에 그 리스트가 공유될 수 있습니다. 이번 함수들은 기본 상태를 숨기지 않고 호출자가 초기 목록을 지정하도록 만듭니다.

호출 흐름을 단계별로 적습니다

예를 들어 initial이 빈 배열이고 titles가 복습, 정리 두 개이면 먼저 list_tasks가 빈 배열을 반환합니다. 첫 반복에서 len은 0이고 create_task에 1과 복습을 전달합니다. 반환 항목을 새 목록에 붙입니다. 둘째 반복에서 len은 1이고 번호 2가 생깁니다. 마지막 list_tasks는 두 항목을 복사해 반환합니다. initial은 계속 빈 배열입니다. 이 순서를 추적 표로 남깁니다.

호출 도식은 main → list_tasks → main → create_task → main으로 시작합니다. 화살표 옆에 인자와 반환값을 씁니다. create_task는 list_tasks를 직접 호출하지 않습니다. 비슷한 함수가 가까이 배치되었다고 서로 호출한다고 판단하지 않고 실제 괄호를 가진 호출식을 찾습니다. 함수를 선언한 위치와 실행 순서를 혼동하면 오류가 어느 단계에서 났는지 설명하기 어렵습니다.

실습 starter의 create_task는 새 항목을 완료 상태로 만들고 list_tasks는 빈 목록만 반환합니다. 두 함수의 계약을 기준으로 수정합니다. main의 입력 해석과 반복은 유지합니다. 빈 제목의 검증이나 HTTP 응답 코드를 여기서 추가하지 않습니다. initial과 titles의 유효성은 이번 입력의 전제이며, 다음 미션의 함수 경계에서 모든 검증을 연결합니다.

실습 출력은 tasks와 original을 가진 JSON 객체입니다. tasks는 생성 뒤 조회 목록이고 original은 입력 initial입니다. 두 필드를 함께 확인하는 이유는 생성 결과만 맞는 구현과 원본도 보존하는 구현을 구분하려는 것입니다. 빈 titles에서도 기존 목록이 조회되어야 합니다. 중복 제목은 같은 문자열을 가진 서로 다른 번호의 항목으로 유지합니다.

흔한 실수는 create_task 호출 뒤 변수에 받지 않고 결과를 버리는 것입니다. 반환값을 다음 리스트에 넣는 줄이 있는지 찾습니다. missing required positional argument라는 TypeError는 정의의 인자 수와 호출의 값을 대조합니다. NoneType 관련 오류가 나면 어떤 함수의 return이 빠졌는지 확인합니다. NameError는 함수 내부 변수 이름을 외부에서 직접 읽으려 했을 때도 발생합니다.

추적 표에는 코드 한 줄마다 자세한 설명을 붙이기보다 경계에서 들어온 값과 나간 값을 적습니다. 오류가 나면 정상 반환이 있었는지, 예외가 위로 전파되었는지도 표시합니다. AI에게 설명을 받았다면 실행 입력을 바꿔 그 설명이 유지되는지 직접 확인합니다. 이번 목표는 함수를 외워 쓰는 일이 아니라 처음 보는 호출에서도 상태와 결과의 흐름을 복원하는 일입니다.

따라하기

반환과 화면 출력을 구분합니다

코드를 main.py에 저장해 python3 main.py로 실행할 수도 있습니다. 함수 입력과 반환값을 적습니다.

def create_task(task_id, title):
    return {"id": task_id, "title": title, "done": False}
task = create_task(1, "복습")
print(task["id"], task["title"], task["done"])

실행 결과

1 복습 False

두 호출을 순서대로 연결합니다

각 반복의 state 길이, 함수 인자와 반환 항목을 표로 만듭니다.

import json
import sys


def create_task(task_id, title):
    return {"id": task_id, "title": title, "done": False}


def list_tasks(tasks):
    return [dict(task) for task in tasks]

initial = []
state = list_tasks(initial)
for title in ["복습", "정리"]:
    state = state + [create_task(len(state) + 1, title)]
print(json.dumps(list_tasks(state), ensure_ascii=False))
print(initial)

실행 결과

[{"id": 1, "title": "복습", "done": false}, {"id": 2, "title": "정리", "done": false}]
[]

목록과 항목을 복사합니다

출력 후 원본과 조회 목록이 독립적인 이유를 설명합니다.

def list_tasks(tasks):
    return [dict(task) for task in tasks]
original = [{"id": 1, "title": "복습", "done": False}]
view = list_tasks(original)
view[0]["done"] = True
print(original[0]["done"], view[0]["done"])

실행 결과

False True

브라우저 실습과 미션에 연결합니다

starter에서 지정한 함수를 수정한 뒤 정상·경계·실패 테스트를 실행합니다. 실패 입력을 손으로 추적하고 기대값 대신 구현을 고칩니다. 추적 표를 보관하고 다음 레슨의 함수 경계에서 사용합니다.

확인 문제

실습

initial은 번호 1부터 연속된 정상 항목 배열, titles는 정규화와 길이 검사를 통과한 문자열 배열입니다. create_task(task_id,title)는 새 미완료 항목을 반환하고 list_tasks(tasks)는 각 항목을 복사해 같은 순서로 반환합니다. 두 함수만 수정합니다. main은 titles를 순서대로 생성하고 tasks와 original을 출력합니다. 중복 제목을 유지하고 initial을 변경하지 않습니다. 잘못된 제목 입력 검증은 이번 부분 실습에서 제외합니다.

모범 답안
import json
import sys


def create_task(task_id, title):
    return {"id": task_id, "title": title, "done": False}


def list_tasks(tasks):
    return [dict(task) for task in tasks]

payload = json.load(sys.stdin)
state = list_tasks(payload["initial"])
for title in payload["titles"]:
    state = state + [create_task(len(state) + 1, title)]
print(json.dumps({"tasks": list_tasks(state), "original": payload["initial"]}, ensure_ascii=False))

더 읽기

면접 질문

  • AI가 만든 코드가 실행될 때 추가로 확인할 내용을 설명해 주시면 됩니다.