Devin.KR

입력과 출력 계약

75분 안팎

학습 목표

제목 공백·길이·완료 상태의 입력과 기대 출력을 표로 정의합니다.

개념

계약은 결과를 비교할 약속입니다

같은 “제목 추가”라도 공백을 보존하는 앱과 제거하는 앱은 서로 다른 결과를 냅니다. 개발자가 AI에게 선택을 맡긴 뒤 마음에 들지 않는 결과를 고치기만 하면 요청의 기준이 계속 움직입니다. 입력과 출력 계약은 어떤 값이 들어오고 어떤 결과와 상태를 내보낼지 먼저 고정하는 약속입니다. 함수 이름이나 저장 자료구조를 모두 지정하는 대신 밖에서 관찰할 행동을 정합니다.

이 모듈은 HTTP 서버를 구현하지 않습니다. 사례의 input은 JSON 객체이며 op는 add, list, complete 가운데 하나입니다. 생성 입력에는 title, 완료 입력에는 id를 둡니다. 조회는 op만 있으면 됩니다. JSON은 이름과 값을 표현하는 데이터 형식으로, 문자열에는 큰따옴표를 쓰며 참과 거짓은 true와 false로 적습니다. Python 코드 속 True와 JSON의 true를 혼동하지 않도록 파일 안의 표현을 구분합니다.

출력과 상태는 역할이 다릅니다. 출력은 작업을 요청한 쪽이 받은 결과입니다. 상태는 작업 이후 목록에 남아 있는 모든 항목입니다. “추가 성공”만 반환하고 목록을 바꾸지 않는 프로그램은 출력 일부는 맞아도 계약은 어깁니다. 그래서 cases.json에는 expected와 expectedState를 나란히 둡니다. 생성 성공의 expected에는 새 항목을, expectedState에는 그 항목을 포함한 전체 목록을 적습니다.

이번 계약에서 할 일 항목은 id, title, done을 가집니다. id는 성공 생성 순서의 양의 정수이며 첫 항목은 1입니다. title은 정리된 문자열이며 done은 JSON boolean입니다. 새 항목의 done은 false입니다. 삭제가 제외되어 있으므로 정상 초기 상태의 번호는 1부터 연속입니다. 생성 실패는 번호를 소비하지 않습니다. 기존 항목이 한 개라면 다음 성공 생성 번호는 2로 정합니다.

제목과 번호의 경계를 정합니다

제목은 문자열만 받습니다. 숫자 12를 문자열 “12”로 자동 변환하지 않습니다. 문자열인지 확인한 뒤 앞뒤 공백을 제거하고, 정리한 제목의 길이가 1부터 40까지인지 검사합니다. 내부 공백은 그대로 유지합니다. 공백만 있으면 EMPTY_TITLE, 40을 넘으면 TITLE_TOO_LONG, 문자열이 아니면 INVALID_TITLE_TYPE을 반환합니다. 여러 조건을 검사할 때에는 형식, 정규화, 빈 값, 상한 순서로 정합니다.

길이는 이 프로젝트의 Python 참조 모델에서 len으로 세는 코드 포인트 수입니다. 화면에서 보이는 글자 수나 파일의 바이트 수와 같다고 가정하지 않습니다. 예를 들어 결합 문자를 포함한 제목은 눈에 하나로 보여도 여러 코드 포인트일 수 있습니다. 이번 길이 사례는 “가”를 반복하여 해석 차이를 줄입니다. Java로 옮길 때도 같은 계약을 유지하도록 길이 계산을 확인해야 하며 언어의 기본 문자열 길이를 그대로 믿지 않습니다.

정규화는 비교 전에 입력을 정해 둔 형태로 만드는 작업입니다. “ 복습 ”은 “복습”으로 저장합니다. 단, “자료 정리”의 가운데 두 공백을 하나로 바꾸지는 않습니다. 앞뒤 공백은 Python strip이 제거하는 공백으로 정합니다. 실습 사례에는 보통 공백과 탭을 사용합니다. 보이는 글자만 보고 입력이 비었는지 판단하지 말고 정리한 값의 길이를 비교하는 이유가 여기에 있습니다.

