AI 개발 · 기본
바이브 코딩 실습 - 작은 서비스 만들기
남이 짠 코드에 기능 넣기 - 읽기·요약·사실 확인
낯선 코드의 구조를 AI 에게 요약받고 직접 호출 흐름으로 확인하기, 요약이 틀린 곳 찾기, 기존 관례를 따르는 변경, 문서·주석 생성 후 사실 확인, 완성 코드는 주어진 기존 모듈(장 안에 포함)에 통계 기능을 관례대로 추가하고 결과를 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 기능 추가 요청을 명세와 회귀 테스트로 다루었다. 회귀 테스트는 변경 뒤에도 이미 되던 동작이 유지되는지 확인하는 테스트다. 이번에는 자신이 만들지 않은 예약 코드를 넘겨받았다고 가정한다. 함수 이름과 주석만 보고 고치기보다, 실제로 실행되는 길을 찾아 이해한 뒤 작은 통계 기능을 붙인다.
AI에게 구조를 요약받으면 낯선 코드를 읽는 출발점을 얻을 수 있다. 그러나 요약은 코드의 동작을 대신 증명하지 않는다. “취소 예약은 제외한다”라는 설명이 맞는지 확인하려면 어느 함수가 어떤 조회문을 실행하는지 찾아야 한다. 이번 실습은 읽기, 요약, 사실 확인, 변경, 재확인의 순서로 진행한다.
- AI가 만든 구조 요약을 함수와 조회문의 실제 위치에 연결한다.
- 프로그램의 시작점부터 함수가 호출되는 순서를 직접 따라간다.
- 기존 코드의 이름, 인수, 반환값, 조회 방식을 따르는 통계 함수를 추가한다.
- 생성된 주석과 문서의 주장을 실행 결과와 테스트로 확인한다.
- 변경 전후의 차이를 검토하고 기존 예약 조회가 유지되는지 확인한다.
문제 상황
동아리와 스터디가 함께 쓰는 공간 예약 서비스를 다른 담당자에게서 넘겨받았다. 프로그램에는 공간과 예약을 저장하는 표가 있으며, 선택한 날짜의 확정 예약을 읽는 함수가 있다. 새 요청은 “날짜별로 공간마다 확정 예약이 몇 건인지, 총 몇 분인지 출력해 달라”는 것이다.
요청 자체는 작지만 기존 코드를 잘못 이해하면 결과가 달라진다. 취소 예약까지 합산할 수 있고, 예약이 없는 공간을 결과에서 빠뜨릴 수 있다. 다른 날짜의 예약을 섞을 수도 있다. 조회 결과의 형태를 바꾸면 원래 예약 목록을 사용하던 코드에도 영향을 준다.
이 장에서 넘겨받은 기존 모듈은 완성 코드의 ‘기존 모듈 영역’에 그대로 포함한다. 데이터베이스를 연결하는 함수, 표를 만드는 함수, 예제 자료를 넣는 함수, 확정 예약을 읽는 함수가 그 영역이다. 별도의 앞 장 파일을 준비할 필요는 없다. 통계 함수와 확인 함수, 출력 부분을 추가하여 main.py 하나로 실행한다.
작업 전에 다음 조건을 짧게 고정한다. 총 예약 건수를 세는 요청이 아니라, 지정한 날짜의 확정 예약만 집계하는 요청이다. 공간이 결과에 등장하는 조건과 예약이 합계에 포함되는 조건도 구분한다.
| 항목 | 정한 동작 | 확인 자료 |
|---|---|---|
| 집계 대상 | 지정한 날짜의 확정 예약 | 취소 예약과 다른 날짜 예약 |
| 집계 단위 | 공간마다 건수와 총 예약 분 | 같은 공간의 확정 예약 두 건 |
| 빈 공간 | 건수와 시간을 0으로 표시 | 취소 예약만 있는 공간과 예약이 없는 공간 |
| 출력 순서 | 공간 번호 오름차순 | 공간 번호 1부터 4까지 |
여기서 총 예약 분은 이용자가 실제로 머문 시간이 아니다. 저장된 종료 분에서 시작 분을 뺀 예약 길이의 합이다. 두 표현을 섞으면 계산은 맞아도 설명이 틀린 문서가 만들어진다.
요약을 코드의 근거에 연결한다
처음에는 코드 전체를 세세하게 설명해 달라고 하기보다, 작업에 필요한 지도를 요청한다. 시작점, 저장 구조, 조회 조건, 반환값을 구분하면 확인할 대상을 줄일 수 있다. 아래 요청과 답은 읽기 방법을 설명하기 위해 만든 대화 예시다. 실제 프로그램 실행에는 AI 연결이나 네트워크 호출이 필요하지 않다.
이 모듈에 날짜별 공간 통계를 추가하려 한다. 아직 수정하지 말고 시작점, 함수별 역할, 데이터베이스 표, 확정 예약을 읽는 조건을 요약해 달라. 각 설명에 확인할 함수 이름을 붙여 달라. 코드로 확인되지 않은 의도는 추측이라고 표시해 달라.
요약에는 다음처럼 사실과 추측이 섞일 수 있다. 일부러 틀린 내용을 포함한 예시를 보고, 어떤 근거를 확인해야 하는지 생각해 본다.
connect_db는 메모리 데이터베이스를 만들고 행을 이름으로 읽도록 설정한다. list_confirmed는 날짜를 받아 확정 예약을 반환한다. start_minute와 end_minute는 Unix 시각으로 보인다. 외래 키를 선언했으므로 연결할 때 별도의 설정은 필요하지 않다. 공간은 이름순으로 출력된다.
앞의 두 문장은 코드와 맞는다. 나머지는 그대로 믿으면 안 된다. start_minute에 들어가는 540은 이 실습에서 자정 이후 540분, 즉 오전 9시를 뜻한다. Unix 시각은 기준 시점 이후의 경과 시간을 표현하는 방식인데, 이 모듈은 그런 값을 저장하지 않는다. 이름에 minute가 있다고 해서 저장 규칙까지 알 수 있는 것은 아니다.
외래 키는 다른 표의 값을 참조하는 관계다. 이 코드의 예약은 공간 번호를 참조한다. sqlite3에서는 표에 관계를 선언하는 것과 연결에서 그 관계의 검사를 켜는 것을 나누어 확인해야 한다. connect_db에 있는 PRAGMA foreign_keys = ON이 검사 활성화의 근거다. 정렬도 함수 이름으로 판단하지 않는다. 기존 조회문 끝에 있는 ORDER BY id가 공간 이름순이라는 요약을 반박한다. 여기서 id는 예약 번호다.
요약을 검토할 때는 문장마다 ‘사실’, ‘추측’, ‘틀림’을 붙이는 것만으로 끝내지 않는다. 어느 코드가 근거인지 기록하고, 틀린 설명이 변경에 어떤 영향을 주는지도 적는다. 분 단위를 시각으로 오해하면 불필요한 변환을 넣게 되고, 정렬을 오해하면 출력 순서를 바꾸게 된다.
| 요약의 주장 | 확인 위치 | 판정과 조치 |
|---|---|---|
| 행을 이름으로 읽는다 | connect_db의 row_factory | 맞음. 기존 행 접근 방식을 유지한다 |
| 시간은 Unix 시각이다 | seed_data의 540과 630 | 틀림. 자정 이후 분으로 설명한다 |
| 외래 키 선언만으로 충분하다 | connect_db의 PRAGMA | 틀림. 연결 설정을 유지한다 |
| 공간 이름순으로 출력한다 | list_confirmed의 ORDER BY id | 틀림. 기존 목록은 예약 번호순이다 |
AI가 “아마 다른 곳에서 처리한다”라고 답하면 확인할 함수나 호출 위치를 다시 요구한다. 확인할 수 있는 코드가 없으면 그 설명은 보류한다. 긴 설명보다 주장 하나와 근거 하나가 연결된 기록이 변경 작업에 더 도움이 된다.
시작점에서 호출 흐름을 따라간다
호출 흐름은 어떤 함수가 다음에 어떤 함수를 실행하는지 이어 놓은 순서다. 이 파일의 시작점은 마지막의 조건문이다. 파일을 직접 실행하면 main이 호출되고, main은 연결 생성, 표 생성, 예제 자료 저장, 확인, 출력을 차례로 수행한다.
함수 정의를 위에서부터 읽는 순서와 실제 호출 순서는 같지 않을 수 있다. 정의된 함수가 한 번도 호출되지 않을 수도 있다. 따라서 먼저 시작점을 찾고, main 안의 함수 호출을 따라가며 각 함수가 무엇을 돌려주는지 확인한다.
완성 코드에서는 verify_behavior 안에서도 기존 목록 조회와 새 통계 조회를 호출한다. 이후 main의 출력 부분에서 다시 조회한다. 함수가 두 번 호출된다는 사실보다 중요한 것은 조회가 저장 자료를 바꾸지 않는다는 점이다. 확인 과정이 자료를 수정하면 화면에 출력할 결과까지 달라질 수 있다.
데이터가 이동하는 형태도 함께 읽는다. connect_db는 연결 객체를 반환한다. 연결 객체는 데이터베이스에 명령을 보내는 통로다. list_confirmed는 그 연결과 날짜 문자열을 받고, 조회한 행을 사전의 목록으로 바꾸어 반환한다. 새 함수도 같은 연결과 날짜를 받고 사전 목록을 반환하도록 맞춘다.
표의 내용을 전부 읽어 Python 반복문으로 합산하는 방식도 가능하다. 하지만 기존 모듈은 조회 조건과 정렬을 SQL에 적고, 조회 결과만 사전으로 바꾼다. SQL은 데이터베이스에 조회와 저장 작업을 요청하는 언어다. 이번 변경에서도 이 방식을 이어 가면 새 함수만 유별난 구조가 되는 일을 줄일 수 있다.
관례를 지키고 설명도 검증한다
관례는 이 코드에서 반복해서 쓰는 작성 방식이다. 새로운 설계 원칙을 도입하는 것보다 먼저 기존 함수의 이름, 인수 순서, 반환 형태, 값을 조회문에 전달하는 방법을 살핀다. 기존 함수가 conn과 day를 받으므로 새 함수도 같은 순서를 따른다. 날짜를 문자열에 직접 끼워 넣지 않고 물음표 자리에 별도 값으로 전달하는 방식도 유지한다.
통계 조회에는 LEFT JOIN을 사용한다. 두 표를 연결하면서 왼쪽 표의 모든 행을 남기는 방식이다. 공간 표를 왼쪽에 두면 예약이 없는 공간도 결과에 남는다. 다만 날짜와 상태 조건을 WHERE에 넣으면 연결 결과에서 공간 자체가 빠질 수 있다. 이번 조회에서는 예약이 합계에 참여할 조건을 ON 안에 둔다.
COUNT(b.id)는 연결된 예약 번호만 센다. 예약이 없는 공간에는 대응하는 예약 번호가 없으므로 건수는 0이 된다. COUNT(*)를 쓰면 예약이 없는 공간에 남겨진 결과 행까지 셀 수 있다. SUM은 더할 예약이 없을 때 값이 없음을 뜻하는 NULL을 반환하므로 COALESCE로 0을 선택한다. 이 함수는 앞의 값이 NULL이면 뒤의 값을 사용한다.
AI에게 변경을 맡길 때도 경계를 명확히 적는다. 구현 요청에 통계 조건과 기존 관례를 함께 넣으면 결과를 검토할 기준이 생긴다.
기존 연결 함수와 list_confirmed는 유지하고 summarize_rooms(conn, day)를 추가해 달라. 모든 공간을 공간 번호순으로 반환하되, 지정한 날짜의 confirmed 예약만 센다. 총 예약 분은 end_minute에서 start_minute를 뺀 값의 합이다. 반환값은 기존처럼 사전 목록으로 한다. 취소 예약만 있는 공간과 예약이 없는 공간은 모두 0을 반환해야 한다.
변경을 받은 뒤에는 차이 비교(diff)를 본다. 이는 수정 전후에 추가되거나 제거된 부분을 나란히 확인하는 방법이다. 요청하지 않은 기존 함수의 이름 변경이나 저장 방식 변경이 있다면 이유를 확인한다. 이번 변경에서 필요한 핵심은 새 조회 함수, 그 결과를 확인하는 코드, 출력 코드다.
문서와 주석도 별도의 확인 대상이다. “확정 예약의 실제 사용 시간을 출력한다”라는 문장은 실행 결과만 보고 만들어질 수 있지만 사실과 다르다. 이 프로그램에는 입실과 퇴실 기록이 없다. “입력 시간을 검증한다”도 틀린 설명이다. 통계 함수는 저장된 값을 읽으며, 음수 길이의 예약을 검사하지 않는다.
통계 함수의 설명을 작성해 달라. 설명의 각 주장에 대응하는 코드나 확인 함수를 함께 제시해 달라. 실제 이용 시간 측정, 시간 입력 검증, 모든 날짜 집계처럼 구현하지 않은 기능은 설명에 넣지 말아 달라.
완성 코드의 함수 설명은 지정 날짜, 확정 상태, 모든 공간, 정렬 기준만 약속한다. 테스트 역시 이 약속과 원래 목록 조회가 유지되는지를 확인한다. 주석을 길게 만드는 것보다 증명할 수 있는 범위로 줄이는 편이 낫다.
완성 코드
다음 내용을 main.py에 저장한다. ‘기존 모듈 영역’은 넘겨받았다고 가정한 코드이며, 통계 기능을 추가할 때 유지할 기준이다. 메모리 데이터베이스를 사용하므로 실행할 때마다 같은 자료로 시작하고 종료하면 자료가 사라진다. 날짜와 시간도 고정되어 있어 실행 결과가 달라지지 않는다.
import sqlite3
# 기존 모듈 영역 시작
def connect_db():
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def create_tables(conn):
conn.executescript("""
CREATE TABLE rooms (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE bookings (
id INTEGER PRIMARY KEY,
room_id INTEGER NOT NULL REFERENCES rooms(id),
day TEXT NOT NULL,
start_minute INTEGER NOT NULL,
end_minute INTEGER NOT NULL,
status TEXT NOT NULL
);
""")
def seed_data(conn):
rooms = [
(1, "다락방"),
(2, "배움방"),
(3, "모임방"),
(4, "작업방"),
]
bookings = [
(1, 1, "2026-10-07", 540, 630, "confirmed"),
(2, 1, "2026-10-07", 660, 720, "confirmed"),
(3, 2, "2026-10-07", 600, 720, "confirmed"),
(4, 2, "2026-10-07", 780, 900, "cancelled"),
(5, 3, "2026-10-07", 540, 600, "cancelled"),
(6, 1, "2026-10-08", 540, 600, "confirmed"),
]
conn.executemany(
"INSERT INTO rooms (id, name) VALUES (?, ?)",
rooms,
)
conn.executemany(
"""
INSERT INTO bookings (
id, room_id, day, start_minute, end_minute, status
) VALUES (?, ?, ?, ?, ?, ?)
""",
bookings,
)
conn.commit()
def list_confirmed(conn, day):
rows = conn.execute(
"""
SELECT id, room_id, start_minute, end_minute
FROM bookings
WHERE day = ? AND status = 'confirmed'
ORDER BY id
""",
(day,),
).fetchall()
return [dict(row) for row in rows]
# 기존 모듈 영역 끝
# 추가한 통계 기능
def summarize_rooms(conn, day):
"""지정 날짜의 확정 예약 통계를 모든 공간에 대해 번호순으로 반환한다."""
rows = conn.execute(
"""
SELECT
r.id AS room_id,
r.name AS room_name,
COUNT(b.id) AS booking_count,
COALESCE(
SUM(b.end_minute - b.start_minute), 0
) AS total_minutes
FROM rooms AS r
LEFT JOIN bookings AS b
ON b.room_id = r.id
AND b.day = ?
AND b.status = 'confirmed'
GROUP BY r.id, r.name
ORDER BY r.id
""",
(day,),
).fetchall()
return [dict(row) for row in rows]
def check_equal(actual, expected, label):
if actual != expected:
raise AssertionError(
f"{label}: 기대값={expected!r}, 실제값={actual!r}"
)
def verify_behavior(conn):
day = "2026-10-07"
check_equal(
list_confirmed(conn, day),
[
{
"id": 1,
"room_id": 1,
"start_minute": 540,
"end_minute": 630,
},
{
"id": 2,
"room_id": 1,
"start_minute": 660,
"end_minute": 720,
},
{
"id": 3,
"room_id": 2,
"start_minute": 600,
"end_minute": 720,
},
],
"기존 확정 예약 조회",
)
stats = summarize_rooms(conn, day)
actual = [
(
row["room_id"],
row["room_name"],
row["booking_count"],
row["total_minutes"],
)
for row in stats
]
check_equal(
actual,
[
(1, "다락방", 2, 150),
(2, "배움방", 1, 120),
(3, "모임방", 0, 0),
(4, "작업방", 0, 0),
],
"날짜별 공간 통계",
)
check_equal(
[
(row["booking_count"], row["total_minutes"])
for row in summarize_rooms(conn, "2026-10-09")
],
[(0, 0), (0, 0), (0, 0), (0, 0)],
"예약이 없는 날짜",
)
def main():
conn = connect_db()
try:
create_tables(conn)
seed_data(conn)
verify_behavior(conn)
day = "2026-10-07"
bookings = list_confirmed(conn, day)
stats = summarize_rooms(conn, day)
print("검증: 통과")
print(f"기준 날짜: {day}")
print(f"기존 조회: 확정 예약 {len(bookings)}건")
for row in stats:
print(
f"{row['room_name']}: "
f"{row['booking_count']}건, "
f"{row['total_minutes']}분"
)
total_minutes = sum(
row["total_minutes"] for row in stats
)
print(f"전체 예약 시간: {total_minutes}분")
finally:
conn.close()
if __name__ == "__main__":
main()
줄별 해설
첫 줄은 표준 라이브러리 sqlite3를 가져온다. connect_db에서 사용하는 :memory:는 파일 경로 대신 메모리에 데이터베이스를 만들라는 뜻이다. row_factory를 sqlite3.Row로 지정하면 조회한 행을 숫자 위치뿐 아니라 열 이름으로 읽을 수 있다. 이어서 외래 키 검사를 켜고 연결을 반환한다.
create_tables는 공간과 예약 표를 만든다. 예약의 room_id는 공간 표의 id를 참조한다. day와 두 minute 열은 날짜와 자정 이후의 분을 따로 저장한다. 이 실습의 예제 예약은 모두 같은 날 시작하고 끝난다. 날짜를 넘기는 예약을 해석하는 규칙은 이 모듈에 없다.
seed_data는 공간 네 곳과 예약 여섯 건을 넣는다. 첫 공간에는 해당 날짜의 확정 예약 두 건과 다음 날짜 예약 한 건이 있다. 두 번째 공간에는 확정 예약과 취소 예약이 함께 있다. 세 번째 공간에는 취소 예약만 있고, 네 번째 공간에는 예약이 없다. 이 차이가 조회 조건의 실수를 드러내는 자료가 된다.
executemany는 같은 저장 명령에 여러 묶음의 값을 차례로 전달한다. 물음표는 값을 받을 자리다. commit은 저장 작업을 확정한다. list_confirmed의 execute에는 날짜 하나를 담은 (day,)를 전달한다. 쉼표가 있어야 원소 하나인 튜플이 된다. 튜플은 값을 묶어 전달하는 자료형이며, 여기서는 조회문에 전달할 값의 순서를 담는다.
기존 조회는 날짜와 confirmed 상태를 동시에 검사한다. fetchall은 결과 행을 모두 가져오고, 마지막 줄은 각 행을 사전으로 바꾸어 목록에 넣는다. 새 통계 함수의 끝부분도 같은 형태다. 이 때문에 호출하는 쪽은 두 함수의 결과를 익숙한 방식으로 읽을 수 있다.
summarize_rooms의 SELECT는 반환할 열을 정한다. AS는 조회 결과에서 사용할 이름을 붙인다. 표에는 id가 있지만 결과에서는 room_id로 받으므로 뜻이 분명해진다. COUNT는 예약 건수를 계산하고 SUM은 예약 길이를 합한다. 공간 이름을 사람이 읽는 표시에 사용하되, 공간 번호로 구분하고 정렬한다.
LEFT JOIN 아래 ON은 예약 표를 연결할 조건이다. 공간 번호가 같고 날짜가 일치하며 확정 상태여야 합계에 참여한다. GROUP BY는 연결된 행을 공간별로 묶는다. 공간 번호와 이름을 함께 적어 반환하는 공간 정보와 묶는 기준이 드러나게 했다.
check_equal은 실제값과 기대값이 다르면 AssertionError를 발생시킨다. 오류에는 확인 항목 이름과 두 값이 함께 들어간다. 기대값을 새 통계 결과로 다시 계산하지 않고, 예제 자료에서 손으로 구한 수치를 적었다. 같은 계산 실수를 기대값에도 넣지 않기 위해서다.
verify_behavior는 먼저 기존 조회의 전체 사전 목록을 확인한다. 예약 번호만 맞아도 시간이 바뀌었다면 실패한다. 다음에는 통계의 공간 이름, 건수, 시간을 함께 확인한다. 마지막 확인은 예약이 전혀 없는 날짜에도 모든 공간이 0으로 반환되는지 검사한다.
main은 검증에 성공한 뒤 결과를 출력한다. 실패하면 “검증: 통과”가 출력되기 전에 오류가 발생한다. finally는 중간에 오류가 나더라도 연결을 닫는다. 마지막 조건문은 이 파일을 직접 실행할 때 main을 호출한다. 함수 정의만 읽어서는 보이지 않던 실행의 출발점이다.
실행 결과
먼저 경고를 오류로 취급하며 문법을 확인하고, 이어서 프로그램을 실행한다. 첫 명령이 성공하면 출력 없이 끝난다. 이 명령은 컴파일 결과를 저장하기 위해 실행 폴더에 __pycache__를 만들 수 있다. 문법 확인은 조회 조건이 맞는지까지 검사하지 않으므로 두 번째 실행도 필요하다.
python3 -W error -m py_compile main.py
python3 main.py
프로그램의 예상 출력은 다음과 같다.
검증: 통과
기준 날짜: 2026-10-07
기존 조회: 확정 예약 3건
다락방: 2건, 150분
배움방: 1건, 120분
모임방: 0건, 0분
작업방: 0건, 0분
전체 예약 시간: 270분
다락방은 90분과 60분을 더해 150분이다. 배움방의 취소 예약 120분은 제외하여 확정 예약의 120분만 남는다. 모임방은 취소 예약만 있으므로 0이고 작업방은 예약 자체가 없어 0이다. 다음 날짜의 다락방 예약도 합계에 들어가지 않는다.
실행이 끝났다고 모든 요구를 확인한 것은 아니다. 변경 전후 비교에서 기존 모듈 영역을 살펴보고, 추가된 조회의 날짜 자리와 상태 조건을 확인한다. 이어서 출력의 270분을 예제 자료에서 직접 계산한 합계와 비교한다. 문법 검사, 동작 확인, 차이 검토는 서로 다른 실수를 찾는다.
실무에서 자주 틀리는 것
함수 이름을 기능의 증거로 삼는다
이름에 confirmed가 있어도 상태 조건이 구현되어 있다는 뜻은 아니다. 다음은 이름과 달리 취소 예약까지 돌려주는 실행 가능한 축소 예제다.
def list_confirmed(rows):
return rows
rows = [{"status": "confirmed"}, {"status": "cancelled"}]
print(len(list_confirmed(rows)))
결과는 2다. 이름에 기대한 규칙을 실제 조건으로 확인해야 한다. 고친 예제는 확정 상태만 선택하여 1을 출력한다.
def list_confirmed(rows):
return [
row for row in rows
if row["status"] == "confirmed"
]
rows = [{"status": "confirmed"}, {"status": "cancelled"}]
print(len(list_confirmed(rows)))
실제 모듈에서는 이 조건이 Python 반복문이 아니라 SQL의 WHERE에 있다. 조건의 위치는 달라도 확인해야 할 주장은 같다.
전체 공간을 남긴 뒤 다시 걸러 버린다
빈 공간까지 반환하라는 요구를 받았는데 건수가 있는 결과만 남기면 요구를 잃는다. 아래 코드는 SQL에서 잘 계산했더라도 마지막 가공에서 빈 공간을 제거하는 실수를 보여 준다.
stats = [
{"room_id": 1, "booking_count": 2},
{"room_id": 2, "booking_count": 0},
]
result = [row for row in stats if row["booking_count"]]
print([row["room_id"] for row in result])
결과는 [1]이다. 모든 공간을 반환하기로 했다면 결과를 그대로 유지한다. 고친 코드는 [1, 2]를 출력한다.
stats = [
{"room_id": 1, "booking_count": 2},
{"room_id": 2, "booking_count": 0},
]
result = stats
print([row["room_id"] for row in result])
완성 코드에서도 같은 위험을 확인해야 한다. ON에서 날짜와 상태를 제한한 이유는 공간을 남기면서 집계 대상 예약만 선택하기 위해서다.
반환 관례를 바꾸고 호출 코드를 그대로 둔다
기존 함수는 사전을 반환하는데 새 함수가 숫자 위치로 읽어야 하는 튜플을 반환하면 사용 방식이 달라진다. 아래 예제는 실행되지만 기존 호출 방식과 맞지 않는 반환값을 만든다.
def summarize_rooms():
return [(1, "다락방", 2, 150)]
print(summarize_rooms()[0])
반환 형태를 바꿔야 할 이유가 없다면 사전으로 맞춘다. 다음 예제는 열 이름으로 예약 건수를 읽어 2를 출력한다.
def summarize_rooms():
return [{
"room_id": 1,
"room_name": "다락방",
"booking_count": 2,
"total_minutes": 150,
}]
print(summarize_rooms()[0]["booking_count"])
관례를 따른다는 것은 새 코드가 기존 코드와 비슷하게 보인다는 뜻에 그치지 않는다. 호출하는 사람이 이미 사용하는 읽기 방식을 이어 갈 수 있다는 뜻이다.
주석이 실제보다 많은 일을 약속한다
실행되는 코드는 같아도 설명이 틀리면 다음 담당자는 없는 기능을 믿고 사용한다. 아래 함수는 입력을 검증하지 않으며 실제 이용 기록도 받지 않는다.
def reservation_minutes(start_minute, end_minute):
"""시간을 검증하고 실제 이용 시간을 계산한다."""
return end_minute - start_minute
print(reservation_minutes(540, 630))
출력은 90이지만 주석의 두 주장은 증명되지 않는다. 코드를 바꾸지 않을 작업이라면 설명을 구현 범위에 맞춘다.
def reservation_minutes(start_minute, end_minute):
"""저장된 종료 분에서 시작 분을 빼 예약 길이를 반환한다."""
return end_minute - start_minute
print(reservation_minutes(540, 630))
AI가 쓴 문서에서도 “자동으로”, “검증한다”, “모든 경우” 같은 표현에 대응하는 코드를 찾는다. 근거가 없다면 표현을 줄이거나 구현이 필요한 별도 요청으로 남긴다.
한눈에 보기
| 단계 | 할 일 | 남길 근거 |
|---|---|---|
| 읽기 | 시작점과 호출 순서를 찾는다 | main에서 조회까지 이어지는 함수 이름 |
| 요약 확인 | 주장마다 코드 위치를 연결한다 | 조건, 반환문, 연결 설정 |
| 변경 | 기존 인수와 반환 관례를 따른다 | 기존 조회와 새 조회의 형태 비교 |
| 검증 | 기존 동작과 새 규칙을 확인한다 | 고정 자료와 손으로 정한 기대값 |
| 설명 확인 | 주석과 문서의 약속을 점검한다 | 각 설명에 대응하는 코드와 테스트 |
실행에 성공했다는 사실은 준비한 자료에서 확인을 통과했다는 뜻이다. 이 코드가 확인하지 않는 시간 입력 규칙까지 보장하지는 않는다. 맡은 기능의 범위를 정확히 설명하는 것도 변경의 일부다.
연습 문제
- AI가 “통계 함수는 모든 날짜의 확정 예약을 합산한다”라고 요약했다. 이 문장을 반박하는 코드 두 곳을 찾고, 다락방의 다른 날짜 예약이 잘못 포함되면 총 예약 분이 얼마가 되는지 계산하라.
- summarize_rooms의 COUNT(b.id)를 COUNT(*)로 바꾸었다고 가정하라. 모임방과 작업방의 예약 건수는 어떻게 달라지는지 설명하고, 어떤 확인 항목이 이 실수를 잡는지 찾으라.
- 예제 자료에서 배움방의 취소 예약을 확정 예약으로 바꾸어라. 기존 목록 조회의 기대값, 공간 통계의 기대값, 전체 예약 시간 출력을 함께 갱신하라. 다락방의 값도 바꿔야 하는지 설명하라.
- 다음 설명을 구현에 맞게 고쳐라. “summarize_rooms는 잘못된 시간을 검사하고, 공간 이름순으로 실제 이용 시간을 보여 준다.” 고친 문장의 각 주장에 대응하는 코드 위치를 적어라.
정답과 해설
ON 안의 b.day = ?가 날짜를 제한하고, execute에 전달한 (day,)가 그 날짜 값을 제공한다. main은 2026-10-07을 전달한다. 다른 날짜 예약은 600 - 540으로 60분이다. 이를 섞으면 다락방은 150분 대신 210분이 된다. 날짜별 공간 통계의 기대값이 이 차이를 잡는다.
두 공간 모두 건수가 0에서 1로 바뀐다. 조건에 맞는 예약이 없어도 LEFT JOIN은 공간 행을 남기며, COUNT(*)는 그 행을 센다. COUNT(b.id)는 값이 없는 예약 번호를 세지 않는다. 날짜별 공간 통계에서 모임방과 작업방을 확인하는 부분이 실패하고, 예약이 없는 날짜 확인도 실패한다.
예약 번호 4의 상태를 confirmed로 바꾸면 기존 목록의 기대값에 id 4, room_id 2, start_minute 780, end_minute 900인 사전을 추가한다. 예약 번호순이므로 마지막에 둔다. 배움방 통계는 2건, 240분이 되고 전체 예약 시간은 390분이 된다. 다락방의 예약 자료와 조회 조건은 바뀌지 않았으므로 2건, 150분을 유지한다. 저장 자료를 바꾼 경우에는 기대값과 출력 설명도 함께 검토해야 한다.
“summarize_rooms는 지정 날짜의 확정 예약에 대해 공간마다 예약 건수와 총 예약 분을 계산하고, 모든 공간을 공간 번호순으로 반환한다”라고 고친다. 날짜와 상태는 ON, 건수와 시간은 COUNT와 SUM, 모든 공간은 rooms를 왼쪽에 둔 LEFT JOIN, 정렬은 ORDER BY r.id가 근거다. 시간 검증과 실제 이용 측정은 구현되어 있지 않으므로 설명에서 제거한다.
낯선 코드를 다루는 작업의 결과물은 추가된 함수만이 아니다. 어떤 설명을 확인했고, 어떤 관례를 유지했으며, 어떤 자료로 변경을 검증했는지도 함께 남긴다. 그 기록이 있으면 다음 사람이 같은 코드를 읽을 때 요약과 사실을 다시 구분할 수 있다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.