AI 개발 · 기본
바이브 코딩 실습 - 작은 서비스 만들기
명령으로 쓰는 첫 버전 - 입력을 받아 저장까지
가장 작은 동작하는 버전부터, 명령 문자열을 해석해 예약 추가·조회·취소, DB 저장과 규칙 함수 연결, 첫 버전에서 일부러 하지 않을 것 정하기, 완성 코드는 미리 정한 명령 목록을 차례로 실행해 결과를 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 예약 시간이 겹치는지 판단하는 규칙을 함수와 테스트로 고정했다. 이제 그 함수를 실제 입력과 연결한다. 사용자가 예약 추가 명령을 보내면 문자열을 나누고, 예약 규칙을 확인한 뒤, 데이터베이스에 저장한다. 조회와 취소도 같은 입구를 통해 처리한다.
이번 장의 결과물은 화면 없이 명령으로 쓰는 첫 버전이다. 프로그램 안에 미리 정한 명령 목록을 넣고 차례로 실행한다. 입력을 기다리지 않으므로 python3 main.py로 실행하면 결과를 출력하고 바로 끝난다. 실행할 때마다 같은 결과를 얻도록 메모리 데이터베이스와 고정된 예약 날짜를 사용한다.
- 가장 작은 동작을 먼저 만들고 추가·조회·취소로 넓힌다.
- 명령 문자열을 명령 이름과 필요한 값으로 나눈다.
- 예약 충돌 함수와 데이터베이스 저장을 연결한다.
- 첫 버전의 범위와 일부러 미룰 기능을 구분한다.
- 예상 결과와 실제 결과를 비교하고 변경 내용을 검토한다.
문제 상황
한 스터디 모임이 공간 두 곳을 함께 쓴다. 지금은 대화방에 “A 공간, 오전 아홉 시부터 열 시까지”라고 남긴다. 뒤늦게 들어온 사람은 앞선 메시지를 놓치고 같은 시간을 신청한다. 취소 메시지를 따로 남기면 처음 예약 글과 취소 글을 함께 찾아야 현재 상태를 알 수 있다.
모임 운영자는 먼저 세 가지 일을 한곳에서 처리하려 한다. 예약을 추가하고, 현재 예약을 보고, 예약 번호로 취소하는 일이다. 달력 화면이나 알림이 없어도 이 세 동작이 연결되면 예약 규칙이 실제 저장 과정에서 지켜지는지 확인할 수 있다.
여기서 서둘러 여러 기능을 요청하면 중요한 연결이 흐려진다. 충돌을 검사하는 함수가 있어도 저장 함수에서 호출하지 않으면 중복 예약이 들어간다. 취소 기능이 있어도 조회가 취소 상태를 무시하지 않으면 빈 시간이 계속 차 있는 것처럼 보인다. 먼저 한 예약이 들어와 조회되고 취소되는 길을 끝까지 확인해야 한다.
이번 실습에서 지원하는 날짜는 2026년 10월 7일 하루다. 공간은 A와 B로 고정한다. 예약자는 명령에 적힌 이름이며, 이름을 인증하거나 권한을 확인하지 않는다. 취소 명령은 예약 번호만 받는다. 이런 제한은 실제 운영에 충분하다는 뜻이 아니라, 이번에 검증할 연결을 작게 정한 것이다.
작은 동작부터 끝까지 연결한다
처음 구현할 동작은 예약 하나를 추가하고 바로 조회하는 것이다. 입력 해석만 만들거나 표만 만드는 것으로 멈추지 않는다. 명령이 데이터베이스까지 도착하고, 저장된 값이 다시 출력으로 돌아오는 것을 확인한다. 이 길이 동작하면 두 번째 예약을 넣어 충돌을 확인하고, 마지막으로 취소를 연결한다.
명령을 처리하는 함수는 문자열 하나를 받아 결과 문자열 하나를 돌려준다. 이 약속을 인터페이스(interface)라고 부른다. 함수 바깥에서는 데이터베이스의 세부 작업을 몰라도 명령과 결과를 비교할 수 있다. 출력은 명령 목록을 실행하는 부분에서 담당한다. 처리 함수 안에서 여기저기 출력하면 어떤 결과를 검사해야 하는지 흐려진다.
첫 버전에서 하지 않을 일도 함께 적는다. 대화식 입력, 여러 날짜 지원, 로그인, 반복 예약, 동시에 들어오는 요청 처리는 이번 범위에 넣지 않는다. 여러 날짜를 지원하려면 충돌 비교에 날짜가 들어가야 하고, 여러 사람이 동시에 요청하면 검사와 저장 사이의 경쟁도 다뤄야 한다. 지금 코드가 그런 문제까지 해결한 것으로 설명해서는 안 된다.
| 구분 | 이번에 하는 일 | 뒤로 미루는 일 |
|---|---|---|
| 입력 | 정해진 명령 목록 실행 | 키보드 입력을 계속 기다리는 기능 |
| 예약 | 고정 날짜의 추가·조회·취소 | 여러 날짜와 반복 예약 |
| 사용자 | 예약자 이름 저장 | 로그인과 취소 권한 확인 |
| 저장 | 한 연결의 메모리 데이터베이스 | 프로그램 종료 후 보존과 동시 요청 처리 |
AI에게 일을 맡길 때도 이 경계를 요청문에 넣는다. “예약 서비스를 만들어 달라”는 요청만으로는 어떤 입력과 출력이 필요한지 정해지지 않는다. 다음처럼 첫 번째 작업을 작게 요청할 수 있다.
Python 3.12 표준 라이브러리만 사용한다. main.py 하나에서 메모리 sqlite3 데이터베이스를 만들고, ADD와 LIST 명령을 처리하라. 날짜는 2026년 10월 7일로 고정한다. ADD는 공간, 예약자, 시작 시각, 종료 시각을 받는다. 기존 충돌 규칙을 저장 전에 호출하라. 입력을 기다리지 말고 명령 목록을 실행한 뒤 종료하라. CANCEL과 화면 기능은 아직 추가하지 말라.
답이 오면 함수 이름이나 설명이 그럴듯한지만 보지 않는다. 실제로 실행해서 추가한 예약이 조회되는지 확인한다. 그다음 변경 요청으로 충돌 사례와 취소를 붙인다. 아래 완성 코드는 이 작은 작업들을 합친 결과이며, 앞 장의 파일 없이 실행할 수 있도록 충돌 함수도 포함한다.
명령 문자열을 실행 가능한 값으로 바꾼다
명령의 형태를 먼저 정한다. 명령 이름은 ADD, LIST, CANCEL 세 가지다. ADD 뒤에는 공간 코드, 예약자 이름, 시작 시각, 종료 시각이 온다. CANCEL 뒤에는 예약 번호가 온다. LIST에는 추가 값이 없다. 형태가 정해져 있어야 AI가 만든 코드와 사람이 기대하는 동작을 같은 기준으로 비교할 수 있다.
| 명령 | 예시 | 결과 |
|---|---|---|
| ADD | ADD A 민서 09:00 10:00 | 성공하면 새 예약 번호 |
| LIST | LIST | 활성 예약 목록 또는 예약 없음 |
| CANCEL | CANCEL 1 | 취소 성공 또는 활성 예약 없음 |
문자열을 나누는 작업에는 표준 라이브러리의 shlex.split을 쓴다. 단순히 공백으로 나누면 “독서 모임”처럼 공백이 있는 이름이 두 값으로 쪼개진다. 이 함수는 따옴표로 감싼 부분을 한 값으로 묶는다. 따라서 ADD A "독서 모임" 13:00 14:00에서 예약자 이름은 하나의 값이 된다.
이것은 운영체제 명령을 실행하는 작업이 아니다. 문자열을 값의 목록으로 나눌 뿐이다. 완성 코드에는 명령을 운영체제로 넘기는 함수가 없다. 입력을 해석하는 도구와 외부 프로그램을 실행하는 도구를 구분해야 한다.
시각은 HH:MM 형태로 받되, 데이터베이스에는 자정부터 지난 분으로 저장한다. 09:00은 540, 10:00은 600이다. 숫자로 바꾸면 충돌 함수는 문자열의 표기 방식에 신경 쓰지 않고 크기를 비교할 수 있다. 조회할 때는 저장된 분을 다시 HH:MM으로 바꾼다.
이번에는 시각 변환과 명령별 값 개수에 필요한 검사만 넣는다. 시각은 정확히 두 자리 시와 두 자리 분을 요구한다. 시작이 종료보다 빠르지 않으면 예약을 받지 않는다. 자정을 넘어가는 예약도 이번 형식으로는 표현하지 않는다. 오류를 종류별로 설계하고 기록하는 작업은 뒤에서 더 자세히 다룬다.
충돌 확인과 저장을 한 흐름으로 묶는다
예약 시간은 시작을 포함하고 종료는 포함하지 않는 구간으로 생각한다. 09:00부터 10:00까지 예약한 뒤 10:00부터 11:00까지 예약하는 것은 허용한다. 두 예약이 겹치는 조건은 새 시작이 기존 종료보다 빠르고, 기존 시작이 새 종료보다 빠른 경우다. 같은 공간의 활성 예약에만 이 조건을 적용한다.
데이터베이스에는 공간 표와 예약 표를 만든다. 예약 표의 공간 코드는 공간 표를 참조한다. 이런 연결을 외래 키(foreign key)라고 한다. sqlite3 연결에서 외래 키 검사를 켜고, 코드에서도 공간이 존재하는지 확인한다. 코드의 검사는 사람이 읽을 결과를 만드는 데 쓰이고, 데이터베이스의 제약은 저장되는 값의 관계를 지키는 데 쓰인다.
취소할 때는 예약 행을 지우지 않고 상태를 cancelled로 바꾼다. 활성 상태는 active다. 충돌 검사와 조회는 활성 예약만 가져온다. 따라서 취소한 시간은 다시 예약할 수 있고, 취소한 행은 연결이 살아 있는 동안 남는다.
추가와 취소의 저장 작업에는 with conn:을 사용한다. 트랜잭션(transaction)은 저장 변경을 하나의 묶음으로 다루는 단위다. 이 블록에서 변경이 정상적으로 끝나면 확정하고, 예외가 발생하면 해당 변경을 되돌린다. 다만 이 코드의 조회 후 추가 흐름은 한 프로그램이 순서대로 명령을 처리한다는 전제에 맞춰져 있다. 이 블록만으로 여러 연결의 동시 예약 문제가 해결되지는 않는다.
SQL 문장에는 값이 들어갈 자리에 ?를 쓰고 실제 값은 별도로 전달한다. 이를 매개변수 바인딩(parameter binding)이라고 한다. 예약자 이름에 따옴표가 들어 있어도 값을 SQL 문장 일부로 조립하지 않는다. 명령 문자열을 나눈 뒤에도 데이터베이스에 값을 안전하게 전달하는 별도 단계가 필요하다.
완성 코드
다음 내용을 main.py로 저장한다. 실행할 때마다 데이터베이스를 새로 만들기 때문에 이전 실행의 예약이 섞이지 않는다. 테스트용 비교에도 일반 조건문을 사용해, 예상과 다른 결과가 나오면 실행을 멈추게 한다.
import shlex
import sqlite3
from datetime import datetime
DAY = "2026-10-07"
def overlaps(start, end, old_start, old_end):
return start < old_end and old_start < end
def check_rules():
cases = [
(540, 600, 570, 630, True),
(540, 600, 600, 660, False),
(600, 660, 540, 600, False),
(540, 660, 570, 600, True),
]
for start, end, old_start, old_end, expected in cases:
actual = overlaps(start, end, old_start, old_end)
if actual != expected:
raise AssertionError("충돌 규칙 확인 실패")
def to_minutes(text):
value = datetime.strptime(text, "%H:%M")
if value.strftime("%H:%M") != text:
raise ValueError("시각은 HH:MM으로 적는다")
return value.hour * 60 + value.minute
def to_clock(minutes):
hour, minute = divmod(minutes, 60)
return f"{hour:02d}:{minute:02d}"
def create_db():
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
conn.executescript("""
CREATE TABLE rooms (
code TEXT PRIMARY KEY
);
CREATE TABLE reservations (
id INTEGER PRIMARY KEY,
room_code TEXT NOT NULL REFERENCES rooms(code),
member TEXT NOT NULL CHECK (length(member) > 0),
start_min INTEGER NOT NULL,
end_min INTEGER NOT NULL,
status TEXT NOT NULL
CHECK (status IN ('active', 'cancelled')),
CHECK (
0 <= start_min
AND start_min < end_min
AND end_min < 1440
)
);
""")
with conn:
conn.executemany(
"INSERT INTO rooms (code) VALUES (?)",
[("A",), ("B",)],
)
return conn
def add_reservation(conn, room, member, start_text, end_text):
start = to_minutes(start_text)
end = to_minutes(end_text)
if start >= end:
return "오류: 시작은 종료보다 빨라야 한다"
if not member.strip():
return "오류: 예약자 이름이 비어 있다"
with conn:
room_row = conn.execute(
"SELECT code FROM rooms WHERE code = ?",
(room,),
).fetchone()
if room_row is None:
return "오류: 없는 공간이다"
rows = conn.execute(
"""
SELECT start_min, end_min
FROM reservations
WHERE room_code = ? AND status = 'active'
""",
(room,),
).fetchall()
for row in rows:
if overlaps(
start, end, row["start_min"], row["end_min"]
):
return "거절: 같은 공간의 예약과 시간이 겹친다"
cursor = conn.execute(
"""
INSERT INTO reservations
(room_code, member, start_min, end_min, status)
VALUES (?, ?, ?, ?, 'active')
""",
(room, member, start, end),
)
reservation_id = cursor.lastrowid
return f"추가: 예약 {reservation_id}"
def list_reservations(conn):
rows = conn.execute("""
SELECT id, room_code, member, start_min, end_min
FROM reservations
WHERE status = 'active'
ORDER BY start_min, room_code, id
""").fetchall()
if not rows:
return "예약 없음"
lines = []
for row in rows:
start = to_clock(row["start_min"])
end = to_clock(row["end_min"])
lines.append(
f'{row["id"]} | {DAY} | {row["room_code"]} | '
f'{row["member"]} | {start}-{end}'
)
return "\n".join(lines)
def cancel_reservation(conn, reservation_id):
with conn:
cursor = conn.execute(
"""
UPDATE reservations
SET status = 'cancelled'
WHERE id = ? AND status = 'active'
""",
(reservation_id,),
)
changed = cursor.rowcount
if changed == 0:
return "취소 불가: 활성 예약이 없다"
return f"취소: 예약 {reservation_id}"
def execute_command(conn, command):
parts = shlex.split(command)
if not parts:
return "오류: 명령이 비어 있다"
action = parts[0]
try:
if action == "ADD" and len(parts) == 5:
return add_reservation(conn, *parts[1:])
if action == "LIST" and len(parts) == 1:
return list_reservations(conn)
if action == "CANCEL" and len(parts) == 2:
reservation_id = int(parts[1])
return cancel_reservation(conn, reservation_id)
except ValueError:
return "오류: 시각 또는 예약 번호 형식이 잘못됐다"
return "오류: 명령 이름 또는 값 개수를 확인한다"
def main():
check_rules()
conn = create_db()
scenarios = [
("LIST", "예약 없음"),
("ADD A 민서 09:00 10:00", "추가: 예약 1"),
(
"LIST",
"1 | 2026-10-07 | A | 민서 | 09:00-10:00",
),
(
"ADD A 준호 09:30 10:30",
"거절: 같은 공간의 예약과 시간이 겹친다",
),
("ADD B 준호 09:30 10:30", "추가: 예약 2"),
("ADD A 수아 10:00 11:00", "추가: 예약 3"),
("CANCEL 1", "취소: 예약 1"),
("CANCEL 1", "취소 불가: 활성 예약이 없다"),
("ADD A 다온 09:00 10:00", "추가: 예약 4"),
(
"LIST",
"4 | 2026-10-07 | A | 다온 | 09:00-10:00\n"
"2 | 2026-10-07 | B | 준호 | 09:30-10:30\n"
"3 | 2026-10-07 | A | 수아 | 10:00-11:00",
),
]
try:
for command, expected in scenarios:
actual = execute_command(conn, command)
if actual != expected:
raise AssertionError(
f"{command}: 예상 {expected!r}, 실제 {actual!r}"
)
print(f"> {command}")
print(actual)
print("확인: 규칙 4건, 명령 10건 통과")
finally:
conn.close()
if __name__ == "__main__":
main()
줄별 해설
가져오기와 고정값
첫 세 줄은 문자열 분리, 데이터베이스 연결, 시각 해석에 쓸 도구를 가져온다. 모두 표준 라이브러리이므로 따로 설치하지 않는다. DAY는 조회 결과에 붙이는 고정 날짜다. 예약 표에 날짜 열을 두지 않은 이유는 모든 예약이 이 하루에 속하기 때문이다. 날짜를 입력받도록 바꿀 때는 이 상수만 바꾸는 것으로 끝나지 않는다.
규칙과 시각 변환
overlaps의 두 비교는 모두 엄격한 부등호다. 종료와 시작이 같은 경계는 겹침으로 보지 않는다. check_rules는 부분 겹침, 양쪽의 인접 구간, 한 구간이 다른 구간을 포함하는 경우를 확인한다. 규칙 함수가 수정되면 저장 동작을 시작하기 전에 이 비교부터 실행된다.
to_minutes는 입력을 시각으로 해석한 뒤 다시 같은 형식으로 출력해 원문과 비교한다. 해석에 성공하더라도 9:00처럼 정한 표기와 다르면 받지 않는다. to_clock의 divmod는 나눈 몫과 나머지를 함께 돌려준다. 600을 60으로 나눈 몫은 10, 나머지는 0이므로 10:00이 된다.
연결과 표 생성
create_db의 :memory:는 파일 경로 대신 메모리에 데이터베이스를 만든다는 뜻이다. 연결을 닫으면 내용도 사라진다. 따라서 여기서 저장 성공은 연결이 열린 동안 다시 조회할 수 있다는 뜻이다. 다음 실행까지 보존된다는 뜻으로 받아들이면 안 된다.
row_factory를 설정하면 조회한 행에서 열 번호 대신 row["start_min"]처럼 열 이름을 사용할 수 있다. 공간 표와 예약 표를 만든 다음 A와 B를 넣는다. executemany는 같은 SQL 문장을 여러 값에 반복 적용한다. 날짜, 공간 목록, 빈 데이터베이스가 모두 고정되어 실행 출발점이 일정하다.
추가에서 번호 반환까지
add_reservation은 시각을 숫자로 바꾸고 구간 방향과 이름을 확인한다. 이어 공간의 존재를 조회한다. fetchone은 행 하나를 가져오고, 결과가 없으면 None이 된다. 공간이 있으면 그 공간의 활성 예약을 모두 가져와 앞 장의 규칙 함수를 적용한다.
충돌하면 INSERT 문장에 도달하기 전에 결과를 돌려준다. 거절된 요청은 예약 행을 만들지 않는다. 충돌이 없을 때만 INSERT를 실행하고, 실행 결과를 담은 객체에서 lastrowid로 방금 만든 예약 번호를 얻는다. 예약 번호를 예약 개수로 계산하지 않는 이유는 취소와 삭제가 생겨도 개수와 번호가 같은 개념이 아니기 때문이다.
조회와 취소
list_reservations는 활성 예약만 읽고 시작 시각, 공간 코드, 예약 번호 순으로 정렬한다. 정렬을 명시해야 출력 순서가 우연한 저장 방식에 기대지 않는다. 문자열 목록에 한 줄씩 넣고 "\n".join(lines)으로 합치면 처리 함수가 여러 줄의 결과도 하나의 문자열로 돌려줄 수 있다.
cancel_reservation은 번호와 활성 상태가 모두 맞는 행만 바꾼다. rowcount는 UPDATE가 영향을 준 행 수다. 이미 취소된 예약은 조건에 맞지 않아 0이 된다. 이 때문에 같은 취소를 두 번 요청해도 두 번째 결과를 성공으로 표시하지 않는다.
명령 분기와 확인 목록
execute_command는 명령 이름과 값 개수를 함께 확인한다. ADD에서 사용하는 *parts[1:]는 남은 네 값을 함수의 네 인수로 펼쳐 전달한다. 이 문법이 낯설다면 공간, 이름, 시작, 종료를 각각 변수에 넣어 호출해도 동작은 같다.
main의 각 항목은 명령과 예상 결과의 쌍이다. 문자열 뒤의 \n은 예상 결과 안에서 줄바꿈을 뜻한다. 실제 결과가 다르면 AssertionError를 발생시키며 예상값과 실제값을 보여 준다. 모든 비교가 끝난 경우에만 마지막 통과 문장이 출력된다. finally는 비교 도중 실패해도 연결을 닫는다.
AI가 취소 기능을 추가했다면 변경 전후의 차이인 diff를 검토한다. 취소 함수만 생겼는지 보는 것으로는 부족하다. 조회와 충돌 조회에 활성 상태 조건이 들어갔는지, 충돌 비교의 부등호가 유지됐는지, 기존 명령의 예상 결과가 이유 없이 바뀌지 않았는지 확인한다. 테스트를 통과시키려고 예상값을 실제값에 맞춰 고치는 일도 피해야 한다.
실행 결과
터미널에서 다음 명령을 실행한다. 경고도 확인하도록 옵션을 붙였으며, 기본 실행 명령도 그대로 사용할 수 있다.
python3 -W error main.py
예상 출력은 다음과 같다. python3 main.py로 실행해도 같은 출력이 나온다.
> LIST
예약 없음
> ADD A 민서 09:00 10:00
추가: 예약 1
> LIST
1 | 2026-10-07 | A | 민서 | 09:00-10:00
> ADD A 준호 09:30 10:30
거절: 같은 공간의 예약과 시간이 겹친다
> ADD B 준호 09:30 10:30
추가: 예약 2
> ADD A 수아 10:00 11:00
추가: 예약 3
> CANCEL 1
취소: 예약 1
> CANCEL 1
취소 불가: 활성 예약이 없다
> ADD A 다온 09:00 10:00
추가: 예약 4
> LIST
4 | 2026-10-07 | A | 다온 | 09:00-10:00
2 | 2026-10-07 | B | 준호 | 09:30-10:30
3 | 2026-10-07 | A | 수아 | 10:00-11:00
확인: 규칙 4건, 명령 10건 통과
겹치는 A 공간 요청은 거절되지만 같은 시각의 B 공간 요청은 저장된다. A 공간의 10:00 시작 예약은 앞 예약의 종료와 맞닿아 허용된다. 예약 1을 취소한 뒤에는 그 자리에 예약 4를 넣을 수 있다. 최종 조회에 예약 1이 없는 것도 함께 확인한다.
이 원고에서는 도구를 실행하지 않았으므로 실행 결과를 실측했다고 주장하지 않는다. 위 출력은 코드의 명령 목록과 예상값에 따른 결과다. 독자는 자신의 환경에서 실행해 비교해야 한다. 컴파일 단계도 따로 확인하려면 다음 명령을 실행한다. 성공하면 출력 없이 종료하며, 생성되는 임시 디렉터리는 블록이 끝날 때 정리된다.
python3 -W error -c 'import py_compile, tempfile; from pathlib import Path; tmp = tempfile.TemporaryDirectory(); py_compile.compile("main.py", cfile=str(Path(tmp.name) / "main.pyc"), doraise=True); tmp.cleanup()'
실무에서 자주 틀리는 것
다음 짧은 코드는 각각 독립적으로 실행할 수 있다. 잘못된 코드는 문법 오류보다 의도와 다른 결과를 만드는 예다.
종료 경계를 겹침으로 처리한다
부등호에 등호를 넣으면 앞 예약이 끝나는 시각에 시작하는 예약도 막힌다. 규칙을 고칠 때는 저장 코드와 떨어진 함수 테스트부터 확인한다.
틀린 코드다.
def overlaps(start, end, old_start, old_end):
return start <= old_end and old_start <= end
print(overlaps(600, 660, 540, 600))
이 코드는 True를 출력한다. 고친 코드는 False를 출력한다.
def overlaps(start, end, old_start, old_end):
return start < old_end and old_start < end
print(overlaps(600, 660, 540, 600))
따옴표로 묶은 이름도 공백에서 나눈다
공백 분리만 쓰면 예약자 이름 때문에 값 개수가 달라진다. 따옴표를 지원하기로 정했다면 해석 함수도 그 약속에 맞춰야 한다.
틀린 코드다.
command = 'ADD A "독서 모임" 13:00 14:00'
parts = command.split()
print(len(parts))
6을 출력하므로 ADD의 형식 검사에서 거절된다. 고친 코드는 5를 출력한다.
import shlex
command = 'ADD A "독서 모임" 13:00 14:00'
parts = shlex.split(command)
print(len(parts))
취소 성공을 검사하지 않고 선언한다
예약 번호를 찾지 못했는데도 성공 문장을 돌려주면 사용자는 취소됐다고 믿는다. 상태 변경 결과를 확인해야 한다. 아래 예는 데이터베이스에서 읽은 변경 행 수가 0인 상황을 작게 재현한다.
틀린 코드다.
def cancel_message(changed, reservation_id):
return f"취소: 예약 {reservation_id}"
print(cancel_message(0, 7))
고친 코드는 변경된 행이 없으면 취소 불가를 돌려준다.
def cancel_message(changed, reservation_id):
if changed == 0:
return "취소 불가: 활성 예약이 없다"
return f"취소: 예약 {reservation_id}"
print(cancel_message(0, 7))
테스트에 맞추려고 충돌 규칙을 우회한다
코드가 실패하면 예상값을 바꾸기 전에 명세와 입력을 다시 읽는다. 같은 공간의 겹치는 시간을 허용하도록 예상 결과를 고치면 테스트가 지나가도 예약 서비스의 요구는 지켜지지 않는다. 특히 기존 규칙 함수가 호출되는 줄을 삭제했는지 diff에서 확인해야 한다.
틀린 코드는 기존 예약을 받아 놓고도 검사하지 않는다.
def can_add(start, end, existing):
return True
print(can_add(570, 630, [(540, 600)]))
고친 코드는 기존 구간을 확인해 이 요청에 False를 돌려준다.
def can_add(start, end, existing):
for old_start, old_end in existing:
if start < old_end and old_start < end:
return False
return True
print(can_add(570, 630, [(540, 600)]))
한눈에 보기
| 부분 | 책임 | 확인할 증거 |
|---|---|---|
| 명령 처리 | 이름과 값 개수 확인 | ADD·LIST·CANCEL이 같은 입구로 들어감 |
| 시각 변환 | HH:MM을 분으로 변환 | 09:00이 540으로 비교됨 |
| 충돌 규칙 | 같은 공간의 활성 구간 비교 | 겹침은 거절하고 인접은 허용함 |
| 데이터베이스 | 추가와 상태 변경 저장 | 조회와 재예약에 변경이 반영됨 |
| 명령 목록 | 예상값과 실제값 비교 | 다르면 중단하고 차이를 표시함 |
| 변경 검토 | 요구 밖의 변경 확인 | 규칙·정렬·상태 조건을 diff로 읽음 |
이 구조에서는 입력의 모양과 예약 규칙이 서로 다른 함수에 있다. 다음에 요청이 다른 방식으로 들어와도 규칙 함수와 저장 함수를 다시 사용할 근거가 생긴다. 다만 재사용 가능하다는 설명만으로 충분하지 않다. 기존 명령 목록이 계속 통과하는지 확인해야 한다.
연습 문제
- 기존 열 개 명령 뒤에 없는 번호를 취소하는 명령을 추가하라. 예상 결과도 함께 적고, 마지막 통과 문장의 명령 수를 실제 목록 길이로 계산하도록 바꿔라.
- 공백이 있는 예약자 “독서 모임”으로 B 공간의 11:00부터 12:00까지 예약을 추가하라. 이어 조회하고, 새 예약 번호와 최종 정렬 순서를 설명하라.
ADD A 민서 10:00 10:00을 실행 목록 끝에 추가하라. 예상 결과를 정하고, 이 요청이 데이터베이스에 저장되지 않는 이유를 설명하라.- 조회 함수를 고쳐 취소한 예약도 함께 출력하자는 요청을 받았다. 활성 예약 조회를 유지하면서 취소 내역을 제공하려면 명령 명세에서 무엇을 먼저 정해야 하는지 쓰고, AI에게 보낼 요청문을 작성하라. 코드는 바꾸지 않는다.
정답과 해설
없는 예약 취소와 명령 수 계산
main에서 scenarios를 만든 직후에 다음 줄을 넣는다. 번호 999에 해당하는 활성 행이 없으므로 UPDATE의 변경 행 수는 0이다.
scenarios.append(
("CANCEL 999", "취소 불가: 활성 예약이 없다")
)
반복문 다음의 통과 출력은 아래 줄로 바꾼다. 이 조각들은 완성 코드의 지정된 위치에서 실행되는 코드다. 명령을 더 넣을 때 출력 숫자를 따로 수정하는 실수를 줄인다. 규칙 사례 수는 기존 네 개를 유지한다.
print(f"확인: 규칙 4건, 명령 {len(scenarios)}건 통과")
공백이 있는 이름
다른 연습 변경을 하지 않은 완성 코드 기준으로 다음 명령과 결과 쌍을 목록 뒤에 추가한다. 취소한 행을 삭제하지 않았고 현재 가장 큰 번호가 4이므로 새 번호는 5다.
scenarios.append(
('ADD B "독서 모임" 11:00 12:00', "추가: 예약 5")
)
scenarios.append(
(
"LIST",
"4 | 2026-10-07 | A | 다온 | 09:00-10:00\n"
"2 | 2026-10-07 | B | 준호 | 09:30-10:30\n"
"3 | 2026-10-07 | A | 수아 | 10:00-11:00\n"
"5 | 2026-10-07 | B | 독서 모임 | 11:00-12:00",
)
)
시작 시각이 11:00인 예약 5는 마지막에 나온다. 공간 B의 기존 예약은 10:30에 끝나므로 충돌하지 않는다. 이름에 들어 있는 공백은 따옴표 안에 있어 한 값으로 전달된다. 명령 수 출력도 첫 문제처럼 계산하도록 바꿔야 한다.
길이가 없는 구간
다음 항목을 목록 뒤에 추가한다. 시각 변환에는 성공하지만 시작과 종료가 같으므로 추가 함수가 저장 전에 결과를 돌려준다.
scenarios.append(
(
"ADD A 민서 10:00 10:00",
"오류: 시작은 종료보다 빨라야 한다",
)
)
이를 충돌 검사에 맡기면 구간의 유효성과 다른 예약과의 관계가 섞인다. 먼저 자체적으로 유효한 구간인지 확인하고, 그다음 기존 구간과 비교한다. 예약 표에도 시작이 종료보다 작아야 한다는 제약이 있지만, 정상 명령 처리에서는 저장 전에 설명 가능한 결과를 돌려준다.
취소 내역 요청의 범위 정하기
기존 LIST가 활성 예약만 보여 준다는 약속을 유지할지 먼저 정한다. 여기서는 LIST를 유지하고 HISTORY라는 별도 명령을 추가하는 방향을 택할 수 있다. HISTORY가 보여 줄 상태, 정렬 기준, 빈 결과 문장까지 정해야 예상값을 만들 수 있다. 취소 시각은 현재 저장하지 않으므로 취소 시각순 조회를 요구하려면 데이터 모델 변경도 필요하다.
기존 LIST는 활성 예약만 보여 주도록 유지하라. 새 HISTORY 명령은 추가 값 없이 모든 예약의 번호, 날짜, 공간, 예약자, 시간, 상태를 번호순으로 반환하게 하라. 빈 결과는 “예약 내역 없음”으로 정한다. 취소 시각은 이번에 추가하지 않는다. 기존 명령 목록을 유지하고, 취소된 예약이 HISTORY에는 보이며 LIST에는 보이지 않는 사례를 추가하라. 구현 전에 바뀔 함수와 SQL 조건을 요약하라.
AI의 요약은 검토할 가설이다. 실제 변경에서 LIST의 조건이 빠지지 않았는지 읽고, 기존 비교와 새 비교를 실행한다. 작은 첫 버전의 가치는 기능 수에 있지 않다. 입력, 규칙, 저장, 결과가 이어지는 경로를 직접 확인할 수 있다는 데 있다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.