완료 입력의 id는 양의 정수이고 boolean은 제외합니다. 문자열 “1”도 정수 1과 다릅니다. 입력 형식이 틀리면 INVALID_ID, 형식은 맞지만 목록에 없는 번호면 NOT_FOUND입니다. 이 차이는 사용자가 값을 잘못 보냈는지, 존재하지 않는 항목을 지정했는지 구분하게 합니다. 임의로 첫 항목을 완료하거나 새로운 항목을 만드는 방식은 없는 번호의 처리 규칙을 어깁니다.

이미 완료된 번호를 다시 완료할 때는 성공으로 같은 항목을 반환하고 상태는 유지합니다. 이는 이번 프로젝트의 선택입니다. 다른 제품에서는 별도 오류로 정할 수 있지만 여기서는 한 번 완료한 뒤 다시 요청해도 동일한 완료 상태를 얻습니다. 같은 제목을 두 번 생성하면 두 번호가 생기는 규칙과 혼동하지 않습니다. 생성과 완료는 서로 다른 행동이므로 반복 요청의 결과를 각각 정합니다.

결과와 전체 상태를 표로 작성합니다

성공 결과에는 ok:true와 task 또는 tasks가 들어갑니다. 생성·완료는 task 하나를, 조회는 tasks 배열을 반환합니다. 실패 결과는 ok:false와 error 코드만 반환합니다. 조회할 항목이 없을 때에는 성공과 빈 배열을 반환합니다. 빈 목록을 오류로 보낼지 빈 결과로 보낼지는 선택이 필요한 부분이며, 이 트랙에서는 빈 조회도 정상 사용 상황으로 다룹니다.

목록 조회는 생성 순서, 즉 id 오름차순입니다. 완료 여부에 따라 항목을 옮기거나 숨기지 않습니다. 이 규칙은 아직 정렬 코드를 만들지 않더라도 문서에 먼저 둡니다. 두 항목이 있을 때 1번을 완료하고 조회해도 1번, 2번 순서를 유지합니다. 필터와 정렬 기능은 제외 범위이므로 완료 항목을 목록에서 지우는 행동을 편의 기능으로 덧붙이지 않습니다.

모든 실패는 기존 목록을 보존합니다. 공백 제목을 넣거나 없는 id를 완료한 뒤에 항목 수, 순서, 제목, done 값이 그대로 있어야 합니다. 오류 코드만 맞고 상태가 달라지는 구현을 놓치지 않으려면 실패 사례를 빈 목록에서만 확인하지 않습니다. 미완료 “복습” 항목 하나가 있는 상태에서 실패 입력을 주고 그 항목이 남는지 적습니다. 이전 상태가 무엇인지 써야 이후 상태를 판단할 수 있습니다.

사례 파일의 각 행은 독립 실행입니다. initial은 그 행의 시작 상태이고 expectedState는 그 행의 끝 상태입니다. C01을 실행한 뒤 얻은 목록을 자동으로 C02에 넘기지 않습니다. 순차 시나리오를 검증하려면 한 행의 끝 상태를 다음 입력의 초기 상태로 명시하거나 별도의 순차 검사를 만들어야 합니다. 독립 사례와 연속 실행을 섞으면 없는 번호 사례가 뜻하지 않게 성공할 수 있습니다.

표를 작성할 때 입력의 종류, 초기 상태, 기대 결과, 이후 상태, 요구사항 번호를 함께 적습니다. 빈 제목 행의 요구사항은 R1입니다. 없는 번호 완료 행은 R3입니다. 이 연결을 이용하면 생성 요구를 수정했을 때 어떤 사례를 다시 검토할지 찾을 수 있습니다. 숫자를 많이 붙이는 것보다 각 행이 어떤 약속을 확인하는지 설명할 수 있는 것이 더 중요합니다.

