그래도 기초가 필요한 이유 - AI 답을 판별하는 눈
이 장에서 배우는 것
앞 장에서 AI에게 맡길 때 조심해야 할 정보와 자료를 살펴보았다. 이번에는 돌려받은 코드 안으로 시선을 옮긴다. 개인정보를 넣지 않았고 실행 중 오류도 나지 않았다고 해서 계산 결과까지 맞는 것은 아니다. 문장이 자연스러운 답과 요구에 맞게 동작하는 프로그램은 서로 다른 기준으로 확인해야 한다.
프로그래밍 기초는 AI보다 많은 문법을 외우기 위한 준비가 아니다. AI가 만든 코드에서 의심할 곳을 찾고, 그 의심을 작은 검사로 바꾸는 데 쓰는 도구다. 이 장에서는 일부러 틀리게 만든 함수 세 개를 검사한 뒤 수정한 함수와 비교한다. 같은 검사를 다시 실행해 무엇이 달라졌는지 확인한다.
- 경계값, 성능, 보안에서 그럴듯한 코드가 틀리는 이유를 구분한다.
- 나눗셈, 윤년 판정, 중복 제거에 숨어 있는 오류를 검사로 드러낸다.
- 수정 전후의 코드 차이와 실행 결과를 함께 확인한다.
- 자신이 놓친 오류를 바탕으로 다음에 공부할 기초 주제를 고른다.
문제 상황
모임에서 사용할 작은 정리 프로그램을 AI에게 부탁했다고 하자. 총비용을 인원수로 나누고, 입력한 해가 윤년인지 판단하고, 참가자 번호에서 중복을 지우는 프로그램이다. AI는 기능을 함수 세 개로 나누어 주었다. 함수는 이름을 붙여 두고 필요할 때 실행하는 코드 묶음이다.
총비용이 8이고 인원수가 2일 때 결과는 4.0이다. 2024년을 넣으면 윤년이라고 나온다. 참가자 번호가 이미 작은 순서로 정리되어 있으면 중복 제거 결과도 자연스럽다. 눈에 띄는 예시만 돌려 보면 충분히 잘 만든 프로그램처럼 보인다.
그런데 참가자가 한 명도 없으면 나눗셈이 멈춘다. 1900년을 넣으면 윤년이라고 잘못 판단한다. 참가자 번호를 접수 순서대로 넣었더니 결과가 작은 번호 순서로 바뀐다. 각 함수는 짧고 읽기 쉬워 보이지만 서로 다른 이유로 요구를 어겼다.
여기서 필요한 일은 AI에게 막연히 다시 잘 만들어 달라고 말하는 것이 아니다. 먼저 우리가 원하는 규칙을 정하고, 그 규칙을 어기는 입력을 골라야 한다. 이번 프로그램에서는 다음 세 가지를 약속으로 삼는다.
| 기능 | 입력 범위 | 지켜야 할 규칙 |
|---|---|---|
| 나눗셈 | 정수 두 개 | 나누는 수가 0이면 None, 그 밖에는 나눗셈 결과 |
| 윤년 판정 | 1 이상의 정수인 연도 | 그레고리력 규칙으로 판단하고 True 또는 False 반환 |
| 중복 제거 | 정수로 이루어진 리스트 | 처음 나타난 순서를 유지하며 각 값을 한 번만 남김 |
None은 파이썬에서 값이 없음을 나타내는 특별한 값이다. 이번에는 계산할 수 없다는 뜻으로 사용한다. True와 False는 각각 참과 거짓을 나타낸다. 리스트는 여러 값을 순서대로 담는 자료다. 입력 범위를 정수로 좁혔으므로 글자나 다른 종류의 입력을 처리하는 기능까지 이번 코드에 넣지는 않는다.
그럴듯한 답을 의심할 세 지점
경계값에서는 규칙의 빠진 부분이 드러난다
경계값은 처리 규칙이 달라지는 지점이나 그 주변의 값이다. 나눗셈에서는 나누는 수 0이 그런 지점이다. 총비용이 0인 경우와 인원수가 0인 경우는 겉보기에는 비슷하지만 의미가 다르다. 0을 2로 나누는 계산은 가능하고 결과는 0.0이다. 8을 0으로 나누는 계산은 일반적인 수 나눗셈으로 처리할 수 없다.
윤년에도 규칙이 갈리는 지점이 있다. 이번에 사용할 그레고리력에서는 4로 나누어떨어지는 해가 윤년 후보가 된다. 그중 100으로 나누어떨어지는 해는 평년이지만, 400으로도 나누어떨어지면 다시 윤년이다. 2024년 하나만 확인하면 첫 조건만 작성한 코드도 통과한다. 1900년과 2000년을 함께 넣어야 예외 규칙이 보인다.
기대 결과는 AI가 내놓은 함수의 실행 결과에서 가져오면 안 된다. 그 함수가 틀렸다면 틀린 답을 정답으로 등록하게 된다. 먼저 말로 정한 규칙을 바탕으로 기대 결과를 적고, 그다음 코드를 실행한다. 검사할 코드와 정답을 만드는 근거를 분리하는 습관이 필요하다.
성능은 같은 일을 얼마나 반복하는지와 관계가 있다
성능은 프로그램이 일을 마치는 데 드는 시간이나 메모리 같은 자원과 관련된다. 중복을 지울 때 이미 모은 리스트를 처음부터 끝까지 매번 살피면, 입력이 길어질수록 비교하는 횟수가 빠르게 늘어날 수 있다. 짧은 참가자 명단에서는 느끼기 어렵지만 자료가 커지면 문제가 된다.
집합인 set은 중복된 값을 하나만 보관하는 자료다. 정수처럼 집합에 넣을 수 있는 값의 포함 여부를 확인할 때 유용하다. 그러나 집합만으로는 처음 입력한 순서를 보존한다는 약속을 표현할 수 없다. 이 장의 수정 함수는 집합으로 이미 본 값을 확인하고, 별도의 리스트에 결과 순서를 남긴다.
빠르게 보이는 도구를 골랐다는 이유만으로 요구를 만족한 것은 아니다. 중복을 없앤 뒤 정렬하면 값의 종류는 맞아도 순서가 달라진다. 이번 검사에서는 실행 시간 대신 결과의 순서를 확인한다. 작은 입력으로 결과가 맞는지 확인한 것과 큰 입력에서도 충분히 빠른지 확인한 것은 별개의 판단이다.
보안은 입력을 어떤 역할로 사용하는지부터 본다
보안은 허용하지 않은 동작이나 정보 접근을 막는 일이다. 예를 들어 계산기를 만들어 달라고 했는데 AI가 입력한 글을 eval에 바로 넘기는 코드를 줄 수 있다. eval은 글을 파이썬 식으로 해석해 계산한다. 사용자가 적은 글을 단순한 자료가 아니라 실행할 식으로 취급하므로, 정수만 받아야 하는 요구보다 넓은 동작을 허용한다.
기초 지식이 있으면 이 차이를 질문할 수 있다. “여기는 정수 하나를 읽는 곳인데 왜 파이썬 식 전체를 해석하는가?”라고 짚는 것이다. 이번 완성 코드에는 사용자 입력이나 eval이 없다. 보안 문제를 실험하려고 위험한 식을 실행할 필요도 없다. 요구보다 강한 기능을 사용하는지 코드를 읽고 확인하는 것으로 시작한다.
작은 검사는 어떻게 판단을 돕는가
테스트는 입력을 주었을 때 실제 결과가 기대 결과와 같은지 확인하는 일이다. 이번 프로그램은 각 함수에 세 가지 입력을 넣는다. 정상적인 예시와 규칙이 갈리는 예시를 섞어 모두 아홉 번 검사한다. 함수가 계산 중 멈추더라도 검사 프로그램 전체가 끝나지 않도록 그 사실을 기록하고 다음 검사로 넘어간다.
검사는 수정 전후에 똑같이 적용한다. 수정한 함수에 유리하게 입력이나 기대 결과를 바꾸면 비교의 근거가 사라진다. 먼저 실패를 확인하고, 필요한 코드를 바꾸고, 같은 검사로 다시 확인한다. 통과 개수가 늘었다는 사실과 실제 수정 내용이 요구에 맞는다는 사실을 함께 살펴야 한다.
나눗셈, 윤년 판정, 중복 제거 함수가 약속을 지키는지 확인해 줘. 나누는 수가 0이면 None을 돌려주고, 윤년은 100년과 400년 규칙까지 반영하며, 중복 제거는 처음 나타난 순서를 유지해야 해. 실패한 입력과 기대 결과를 보여 주고, 수정 전후에 같은 검사를 실행해 줘.
이 요청문에는 원하는 동작과 확인 방법이 함께 들어 있다. AI가 “모든 문제를 해결했다”라고 답하더라도 그 문장 자체가 근거는 아니다. 코드를 직접 실행하고, 변경된 줄을 비교해 수정이 요구와 연결되는지 확인한다. 코드의 변경 전후 차이를 보여 주는 비교를 diff라고 한다.
아래 코드에서는 비교하기 쉽도록 틀린 함수를 지우지 않고 남긴다. 이름 끝의 bad는 수정 전 함수, good은 수정 후 함수라는 표시다. 처음부터 틀린 코드를 올바른 코드로 소개하는 예제가 아니라, 검사 프로그램이 오류를 찾아내는 과정을 관찰하는 예제다.
완성 코드
다음 내용을 main.py에 저장한다. 외부 파일이나 추가 설치는 필요 없다. 세 함수의 입력과 기대 결과를 코드 안에 고정했으므로 실행할 때마다 같은 결과가 나온다.
def divide_bad(a, b):
if a == 0:
return None
return a / b
def leap_bad(year):
return year % 4 == 0
def unique_bad(values):
return sorted(set(values))
def divide_good(a, b):
if b == 0:
return None
return a / b
def leap_good(year):
return year % 400 == 0 or (year % 4 == 0 and year % 100 != 0)
def unique_good(values):
seen = set()
result = []
for value in values:
if value not in seen:
seen.add(value)
result.append(value)
return result
CASES = [
("나눗셈", (8, 2), 4.0),
("나눗셈", (0, 2), 0.0),
("나눗셈", (8, 0), None),
("윤년", (2024,), True),
("윤년", (1900,), False),
("윤년", (2000,), True),
("중복 제거", ([3, 1, 3, 2],), [3, 1, 2]),
("중복 제거", ([],), []),
("중복 제거", ([2, 2],), [2]),
]
def check(title, functions):
passed = 0
print(title)
for name, args, expected in CASES:
try:
actual = functions[name](*args)
except Exception as error:
actual = type(error).__name__
if actual == expected:
passed += 1
else:
print(f"실패: {name} 입력={args!r}")
print(f" 기대={expected!r}, 실제={actual!r}")
print(f"통과: {passed}/{len(CASES)}")
def main():
before = {"나눗셈": divide_bad, "윤년": leap_bad, "중복 제거": unique_bad}
after = {"나눗셈": divide_good, "윤년": leap_good, "중복 제거": unique_good}
check("수정 전", before)
print()
check("수정 후", after)
if __name__ == "__main__":
main()
줄별 해설
빈 줄까지 포함하면 완성 코드는 73줄이다. 빈 줄은 함수와 자료 묶음을 구분한다. 아래 설명은 파일에 보이는 순서대로 읽으면 된다. 문법을 한꺼번에 외우기보다 어떤 줄이 약속을 표현하고 어떤 줄이 그 약속을 확인하는지 나누어 본다.
1~22줄: 계산 규칙을 함수로 표현한다
1줄의 def는 함수를 정의한다. 괄호 안 a와 b는 함수를 실행할 때 전달받을 값의 이름이다. 2줄의 if는 조건이 참일 때 들여쓴 코드를 실행한다. ==는 두 값이 같은지 비교한다. 3줄의 return은 결과를 돌려주고 함수 실행을 끝낸다. 따라서 첫 번째 값이 0이면 4줄의 나눗셈까지 가지 않는다. 이것이 첫 번째 오류다.
7~8줄은 윤년 판정의 수정 전 코드다. %는 나눈 나머지를 구한다. year % 4 == 0은 연도를 4로 나눈 나머지가 0인지 묻는다. 이 조건 하나로는 1900년을 구분할 수 없다. 11~12줄은 집합으로 중복을 없앤 뒤 sorted로 정렬한다. 정렬은 작은 값부터 순서를 다시 정하므로 접수 순서를 유지하지 못한다.
15~18줄은 나눗셈에서 확인할 대상을 b로 고친다. 21~22줄은 윤년 규칙을 완성한다. or는 둘 중 하나가 참이면 참이고, and는 양쪽이 모두 참이어야 참이다. !=는 같지 않다는 뜻이다. 괄호 안은 “4로 나누어떨어지고 100으로는 나누어떨어지지 않음”이다. 여기에 “400으로 나누어떨어짐”을 or로 연결한다.
25~45줄: 순서를 남기고 검사 자료를 준비한다
25줄부터 중복 제거의 수정 함수다. 26줄은 이미 본 값을 담을 빈 집합, 27줄은 결과를 담을 빈 리스트를 만든다. 28줄의 for는 입력의 값을 앞에서부터 하나씩 꺼내어 같은 처리를 반복한다. 29줄은 아직 보지 못한 값인지 확인한다. 처음 본 값이면 30줄에서 집합에 기록하고 31줄에서 결과 리스트 끝에 붙인다. 32줄은 완성한 리스트를 반환한다.
35~45줄의 CASES는 검사할 사례를 모은 리스트다. 각 사례는 기능 이름, 함수에 전달할 값 묶음, 기대 결과의 순서다. 괄호로 묶은 자료인 튜플은 여기서 여러 입력을 한 묶음으로 보관한다. (2024,)의 쉼표는 값 하나를 담은 튜플이라는 표시다. 중복 제거 사례에서는 리스트 하나를 튜플에 담아 전달한다.
48~61줄: 실패를 숨기지 않고 계속 검사한다
48줄의 check는 검사 제목과 사용할 함수들을 받는다. 49줄에서 통과 개수를 0으로 시작하고, 50줄에서 제목을 출력한다. 51줄은 각 사례에서 이름, 입력 묶음, 기대 결과를 꺼낸다. 52~55줄의 try와 except는 실행 중 발생한 예외를 처리한다. 예외는 계산을 계속할 수 없는 상황을 알리는 신호다.
53줄의 functions[name]은 이름에 해당하는 함수를 고른다. *args는 입력 묶음을 풀어 함수에 전달한다. (8, 2)는 두 입력으로, ([2, 2],)는 리스트 한 개로 전달된다. 55줄은 예외가 생기면 그 종류의 이름을 실제 결과 자리에 기록한다. 나누는 수가 0이면 'ZeroDivisionError'가 기록된다.
56~57줄은 실제 값이 기대 값과 같으면 통과 개수를 늘린다. 58~60줄은 다른 경우에만 입력과 두 결과를 출력한다. f로 시작하는 문자열은 중괄호 안의 값을 글에 끼워 넣는다. !r은 None이나 리스트, 문자열을 구분해 볼 수 있는 표현으로 보여 준다. 61줄의 len은 전체 사례 개수를 센다.
64~73줄: 같은 자료로 두 번 비교한다
65~66줄의 중괄호 자료는 이름과 값을 짝지어 보관하는 딕셔너리다. 여기서는 기능 이름에 실행할 함수를 연결한다. 함수 이름 뒤에 괄호를 붙이지 않았으므로 즉시 실행하지 않고 함수 자체를 보관한다. 67줄과 69줄은 같은 CASES를 수정 전후 함수에 각각 적용한다. 68줄은 두 결과 사이에 빈 줄을 넣는다.
72~73줄은 이 파일을 직접 실행했을 때 main을 호출하는 부분이다. 이 파일에서 main은 실행 순서를 정리하는 역할을 한다. 검사 함수와 검사 자료를 그대로 두고 사용할 함수 묶음만 바꾸었으므로 수정 전후 결과를 같은 기준으로 비교할 수 있다.
실행 결과
터미널에서 main.py가 있는 폴더로 이동한 뒤 다음 명령을 실행한다.
python3 main.py
예상 출력은 다음과 같다. 앞부분의 실패 네 개는 의도한 관찰 결과다. 검사 프로그램이 중간에 멈추지 않고 잘못된 함수를 찾아낸 것이다.
수정 전
실패: 나눗셈 입력=(0, 2)
기대=0.0, 실제=None
실패: 나눗셈 입력=(8, 0)
기대=None, 실제='ZeroDivisionError'
실패: 윤년 입력=(1900,)
기대=False, 실제=True
실패: 중복 제거 입력=([3, 1, 3, 2],)
기대=[3, 1, 2], 실제=[1, 2, 3]
통과: 5/9
수정 후
통과: 9/9
수정 후의 9/9는 정해 둔 아홉 사례에서 기대 결과와 같았다는 뜻이다. 모든 입력과 모든 환경에서 맞는다는 증명은 아니다. 이번 함수의 입력 범위를 넓히거나 순서에 관한 약속을 바꾸면 검사도 추가하거나 바꾸어야 한다. 통과 숫자만 보지 말고 어떤 사례를 검사했는지 함께 읽는다.
실제로 수정할 때는 diff도 확인한다. 나눗셈은 비교 대상이 a에서 b로 바뀌었는지, 윤년은 100과 400에 관한 조건이 들어갔는지, 중복 제거는 결과 리스트가 입력 순서대로 만들어지는지 살핀다. 요청하지 않은 파일 접근이나 다른 기능이 추가되었다면 그 변경 이유도 확인해야 한다.
실무에서 자주 틀리는 것
0이라는 값만 보고 역할을 구분하지 않는다
틀린 코드는 첫 번째 값이 0인지 확인한다. 고친 코드는 나누는 수를 확인한다. 두 코드는 따로 실행할 수 있는 짧은 비교 예시다.
def divide(a, b):
if a == 0:
return None
return a / b
print(divide(0, 2))
def divide(a, b):
if b == 0:
return None
return a / b
print(divide(0, 2))
첫 코드는 None을 출력하고 둘째 코드는 0.0을 출력한다. 0을 만났다는 사실만으로 처리하지 말고 그 값이 비용인지 인원수인지 구분한다. 변수의 역할을 말로 설명하면 조건에서 어느 변수를 검사해야 하는지 드러난다.
대표 규칙 하나만 구현하고 예외를 빠뜨린다
윤년은 4의 배수라는 설명에서 멈추기 쉽다. 다음 첫 코드는 True를, 고친 코드는 False를 출력한다.
year = 1900
is_leap = year % 4 == 0
print(is_leap)
year = 1900
is_leap = year % 400 == 0 or (year % 4 == 0 and year % 100 != 0)
print(is_leap)
예외 조건을 추가한 뒤에는 2000년도 검사한다. 100의 배수를 모두 제외하는 식으로 고쳤다면 이번에는 2000년을 틀리게 판단한다. 한 실패를 고친 뒤 다른 규칙이 깨지지 않았는지도 같은 검사 묶음으로 확인한다.
값이 같으면 순서도 상관없다고 생각한다
중복 제거 결과의 원소만 같다고 요구를 만족한 것은 아니다. 첫 코드는 [1, 2, 3]을 출력한다. 고친 코드는 [3, 1, 2]를 출력한다.
values = [3, 1, 3, 2]
result = sorted(set(values))
print(result)
values = [3, 1, 3, 2]
result = []
for value in values:
if value not in result:
result.append(value)
print(result)
두 번째 예시는 순서 규칙을 보여 주기 위해 리스트 하나로 줄였다. 다만 자료가 길어지면 result 안을 반복해서 살펴보는 비용이 커질 수 있다. 완성 코드에서는 같은 순서 규칙을 유지하면서 확인용 집합을 추가했다. 정확성과 성능은 각각 근거를 살펴야 한다.
예외를 잡으면 계산도 성공했다고 여긴다
아래 첫 코드는 나눌 수 없는 상황을 0으로 바꾼다. 실행은 이어지지만 실제로 계산한 0과 실패를 구분할 수 없다.
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return 0
print(divide(8, 0))
def divide(a, b):
if b == 0:
return None
return a / b
print(divide(8, 0))
고친 코드는 이 장에서 정한 약속에 따라 None을 출력한다. 다른 프로그램에서는 예외를 그대로 전달하는 설계도 가능하다. 중요한 것은 실패를 어떤 값으로 알릴지 정하고 그 약속을 검사하는 일이다. 완성 코드가 넓은 범위의 예외를 잡는 이유는 여러 함수를 계속 검사하기 위해서다. 일반 계산 함수에 그대로 복사할 처리 방식은 아니다.
한눈에 보기
| 관찰한 문제 | 판단할 질문 | 다음 공부 | 작은 확인 |
|---|---|---|---|
| 0과 None을 혼동함 | 결과가 숫자인가, 값 없음인가 | 자료형 | 0.0과 None을 각각 출력한다 |
| 윤년 예외를 놓침 | 어떤 조건을 함께 만족해야 하는가 | 조건 | 1900년과 2000년을 비교한다 |
| 중복 제거 순서가 바뀜 | 어느 순서로 값을 처리하는가 | 반복 | 정렬되지 않은 입력을 넣는다 |
| 검사와 계산이 뒤섞임 | 입력과 반환 결과가 분명한가 | 함수 | 같은 함수를 다른 입력으로 호출한다 |
| 저장 후 자료가 달라짐 | 쓴 내용과 읽은 내용이 같은가 | 파일 | 짧은 한글 자료를 저장하고 다시 읽는다 |
자료형은 값의 종류다. 숫자, 문자열, 리스트처럼 값의 종류에 따라 가능한 연산과 처리 방식이 달라진다. 0.0은 계산 결과이고 None은 이번 약속에서 계산할 수 없다는 표시다. 이 둘의 차이를 설명하기 어렵다면 자료형부터 살펴보는 편이 좋다.
조건과 반복은 프로그램이 어떤 길로 진행하는지 이해하는 데 쓰인다. 윤년 식이 복잡하게 느껴지면 조건을 작은 질문으로 나누어 본다. 중복 제거 과정이 헷갈리면 입력을 하나씩 읽을 때 seen과 result가 어떻게 바뀌는지 종이에 적어 본다. 기호를 더 외우기 전에 값의 변화를 따라가는 연습을 한다.
함수는 일을 나누고 각 부분을 따로 검사하는 데 도움이 된다. 파일은 실행이 끝난 뒤에도 자료를 남기는 방법이다. 다음에 만들 단어장에서는 입력한 단어와 뜻을 저장하고 다시 읽어야 한다. 이번 장에서 모든 주제를 한꺼번에 공부할 필요는 없다. 방금 설명하지 못한 줄과 놓친 검사 사례를 기준으로 하나를 고르면 된다.
연습 문제
- CASES에 나눗셈 입력 (0, 0)을 추가한다. 기대 결과를 먼저 적고 수정 전후에 어떤 결과가 나올지 예상한다. 실행 결과가 같아도 수정 전 함수가 올바르다고 결론 내릴 수 있는지 설명한다.
- 윤년 사례로 2023년과 2100년을 추가한다. 각각의 기대 결과를 적고, 수정 전 함수가 어느 사례에서 실패하는지 설명한다.
- 중복 제거 입력 [2, 1, 2, 1, 3]을 추가한다. 완성 코드의 seen과 result가 각 값을 처리한 뒤 어떤 내용을 갖는지 순서대로 적는다.
- AI에게 중복 제거 함수만 수정하도록 요청하는 문장을 작성한다. 지켜야 할 순서, 입력 범위, 검사 사례, 변경 후 확인 방법을 포함한다. 이어서 자신이 다음에 공부할 기초 주제 하나와 선택 이유를 적는다.
정답과 해설
1. 결과가 우연히 같아도 잘못된 조건은 남는다
추가할 사례는 다음과 같다.
("나눗셈", (0, 0), None)
이 표현을 CASES의 항목으로 추가하면 된다. 수정 전 함수는 a가 0이므로 None을 반환한다. 수정 후 함수는 b가 0이므로 None을 반환한다. 둘 다 통과하지만 확인한 조건은 다르다. 수정 전 함수는 기존의 (0, 2)와 (8, 0)에서 여전히 실패한다. 사례 하나의 통과로 함수 전체를 판단하지 않는다.
2. 평년에도 서로 다른 이유가 있다
2023년과 2100년의 기대 결과는 모두 False다. 2023년은 4로 나누어떨어지지 않으므로 수정 전후 함수가 모두 False를 반환한다. 2100년은 4와 100으로 나누어떨어지지만 400으로는 나누어떨어지지 않는다. 수정 전 함수는 True를 반환해 실패하고 수정 후 함수는 False를 반환한다.
2023년은 기본 조건을, 2100년은 예외 조건을 확인한다. 결과가 같은 사례라도 검사하는 이유는 다를 수 있다. 기대 결과 옆에 이유를 한 문장으로 적으면 비슷한 예시만 늘리는 일을 줄일 수 있다.
3. 확인용 자료와 출력용 자료의 역할을 나눈다
첫 2를 처리하면 seen에는 2가 들어가고 result는 [2]가 된다. 다음 1을 처리하면 seen에는 2와 1이 있고 result는 [2, 1]이 된다. 이어지는 2와 1은 이미 보았으므로 두 자료가 바뀌지 않는다. 마지막 3을 처리하면 result는 [2, 1, 3]이 된다.
seen은 포함된 값만 살피면 된다. 집합이 어떤 순서로 표시되는지에 의미를 부여하지 않는다. 사용자가 받을 순서는 result가 책임진다. 수정 전 함수의 결과는 [1, 2, 3]이므로 새 사례에서 실패한다.
4. 수정 요청에도 판단 기준을 남긴다
정수 리스트에서 중복을 제거하는 함수만 수정해 줘. 처음 나타난 값의 순서를 유지해야 해. [2, 1, 2, 1, 3]의 기대 결과는 [2, 1, 3]이고, 빈 리스트의 기대 결과는 빈 리스트야. 기존 검사 자료를 유지하고 새 사례를 추가해 줘. 수정 후 같은 검사를 다시 실행할 수 있게 하고, 바뀐 줄과 변경 이유를 설명해 줘.
다음 공부로 반복을 골랐다면 “값을 하나씩 읽을 때 결과 리스트가 어떻게 달라지는지 설명하기 어려웠기 때문”이라고 적을 수 있다. 조건을 골랐다면 “1900년과 2000년이 서로 다른 결과가 되는 이유를 식으로 연결하기 어려웠기 때문”이 선택 이유가 된다.
AI에게 코드를 맡겨도 기대 동작을 정하고 결과를 판단하는 일은 남는다. 기초는 그 판단을 구체적인 입력과 질문으로 바꾸어 준다. 다음 장에서는 이 습관을 가지고 단어장을 만든다. 기능이 하나씩 늘어날 때마다 기대 결과를 적고, 실행하고, 코드 차이를 읽으며 자신이 설명할 수 있는 프로그램으로 완성한다.