계약에는 구현 방식과 검증 시점을 구분해 적습니다. 이 모듈에서 결과 객체의 모양을 정하더라도 HTTP 응답의 상태 코드나 경로는 아직 정하지 않습니다. JSON error와 HTTP 오류 상태를 동일한 개념으로 쓰지 않습니다. 이후 API 연결 모듈에서 이 계약에 전송 계층을 덧붙입니다. 아직 실행하지 않은 API를 검증 완료라고 기록하지 않으면 다음 작성자가 구현해야 할 부분을 정확히 알 수 있습니다.

흔한 오류는 null, 빈 문자열, 필드 누락을 모두 같은 제목으로 취급하는 것입니다. 이번 생성 판정 함수에서는 title을 꺼낼 때 누락도 None으로 받고 문자열 형식 오류로 판정합니다. JSON 문법 오류는 계약의 제목 오류와 다릅니다. 큰따옴표나 쉼표가 잘못되면 JSONDecodeError가 나오므로 우선 입력 파일을 고칩니다. 올바른 JSON 안의 숫자 제목은 프로그램이 INVALID_TITLE_TYPE으로 응답해야 하는 사례입니다.

이번 제출물은 제목 입력 규칙과 성공·실패 결과 표입니다. 1자와 40자를 허용하고 0자와 41자를 거절한다는 점, 길이는 정규화 후 측정한다는 점, 이미 완료와 없는 번호를 다르게 다룬다는 점을 자신의 말로 설명합니다. 계약의 각 선택은 사용자 이야기에 필요한 행동을 지원해야 합니다. 사고를 명세로 바꾸는 일반적인 방법은 더 읽기로 연결하고 이 표는 프로젝트의 실제 판단 기준으로 유지합니다.

따라하기

JSON 값의 종류를 확인합니다

브라우저의 Python 실행 창 또는 Python 파일에서 실행합니다. JSON 숫자, 문자열, boolean은 다른 값입니다. 이 코드는 입력 문자열을 스스로 포함하므로 추가 입력 없이 실행됩니다.

import json
payload = json.loads('{"title":"복습","id":1,"done":false}')
print(type(payload["title"]).__name__)
print(type(payload["id"]).__name__)
print(type(payload["done"]).__name__)

실행 결과

str
int
bool

정규화 뒤 길이를 확인합니다

실행 후 원본 길이와 정리한 길이를 비교합니다. 입력 계약은 정리한 길이를 사용합니다.

title = "  복습  "
clean = title.strip()
print(repr(clean))
print(len(title), len(clean))

실행 결과

'복습'
6 2

결과와 상태 표를 작성합니다

미완료 복습 항목 1번이 있는 상태에서 complete id=9를 적습니다. expected는 ok:false, error:NOT_FOUND이고 expectedState는 원래 항목 하나입니다. 없는 id라는 실패 이유와 R3 연결을 함께 적습니다.

계약의 빈칸을 점검합니다

1자·40자·41자, 문자열 아닌 제목, 이미 완료한 번호, 빈 조회를 표에 추가합니다. 조회의 빈 배열은 성공이고 HTTP 경로와 상태 코드는 후속 결정임을 적습니다.

확인 문제

실습

입력·초기 상태·기대 결과·전체 이후 상태·요구사항 번호를 열로 가진 표를 만듭니다. 정규화, 1자·40자·41자, 빈 값, 숫자 제목, 없는 id, 이미 완료, 빈 조회를 포함합니다. 앞뒤 공백 처리 뒤 길이 측정과 모든 실패의 상태 보존을 표시합니다. 결과와 상태가 다른 역할임을 설명한 문장도 제출합니다.

더 읽기

면접 질문

  • 반환 결과와 작업 이후 상태를 각각 계약에 적어야 하는 이유는 무엇인가요?
  • 완료 요청에서 잘못된 형식의 id와 존재하지 않는 id를 어떻게 구분하나요?