
대 AI 시대
웹에서 질문하던 시절 → IDE로 들어온 순간
예전에는 웹에서 AI에게 질문하고, 나온 스니펫을 복사해서 붙여넣는 방식이 거의 전부였다. 그런데 2025년 6월 4일, Cursor 1.0의 등장으로 흐름이 완전히 바뀌었다. IDE 안에서 바로 “의도 → 수정 → 적용”이 닫힌 루프로 돌아가니까, 웹 기반 질의응답이랑은 비교가 안 될 정도로 생산성이 올라갔다.
이후 대부분의 AI들이 웹 형태를 벗어나 IDE에 통합된 Agent로 진화했고, Claude도 그 흐름에 올라탔다. Claude 쪽에서는 2025년 2월 24일 Claude Code가 공개되면서, “브라우저가 아니라 개발 환경 안에서 실행되는 Claude”가 현실이 됐다.
즉, AI는 더 이상 ‘검색/질문 도구’가 아니라 개발 워크플로우 자체의 구성 요소가 된 것이다.
AI Agents의 한계
다만 당시만 해도 AI Agent가 생성하는 코드는 조금만 복잡해져도 사용자가 원하는 방향에서 벗어나는 경우가 많았다. 맥락·의도·숨은 제약을 완벽히 잡아내기 어렵고, 구조가 커질수록 “그럴듯한데 어딘가 어긋난 결과물”이 늘어났다. 그래서 코드 작성 없는 full-agent 개발은 아직 불가능해 보였다.
패러다임이 흔들리기 시작한 날: Claude Opus 4.5
분위기가 바뀌기 시작한 건 2025년 11월 24일 Claude Opus 4.5가 등장하면서부터다. 핵심은 단순 성능 향상만이 아니라, “목표를 어떻게 달성할지”를 더 안정적으로 끌고 가는 방향으로 진화했다는 점이다.
특히 Plan 모드 같은 흐름이 본격화되면서, 먼저 코드베이스를 읽고(분석하고), 필요한 질문을 던지고, 계획을 세운 뒤 실행하는 식으로 목표 달성률 자체가 확 뛰는 체감이 생겼다. 이 지점부터는 “AI가 코드를 만들어준다”가 아니라, “AI가 목표를 달성해준다”에 가까워졌고, 그래서 개발 방식의 패러다임이 흔들리기 시작했다는 느낌이 들었다.
그래도 ‘full-agent’가 불가능한 이유
그럼에도 크리티컬한 오류들은 남아 있다.
- 목표 해석 오류: 목표의 맥락/의도/숨은 제약을 파악 못해서 엉뚱한 방향으로 가는 문제
- 국소 최적화 함정: 현재 task 기준으로만 최적을 찾아서 전체 최적을 망치는 문제
- 책임 구조 붕괴: 법적/금전적/평판 리스크나 조직 KPI 같은 “실패 비용”을 인식하지 못하는 문제
- 분산된 오류 누적: 각 task는 그럴듯하지만, 전체 시스템 관점에서 치명적 결함이 누적되는 문제
결국 사람의 ‘판단’ 없이 돌아가는 full-agent는 구조적으로 성립하기 어려운 현실이다.
파도에 휩쓸리지 말고, 파도를 타보자
그렇다면 우리는 AI를 사용하면 안 되는 걸까? 결론은 그렇지 않다. full-agent가 불가능하다는 건 ‘AI가 쓸모없다’는 뜻이 아니라, ‘검증과 통제 없이 맡기면 위험하다’는 뜻에 가깝다. 결국 문제는 “AI를 쓰지 말자”가 아니라, AI가 흔들리는 지점(목표 해석 오류, 국소 최적화, 책임 인식 부재, 분산된 오류 누적)을 어떻게 통제할 것인가다.
그리고 이 위험을 감수하더라도 사람들이 AI를 계속 쓰는 이유는 명확하다. 불과 몇 달 전만 해도 “에이전트는 아직 못 믿겠다”는 말이 많았는데, 지금은 오히려 업무 흐름을 AI 기준으로 재구성하는 쪽으로 빠르게 바뀌고 있다.
단순히 코드 한 줄 생성하는 수준이 아니라, '요구사항 분해 → 변경 영향 분석 → 구현 → 리팩토링 → 테스트 수정' 같은 반복 비용이 큰 구간에서 체감 생산성이 너무 크기 때문이다. 즉, “완전 자동화”는 어렵더라도 ‘사람이 판단하고 AI가 실행하는 협업 모델’은 이미 실전에서 충분히 강력하다.
결국 이런 AI 폭풍 속에서 살아남으려면 “AI를 쓰냐 마냐”가 아니라, AI를 ‘제대로’ 사용할 줄 아는가가 핵심이 됐다. 파도에 휩쓸리지 않으려면, 결국 파도에 올라타야 한다.
이 글에서 말하는 테스트는 “정답을 맞히는 장치”라기보다, AI가 어긋나는 방향으로 달려가지 않도록 잡아주는 가드레일에 가깝다.
그래서 테스트를 어떻게 붙이면 AI 개발이 더 안정적으로 굴러가는지를 실험해보려고 한다.
- 자동 TDD (Test-Driven-Development, AI Agent가 TestCode를 먼저 작성하고, 그 테스트코드에 맞춰서 기능을 개발)
- 자동 TLD (Test-Last-Development, AI Agent가 기능을 먼저 개발하고, 그 기능에 맞춰서 테스트코드를 작성)
두 가지 흐름을 같은 요구사항 구현으로 비교해보고, 어떤 방식이 현실적으로 가장 적절한지 판단해보자.
요구사항 정의
요구사항은 간단하게
- 회원가입
- 내 정보 조회
- 비밀번호 수정
세 가지의 API를 구현하는 것이다. 각 API에 대한 디테일한 요구사항은 다음과 같고, TDD와 TLD에 모두 동일한 문서를 제공하고 진행했다.
# 전체 요구사항/제약사항
## 1. 공통
### 1.1 인증 헤더
사용자 정보가 필요한 모든 요청에는 아래 두 헤더를 포함해야 한다.
| 헤더 | 설명 |
|------|------|
| `X-Loopers-LoginId` | 사용자 로그인 ID |
| `X-Loopers-LoginPw` | 사용자 비밀번호 (raw) |
### 1.2 Trim 규칙
| 필드 | 규칙 |
|------|------|
| `loginId`, `name`, `email` | `trim()` 적용 후 검증. 결과가 빈 문자열이면 요청 실패 |
| `password` | `trim()` **금지**. 공백/개행/제어문자가 포함되면 요청 실패 |
### 1.3 에러 응답
- 모든 비즈니스 예외는 `CoreException` + `ErrorType` 조합으로 처리
- 흐름: `throw new CoreException(ErrorType.XXX)` → `GlobalExceptionHandler` → `ErrorResponse(code, message)`
- `@Valid` 검증 실패 → `MethodArgumentNotValidException` → BAD_REQUEST 자동 반환
- 에러 응답 필드: `code` (String), `message` (String)
---
## 2. 회원가입 (POST /api/v1/users)
### 2.1 입력 필드
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `loginId` | String | O | 로그인 ID |
| `password` | String | O | 비밀번호 |
| `name` | String | O | 이름 |
| `birthday` | LocalDate | O | 생년월일 (yyyy-MM-dd) |
| `email` | String | O | 이메일 |
### 2.2 응답 (201 Created)
| 필드 | 타입 | 설명 |
|------|------|------|
| `id` | Long | 사용자 ID |
| `loginId` | String | 로그인 ID |
| `name` | String | 이름 |
| `birthday` | String | 생년월일 (yyyy-MM-dd) |
| `email` | String | 이메일 |
- **비밀번호/해시는 응답에 절대 포함하지 않음**
### 2.3 필드별 검증 규칙
#### loginId (로그인 ID)
| 항목 | 규칙 |
|------|------|
| 허용 문자 | 영문 대소문자 + 숫자만 (`^[a-zA-Z0-9]+$`) |
| 전처리 | `trim()` → **소문자 정규화** |
| 길이 | 4~20자 |
| 중복 검사 | 정규화된(trim + lowercase) 값 기준으로 중복 체크 |
| ErrorType | `INVALID_LOGIN_ID_FORMAT` (400) |
| 중복 시 | `USER_ALREADY_EXISTS` (409) |
#### password (비밀번호)
| 항목 | 규칙 |
|------|------|
| 길이 | 8~16자 |
| 필수 포함 | 영문 대문자(`[A-Z]`), 영문 소문자(`[a-z]`), 숫자(`[0-9]`), ASCII 특수문자 각 1개 이상 |
| 허용 문자 | 영문 대소문자 + 숫자 + ASCII 특수문자만 |
| 금지 | 공백, 개행, 제어문자 |
| trim | **금지** (입력 그대로 검증) |
| 생년월일 포함 금지 | `YYYYMMDD` (예: `19900115`), `YYYY-MM-DD` (예: `1990-01-15`) 형식 모두 체크 |
| 저장 | 해시 저장 (SHA-256 + Base64) |
| ErrorType (형식) | `INVALID_PASSWORD_FORMAT` (400) |
| ErrorType (생년월일) | `PASSWORD_CONTAINS_BIRTHDAY` (400) |
#### name (이름)
| 항목 | 규칙 |
|------|------|
| 허용 문자 | 한글, 영문, 공백 |
| 전처리 | `trim()` 적용. 빈 문자열이면 실패 |
| 길이 | 1~50자 |
| ErrorType | `INVALID_NAME_FORMAT` (400) |
#### birthday (생년월일)
| 항목 | 규칙 |
|------|------|
| 형식 | `yyyy-MM-dd` (ISO-8601) |
| 범위 | 1900-01-01 이후, 오늘 이전 (미래 날짜 금지) |
| ErrorType | `INVALID_BIRTHDAY` (400) |
#### email (이메일)
| 항목 | 규칙 |
|------|------|
| 최대 길이 | 254자 (RFC 5321) |
| 형식 | `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` |
| 금지 | 공백, 제어문자 |
| 도메인 추가 규칙 | 연속 점(`..`) 금지, 시작/끝 점 금지 |
| 전처리 | `trim()` 적용. 빈 문자열이면 실패 |
| ErrorType | `INVALID_EMAIL_FORMAT` (400) |
### 2.4 필수 필드 누락
- 필수 필드가 누락되면 `BAD_REQUEST` (400) 반환
---
## 3. 내 정보 조회 (GET /api/v1/users/me)
### 3.1 인증
- 필수 헤더: `X-Loopers-LoginId`, `X-Loopers-LoginPw`
### 3.2 응답 필드
| 필드 | 타입 | 설명 |
|------|------|------|
| `loginId` | String | 로그인 ID |
| `name` | String | 이름 (**마스킹 적용**) |
| `birthday` | String | 생년월일 (yyyy-MM-dd) |
| `email` | String | 이메일 |
- **비밀번호/해시는 응답에 절대 포함하지 않음**
### 3.3 이름 마스킹 규칙
| 이름 길이 | 규칙 | 예시 |
|-----------|------|------|
| 2자 이상 | 마지막 글자를 `*`로 대체 | `홍길동` → `홍길*`, `John` → `Joh*` |
| 1자 | 전체를 `*`로 대체 | `김` → `*` |
---
## 4. 비밀번호 수정 (PATCH /api/v1/users/me/password)
### 4.1 인증
- 필수 헤더: `X-Loopers-LoginId`, `X-Loopers-LoginPw`
### 4.2 입력 필드
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| `currentPassword` | String | O | 현재 비밀번호 (raw) |
| `newPassword` | String | O | 새 비밀번호 (raw) |
### 4.3 검증 규칙
| # | 규칙 | 설명 |
|---|------|------|
| 1 | 현재 비밀번호 일치 검증 | `currentPassword`가 저장된 해시와 일치해야 함 |
| 2 | 기존 비밀번호와 동일 불가 | `newPassword` ≠ `currentPassword` |
| 3 | 회원가입과 동일한 비밀번호 규칙 | 8~16자, 영문 대소문자 + 숫자 + 특수문자 필수, 공백/개행/제어문자 금지, 생년월일 포함 금지 |
| 4 | 해시 저장 | 새 비밀번호는 해시로 저장 |
| 5 | trim 금지 | 비밀번호 필드는 `trim()` 적용하지 않음 |
---
## 5. ErrorType 정리
| ErrorType | HTTP Status | Code | Message |
|-----------|-------------|------|---------|
| `INTERNAL_ERROR` | 500 | `Internal Server Error` | `일시적인 오류가 발생했습니다.` |
| `BAD_REQUEST` | 400 | `Bad Request` | `잘못된 요청입니다.` |
| `NOT_FOUND` | 404 | `Not Found` | `존재하지 않는 요청입니다.` |
| `CONFLICT` | 409 | `Conflict` | `이미 존재하는 리소스입니다.` |
| `USER_ALREADY_EXISTS` | 409 | `USER_ALREADY_EXISTS` | `이미 가입된 로그인 ID입니다.` |
| `INVALID_PASSWORD_FORMAT` | 400 | `INVALID_PASSWORD_FORMAT` | `비밀번호는 8~16자이며, 영문 대소문자, 숫자, 특수문자를 모두 포함해야 합니다.` |
| `PASSWORD_CONTAINS_BIRTHDAY` | 400 | `PASSWORD_CONTAINS_BIRTHDAY` | `비밀번호에 생년월일을 포함할 수 없습니다.` |
| `INVALID_LOGIN_ID_FORMAT` | 400 | `INVALID_LOGIN_ID_FORMAT` | `로그인 ID는 영문과 숫자만 사용 가능하며, 4~20자여야 합니다.` |
| `INVALID_NAME_FORMAT` | 400 | `INVALID_NAME_FORMAT` | `이름은 한글, 영문, 공백만 사용 가능하며, 최대 50자입니다.` |
| `INVALID_EMAIL_FORMAT` | 400 | `INVALID_EMAIL_FORMAT` | `올바른 이메일 형식이 아닙니다.` |
| `INVALID_BIRTHDAY` | 400 | `INVALID_BIRTHDAY` | `생년월일은 1900-01-01 이후, 오늘 이전이어야 합니다.` |
CLAUDE.md 정의
다음으로 Full-Agent Coding에 가장 큰 영향을 미치는것은 CLAUDE.md 일 것이다.
그런데 나는 TDD와 TLD의 차이만을 보고 싶은 것이기에, 최대한 다른 변수들을 통제하는 것이 중요했다.
따라서 CLAUDE.md의 init setting은 동일하게 가져가고, 이후 개발을 진행하면서 동일한 프롬프트를 통해 CLAUDE.md를 업데이트했다.
여기서 "왜 CLAUDE.md를 동일한 version으로 쓰지 않고, 프롬프트만을 동일하게 가져갔을까?" 라는 의문이 생길수도 있는데, CLAUDE.md를 업데이트 하는 것 또한 TDD와 TLD 차이에 종속되는 것이라 판단했기에 이와 같은 방식으로 진행했다.
초기 CLAUDE.md
1. 기술 스택 및 버전
2. 모듈 구조
3. 패키지 구조
4. 개발 규칙
4-1. 진행 work flow
4-2. 개발 work flow (TDD or TLD)
4-3. 테스트 컨벤션
5. 주의사항
5-1. Never Do
5-2. Recommendation
5-3. Priority
UPDATE_GUIDE.md
1. @CLAUDE.md 을 읽어서 속성을 파악한 후, @CLAUDE.md 에는 어떤 내용들이 들어가야하는지 파악해줘.
2. 파악한 내용들로 @CLAUDE.md 에 들어가야하는 **중요한 내용**의 기준을 세워줘.
3. 지금까지 전체 계획을 진행을 remind한다음, **중요한 내용의 기준**에 부합되는 내용들 중, @CLAUDE.md 에 누락되어있는 부분을 업데이트해줘.
실험 진행
이제 실제로 두 가지 개발 흐름을 같은 조건에서 실행해보고, 차이가 어디서 발생하는지 확인해보자.
이번 실험의 목적은 “AI Agent가 개발을 주도하는 상황에서, 테스트 작성 시점(TDD vs TLD)이 안정성에 어떤 영향을 주는가”를 보는 것이기 때문에, 그 외 변수들은 최대한 통제했다.
실험 공통 조건
- 요구사항 문서: 앞에서 정의한 요구사항 문서를 그대로 사용
- CLAUDE.md 초기 버전: 동일한 버전으로 시작
- CLAUDE.md 업데이트 방식:
- TDD / TLD 모두 동일한 프롬프트(UPDATE_GUIDE.md)를 사용
- 업데이트 시점은 하나의 API 개발이 끝날때마다 진행
- 개발 대상:
- 회원가입 API
- 내 정보 조회 API
- 비밀번호 수정 API
- 개발 주체:
- 모든 코드 작성은 AI Agent(Claude Code)
- 개발자는 방향 제시·검증·중단 판단만 수행
즉, “테스트를 언제 작성하느냐”만 다르고, 나머지는 최대한 동일한 환경에서 실험을 진행했다.
1. 자동 TDD 실험 진행 방식
자동 TDD에서는 AI Agent가 테스트 코드를 먼저 작성하고, 그 테스트를 통과하도록 기능을 구현하는 흐름으로 진행했다.
- 요구사항 문서를 제공하고, “요구사항.md”와 "CLAUDE.md"를 참고해서 특정 API를 개발하라는 프롬프트만 입력
- AI Agent가
- 테스트 시나리오 정리
- 테스트 코드 작성
- 테스트 실패 로그를 기반으로
- 비즈니스 로직 구현
- 리팩토링 반복
- 모든 테스트가 통과하면 해당 API를 완료로 간주
- 이후 UPDATE_GUIDE.md 를 통해 CLAUDE.md를 업데이트
2. 자동 TLD 실험 진행 방식
자동 TLD에서는 반대로, 기능 구현을 먼저 끝낸 뒤 테스트를 작성하는 흐름으로 진행했다.
- 마찬가지로 요구사항 문서를 제공하고, “요구사항.md”와 "CLAUDE.md"를 참고해서 특정 API를 개발하라는 프롬프트만 입력
- AI Agent가
- API 구현
- 도메인/서비스 로직 구성
- 기능 구현이 완료된 이후에
- 자동으로 테스트 코드 작성
- 테스트 실패 → 수정 → 재실행 과정을 반복
- 이후 UPDATE_GUIDE.md 를 통해 CLAUDE.md를 업데이트
해당 실험에서 절대로 직접 소스코드 코드 자체를 수정하지 않음으로, “AI에게 전적으로 맡긴 TDD/TLD” 를 보장했다.
평가
3개의 API에 대해 TDD, TLD로 구현한 이후, 다음의 CheckList를 만들어 Codex로 검증을 진행했다.
평가 기준
평가 기준은 다음과 같다.
- 목적 / 방향
- 목적: “요구한 결과가 정확히 완성됐는지”만 판정
- 방향: 구현 스타일이 아니라 관찰 가능한 결과(API 계약·동작·에러·노출 여부) 중심
- 채점 방식 (게이트)
- 게이트형: 충족(1) / 미충족(0)만 허용(부분점수 없음)
- PASS 조건: 45/45 올패스만 PASS, 하나라도 0이면 FAIL
- 범위 고정: 체크리스트 45개 항목이 평가 범위/기준 자체
- 판정 규칙 (일관성 장치)
- 복합 조건: 한 항목이 A+B+C면 전부 충족해야 1
- 존재/금지 규칙
- “필수 존재” 누락 시 반드시 실패해야 충족
- “금지”는 실제로 차단돼야 충족
- “포함 금지”는 응답/저장소/로그 등 모든 노출 경로에서 없어야 충족
- 증거 채택 기준 (근거 표준화)
- 구현 증거 우선: Controller→Service/Facade→Domain→Repo/Entity 실행 경로로 확인
- 불인정: 문구/주석/테스트 이름만으로는 충족 인정 X
- API 계약 우선: 요청/헤더/응답 필드/에러 구조가 요구와 다르면 즉시 미충족
- 에러 일관성: 상태코드 + 에러 응답 스키마가 일관돼야 충족
- 테스트 평가 운영
- 기능 섹션: 테스트는 보조 증거로만 참고
- 테스트 섹션: 테스트 코드 자체를 독립 채점(T1~T14)
- 요구 범위 미달 시 미충족: 성공만 있고 필요한 실패 케이스/핵심 assert 없으면 0
- 리포트 규격 (재현성)
- 항목별로 ID / 결과(1|0) / 근거 파일:라인 / 판정 사유 기록
- 섹션 합계·총점과 함께 최종 PASS/FAIL 명시
CHECKLIST.md
# 최종 체크리스트 및 게이트형 채점 기준 (v2)
## 1. 평가 철학
- 평가는 게이트형으로만 수행한다: `충족(1)` 또는 `미충족(0)`.
- 부분충족(0.5)은 허용하지 않는다.
- 체크리스트 한 줄이 복합 조건이면 모두 만족해야 `1`.
- 구현이 없거나 반대 동작이거나 증거가 부족하면 `0`.
- 목적은 "원하는 결과가 정확히 완성되었는지" 판정하는 것이다.
## 2. 점수 구조
- 평가 단위: 체크박스 `45개`.
- 항목당 점수: `100 / 45 = 2.222...점`.
- 섹션별 배점:
- 공통(3개): `6.67점`
- 회원가입(14개): `31.11점`
- 내 정보 조회(7개): `15.56점`
- 비밀번호 수정(7개): `15.56점`
- 테스트(14개): `31.11점`
## 3. 점수 계산 및 게이트 판정
- `finalScore = (통과 항목 수 / 45) * 100`
- 최종 제시 점수는 마지막에 반올림한다.
- 게이트 결과:
- `PASS`: `45/45` 전부 통과
- `FAIL`: 하나라도 미통과
## 4. 증거 채택 규칙
- 구현 증거 우선: `Controller -> Service/Facade -> Domain -> Repository/Entity` 실행 경로를 본다.
- 테스트는 기능 섹션에서 보조 증거로만 사용한다.
- 테스트 섹션은 테스트 코드 자체를 독립적으로 채점한다.
- 문구/주석/테스트 이름만으로는 충족 인정하지 않는다.
- API 계약(요청/헤더/응답 필드명/에러 구조)이 요구와 다르면 미충족이다.
- 에러 처리는 상태코드와 응답 스키마가 일관되어야 충족이다.
## 5. 항목 판정 규칙
- 각 항목은 아래 체크박스 그대로 `1/0`으로 판정한다.
- 한 항목 안에 "A + B + C"가 있으면 A/B/C 모두 필요하다.
- "존재" 요구는 누락 시 반드시 실패해야 충족이다.
- "금지" 요구는 금지 입력이 실제로 차단되어야 충족이다.
- "포함 금지" 요구는 응답/저장소/로그 등 노출 경로 전반에서 없어야 충족이다.
## 6. 게이트 체크리스트 (총 45개)
### 6.1 공통 (C1-C3)
- [ ] `C1` loginId, name, email은 `trim()` 후 빈 값이면 실패 처리
- [ ] `C2` password는 `trim()`하지 않고, 공백/개행/제어문자 포함 시 실패 처리
- [ ] `C3` 필수 필드 누락/형식 오류에 대해 일관된 에러 응답
### 6.2 회원가입 (S1-S14)
- [ ] `S1` 요청 바디에 `loginId`, `password`, `name`, `birthDate`, `email` 존재
- [ ] `S2` loginId는 `trim()+소문자 정규화` 후 영문/숫자만 허용
- [ ] `S3` loginId 길이 `4~20` 검증
- [ ] `S4` loginId 중복 불가(정규화 기준)
- [ ] `S5` name은 한글/영문/공백만 허용, 길이 `1~50`
- [ ] `S6` birthDate는 `yyyy-MM-dd` 파싱 가능, 미래 날짜 금지
- [ ] `S7` email 길이 `<= 254`, 기본 이메일 형식 검증
- [ ] `S8` email 공백/제어문자 포함 시 실패
- [ ] `S9` password 길이 `8~16` 검증
- [ ] `S10` password 허용 문자: 영문 대/소문자, 숫자, ASCII 특수문자만
- [ ] `S11` password 공백/개행 포함 시 실패
- [ ] `S12` password에 생년월일 포함 금지(`YYYYMMDD`, `YYYY-MM-DD` 모두 검사)
- [ ] `S13` 비밀번호 해시 저장
- [ ] `S14` 응답에 비밀번호/해시 포함 금지
### 6.3 내 정보 조회 (M1-M7)
- [ ] `M1` 요청 헤더 `X-Loopers-LoginId`, `X-Loopers-LoginPw` 존재
- [ ] `M2` 헤더 값 `null/blank` 검사
- [ ] `M3` loginId는 `trim()+소문자 정규화` 후 조회
- [ ] `M4` 헤더 비밀번호와 해시 비교로 인증
- [ ] `M5` 반환 필드는 `loginId`, `name`, `birthDate`, `email`만 포함
- [ ] `M6` 이름 마스킹: 마지막 글자 `*`
- [ ] `M7` 1글자 이름은 `*`로 반환
### 6.4 비밀번호 수정 (P1-P7)
- [ ] `P1` 헤더 인증 선행 (`X-Loopers-LoginId`, `X-Loopers-LoginPw`)
- [ ] `P2` 요청 바디에 `currentPassword`, `newPassword` 존재
- [ ] `P3` currentPassword가 현재 비밀번호와 일치
- [ ] `P4` newPassword가 현재 비밀번호와 동일하면 실패
- [ ] `P5` newPassword는 회원가입과 동일한 비밀번호 규칙 적용
- [ ] `P6` 새 비밀번호 해시 저장
- [ ] `P7` 응답에 비밀번호/해시 포함 금지
### 6.5 테스트 (T1-T14)
#### 단위 테스트 (T1-T7)
- [ ] `T1` loginId 정규화 및 형식/길이 검증
- [ ] `T2` name 허용 문자/길이 검증
- [ ] `T3` birthDate 포맷/미래 날짜 금지
- [ ] `T4` email 형식/길이/공백·제어문자 실패
- [ ] `T5` password 길이/허용 문자/공백 금지
- [ ] `T6` 생년월일 포함 금지(`YYYYMMDD`, `YYYY-MM-DD`)
- [ ] `T7` 이름 마스킹(1글자/다글자)
#### 통합 테스트 (T8-T11)
- [ ] `T8` 회원가입 성공 및 해시 저장 확인
- [ ] `T9` loginId 중복 실패
- [ ] `T10` 내 정보 조회 성공/인증 실패
- [ ] `T11` 비밀번호 변경 성공/실패(불일치/동일/규칙 위반)
#### E2E 테스트 (T12-T14)
- [ ] `T12` 회원가입 정상/실패
- [ ] `T13` 내 정보 조회 정상/실패
- [ ] `T14` 비밀번호 변경 정상/실패
## 7. 테스트 섹션 운영 규칙
- 테스트가 존재해도 핵심 검증 포인트가 빠지면 미충족이다.
- 성공 케이스만 있고 요구된 실패 케이스가 없으면 미충족이다.
- 가능하면 테스트 실행 결과로 검증한다.
- 테스트 이름이 아니라 assert 내용과 검증 범위를 기준으로 판정한다.
## 8. 리포트 작성 형식 (평가 시)
- 항목별로 `ID / 결과(1|0) / 근거 파일:라인 / 판정 사유`를 기록한다.
- 섹션 합계와 총점을 함께 제시한다.
- 마지막에 게이트 결과를 `PASS` 또는 `FAIL`로 명시한다.
평가 진행
다음의 프롬프트를 입력하여, 세션마다 주관을 최대한 배제하고, CHECKLIST를 바탕으로 평가하도록 요청.
**목표**
- CHECKLIST.md 를 바탕으로 apps/commerce-api/src/main/java/com/loopers/ 를 평가
- 100점 만점 기준으로 몇점인지 확인
**주의사항**
- 냉정하고 합리적으로 판단
- 추가적인 개인판단 없이 무조건 CHECKLIST.md를 바탕으로 정량적인 평가 진행 할 것
**요구사항**
- 목표와 주의사항을 보고 plan을 먼저 세우고, 진행해줘.
결과
각 검사 결과는, CHECKLIST.md에 의해 평가된 검사 결과이다.
Auto - TDD 구현 결과
요약
### 점수 요약
- 공통: `1/3` → `2.22점`
- 회원가입: `6/14` → `13.33점`
- 내 정보 조회: `5/7` → `11.11점`
- 비밀번호 수정: `7/7` → `15.56점`
- 테스트: `5/14` → `11.11점`
- 총 통과: `24/45`
- 계산 점수: `53.33`
- 최종 점수(반올림): **53점 / 100점**
- 게이트: **FAIL**
상세 결과
# 첫번째 검사 결과
### 공통 (C1-C3)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| C1 | 0 | `trim()` 후 빈값 처리 규칙이 없음 (`isBlank()`만 적용). |
| C2 | 0 | 비밀번호 공백/개행/제어문자 금지 검증이 없음. |
| C3 | 1 | 필수 누락/형식 오류에 대해 공통 에러 스키마(`code`,`message`)로 응답. |
### 회원가입 (S1-S14)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| S1 | 0 | 요구 필드명 `birthDate`가 아니라 `birthday` 사용. |
| S2 | 0 | `trim()+소문자 정규화` 없음. |
| S3 | 0 | 길이 `4~20` 중 최소 길이 4 검증 없음. |
| S4 | 0 | 정규화 기준 중복 체크가 아님. |
| S5 | 0 | 이름 공백 허용 없음, 길이도 `1~50`이 아니라 최대 `100`. |
| S6 | 1 | 날짜 파싱(LocalDate) 및 미래 날짜 금지 존재. |
| S7 | 1 | 이메일 길이 `<=254` 및 형식 검증 존재. |
| S8 | 1 | 이메일 내 공백/제어문자 입력은 형식 검증에서 실패 처리됨. |
| S9 | 1 | 비밀번호 길이 `8~16` 검증 존재. |
| S10 | 0 | 허용 문자 “only” 제한 검증 없음. |
| S11 | 0 | 공백/개행 포함 금지 검증 없음. |
| S12 | 0 | `YYYYMMDD`/`YYMMDD`만 검사, `YYYY-MM-DD` 검사는 없음. |
| S13 | 1 | 비밀번호 해시 저장 구현됨. |
| S14 | 1 | 응답에 비밀번호/해시 미포함. |
### 내 정보 조회 (M1-M7)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| M1 | 1 | 요구 인증 헤더 2개를 사용하는 경로 존재. |
| M2 | 1 | 헤더 `null/blank` 검사 존재. |
| M3 | 0 | `trim()`만 있고 소문자 정규화 없음. |
| M4 | 1 | 헤더 비밀번호를 해시 비교로 인증. |
| M5 | 0 | 요구 필드명 `birthDate`가 아니라 `birthday` 사용. |
| M6 | 1 | 마지막 글자 `*` 마스킹 구현. |
| M7 | 1 | 1글자 이름 `*` 반환 구현. |
### 비밀번호 수정 (P1-P7)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| P1 | 1 | 헤더 인증 선행. |
| P2 | 1 | `currentPassword`, `newPassword` 존재/필수 검증. |
| P3 | 1 | 현재 비밀번호 일치 검증 존재. |
| P4 | 1 | 신규 비밀번호가 현재와 같으면 실패 처리. |
| P5 | 1 | 회원가입과 동일 비밀번호 규칙 재사용. |
| P6 | 1 | 새 비밀번호 해시 저장 구현. |
| P7 | 1 | 응답에 비밀번호/해시 미포함. |
### 테스트 (T1-T14)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| T1 | 0 | loginId 정규화(`trim+소문자`) 테스트가 없음. |
| T2 | 1 | 이름 허용 문자/길이 검증 테스트 존재. |
| T3 | 0 | 미래 날짜 금지는 있으나 “포맷” 검증 테스트 없음. |
| T4 | 0 | 이메일 형식/길이는 있으나 공백·제어문자 실패 검증이 부족. |
| T5 | 0 | 비밀번호 길이/문자조합은 있으나 공백 금지 테스트 없음. |
| T6 | 0 | `YYYYMMDD`는 있으나 `YYYY-MM-DD` 테스트 없음. |
| T7 | 1 | 이름 마스킹(1글자/다글자) 테스트 존재. |
| T8 | 0 | 통합 테스트에서 “회원가입 성공 + 해시 저장 확인”이 직접적으로 없음. |
| T9 | 0 | loginId 중복 실패 통합 테스트 없음. |
| T10 | 0 | 내 정보 조회 성공/인증 실패 통합 테스트 없음. |
| T11 | 0 | 비밀번호 변경 성공/실패 통합 테스트 없음. |
| T12 | 1 | 회원가입 E2E 정상/실패 존재. |
| T13 | 1 | 내 정보 조회 E2E 정상/실패 존재. |
| T14 | 1 | 비밀번호 변경 E2E 정상/실패 존재. |
### 점수 요약
- 공통: `1/3` → `2.22점`
- 회원가입: `6/14` → `13.33점`
- 내 정보 조회: `5/7` → `11.11점`
- 비밀번호 수정: `7/7` → `15.56점`
- 테스트: `5/14` → `11.11점`
- 총 통과: `24/45`
- 계산 점수: `53.33`
- 최종 점수(반올림): **53점 / 100점**
- 게이트: **FAIL**
Auto - TLD 구현 결과
요약
## 총점/게이트
- 통과 항목: `31/45`
- 최종 점수: **69/100** (반올림)
- 게이트 결과: **FAIL** (`45/45` 미충족)
## 섹션 합계
- 공통: `2/3` (`4.44점`)
- 회원가입: `8/14` (`17.78점`)
- 내 정보 조회: `5/7` (`11.11점`)
- 비밀번호 수정: `7/7` (`15.56점`)
- 테스트: `9/14` (`20.00점`)
상세 결과
# 첫번째 검사 결과
## 총점/게이트
- 통과 항목: `31/45`
- 최종 점수: **69/100** (반올림)
- 게이트 결과: **FAIL** (`45/45` 미충족)
## 섹션 합계
- 공통: `2/3` (`4.44점`)
- 회원가입: `8/14` (`17.78점`)
- 내 정보 조회: `5/7` (`11.11점`)
- 비밀번호 수정: `7/7` (`15.56점`)
- 테스트: `9/14` (`20.00점`)
## 항목별 결과
### 6.1 공통
| ID | 결과 | 판정 사유 |
|---|---:|---|
| C1 | 1 | loginId/name/email blank 입력 실패 처리 존재 |
| C2 | 0 | password 공백/개행/제어문자 “포함” 차단 검증 부재 |
| C3 | 1 | 필수값/형식 오류 시 일관 에러 스키마 응답 |
### 6.2 회원가입
| ID | 결과 | 판정 사유 |
|---|---:|---|
| S1 | 1 | 요청 바디 필수 5필드 존재 |
| S2 | 0 | loginId `trim()+소문자 정규화` 미적용 |
| S3 | 0 | loginId 최소 길이 4 검증 없음(최대만 존재) |
| S4 | 0 | 정규화 기준 중복검사 아님 |
| S5 | 0 | name 규칙이 요구(1~50, 공백 허용)와 불일치 |
| S6 | 1 | 날짜 파싱 가능 + 미래 날짜 금지 충족 |
| S7 | 1 | email 길이/형식 검증 충족 |
| S8 | 1 | email 공백/제어문자 차단 충족 |
| S9 | 1 | password 8~16 길이 검증 충족 |
| S10 | 0 | password 허용문자 “only” 제한 검증 없음 |
| S11 | 0 | password 공백/개행 포함 금지 검증 없음 |
| S12 | 1 | 생년월일 포함 금지(`YYYYMMDD`, `YYYY-MM-DD`) 충족 |
| S13 | 1 | password 해시 저장 충족 |
| S14 | 1 | 응답에 password/hash 미포함 충족 |
### 6.3 내 정보 조회
| ID | 결과 | 판정 사유 |
|---|---:|---|
| M1 | 1 | 요구 헤더 2개 사용 |
| M2 | 1 | 헤더 null/blank 검사 존재 |
| M3 | 0 | loginId 정규화(trim+소문자) 없이 조회 |
| M4 | 1 | 헤더 비밀번호와 저장 해시 비교 인증 |
| M5 | 0 | 반환 필드명이 요구 `birthDate`와 다름(`birthday`) |
| M6 | 1 | 마지막 글자 `*` 마스킹 충족 |
| M7 | 1 | 1글자 이름 `*` 반환 충족 |
### 6.4 비밀번호 수정
| ID | 결과 | 판정 사유 |
|---|---:|---|
| P1 | 1 | 헤더 인증 선행 충족 |
| P2 | 1 | `currentPassword`, `newPassword` 존재/검증 |
| P3 | 1 | currentPassword 일치 검증 충족 |
| P4 | 1 | 새 비밀번호 동일값 금지 충족 |
| P5 | 1 | 회원가입과 동일 비밀번호 규칙 적용 |
| P6 | 1 | 새 비밀번호 해시 저장 충족 |
| P7 | 1 | 응답에 password/hash 미포함 충족 |
### 6.5 테스트
| ID | 결과 | 판정 사유 |
|---|---:|---|
| T1 | 0 | loginId 정규화(trim+소문자) 테스트 부재 |
| T2 | 0 | name 규칙 테스트가 체크리스트 조건과 불일치 |
| T3 | 0 | 미래 날짜 금지는 있으나 포맷 파싱 테스트 부재 |
| T4 | 1 | email 형식/길이/공백 실패 테스트 존재 |
| T5 | 0 | password 공백 금지/허용문자 범위 테스트 부족 |
| T6 | 1 | 생년월일 포함 금지 테스트 존재 |
| T7 | 1 | 이름 마스킹(1글자/다글자) 테스트 존재 |
| T8 | 0 | 회원가입 성공은 있으나 “해시 저장 확인” assert 부족 |
| T9 | 1 | loginId 중복 실패 테스트 존재 |
| T10 | 1 | 내 정보 조회 성공/인증 실패 테스트 존재 |
| T11 | 1 | 비밀번호 변경 성공/불일치/동일/규칙위반 테스트 존재 |
| T12 | 1 | 회원가입 정상/실패 시나리오 존재 |
| T13 | 1 | 내 정보 조회 정상/실패 시나리오 존재 |
| T14 | 1 | 비밀번호 변경 정상/실패 시나리오 존재 |
직접 개발을 진행할때는, plan 모드를 기준으로 TDD가 판단을 위해 질문하는 경우가 훨씬 많았지만, 그만큼 요구사항에 대해 정확하게 이해하고 진행하고 있다는 느낌을 받았다.
하지만 직접 평가해본 결과 53점(TDD) vs 69점(TLD)로 TLD가 훨씬 괜찮은 모습을 보여주고있다는 것을 알 수 있다.
그러나 우리 목표는 Full-Agent 개발을 통해 요구사항을 정확하게 충족시키는 것이다. 따라서 상대점수보다는, 근본적으로 해당 결과들의 절대 점수가 어느정도 충족되어야만 한다. 따라서 절대 점수가 낮은 원인을 찾아야하므로, '요구사항.md'와 '체크리스트.md'의 일치율을 우선적으로 비교해보겠다.
'요구사항 문서'와 '체크리스트 평가 문서' 비교 결과
결과
- 전제조건 충족도: 25.0 / 31 = 80.6점
- 엄격 기준(부분점수 없이 1/0): 23 / 31 = 74.2점
- 결론: 요구사항만으로 체크리스트 통과 전제조건이 “전부” 갖춰져 있지는 않습니다.
주요 누락/불일치 (통과 리스크 큰 순서)
- birthDate vs birthday 필드명 불일치
CHECKLIST.md:57, CHECKLIST.md:78 ↔ REQUIREMENTS.md:39, REQUIREMENTS.md:127
- 내 정보 조회에서 헤더 null/blank 검증 명시 부족 (M2)
CHECKLIST.md:75 ↔ REQUIREMENTS.md:117
- 내 정보 조회에서 loginId trim+lowercase 후 조회 명시 부족 (M3)
CHECKLIST.md:76 ↔ REQUIREMENTS.md:119
- 내 정보 조회 인증 방식(헤더 비밀번호 vs 저장 해시 비교) 명시 부족 (M4)
CHECKLIST.md:77 ↔ REQUIREMENTS.md:117
- 내 정보 조회 응답 “필드만 포함(only)” + birthDate 키 계약 미명시 (M5)
CHECKLIST.md:78 ↔ REQUIREMENTS.md:123
- 비밀번호 수정의 “헤더 인증 선행”은 의도는 있으나 판정 가능한 수준으로 구체화 부족 (P1)
CHECKLIST.md:84 ↔ REQUIREMENTS.md:143
- 비밀번호 수정 응답에서 비밀번호/해시 미포함 규칙 미명시 (P7)
CHECKLIST.md:90 ↔ REQUIREMENTS.md:141
결론부터 말하면, 초기 CHECKLIST가 REQUIREMENTS를 100% 대변하지 못했고, 그 불일치가 “TDD vs TLD” 결론까지 뒤집어버렸다. 이로 인해 절대적인 점수가 낮게나왔으며, 조금 더 요구사항을 정확하게 구현한다는 느낌을 받은 TDD에서 더 낮은 점수가 나온 것으로 해석할 수 있다.
따라서 정확한 평가를 위해 철저하게 요구사항 바탕으로 CHECKLIST를 새로 생성하고, 해당 버전으로 다시 평가를 진행해보겠다.
아래의 재평가를 통해 'TDD가 TLD보다 요구사항을 더 정확하게 구현한다'라는 가정을 다시 한번 확인해보자.
재평가 결과
CHECKLIST_REQUIREMENTS_ONLY.md
# REQUIREMENTS 기반 최종 체크리스트 및 게이트형 채점 기준 (v1)
## 1. 평가 철학
- 평가는 게이트형으로만 수행한다: `충족(1)` 또는 `미충족(0)`.
- 부분충족(0.5)은 허용하지 않는다.
- 체크리스트 한 줄이 복합 조건이면 모두 만족해야 `1`.
- 구현이 없거나 반대 동작이거나 증거가 부족하면 `0`.
- 목적은 `REQUIREMENTS.md`에 명시된 요구사항 충족 여부만 판정하는 것이다.
## 2. 점수 구조
- 평가 단위: 체크박스 `66개`.
- 항목당 점수: `100 / 66 = 1.515151...점`.
- 섹션별 배점(반올림):
- 공통(6개): `9.09점`
- 회원가입(34개): `51.52점`
- 내 정보 조회(7개): `10.61점`
- 비밀번호 수정(8개): `12.12점`
- ErrorType(11개): `16.67점`
## 3. 점수 계산 및 게이트 판정
- `finalScore = (통과 항목 수 / 66) * 100`
- 최종 제시 점수는 마지막에 반올림한다.
- 게이트 결과:
- `PASS`: `66/66` 전부 통과
- `FAIL`: 하나라도 미통과
## 4. 증거 채택 규칙
- 구현 증거 우선: `Controller -> Service/Facade -> Domain -> Repository/Entity` 실행 경로를 본다.
- 문구/주석/테스트 이름만으로는 충족 인정하지 않는다.
- API 계약(요청/헤더/응답 필드명/에러 구조)이 요구와 다르면 미충족이다.
- 에러 처리는 상태코드와 응답 스키마가 요구사항과 일치해야 충족이다.
## 5. 항목 판정 규칙
- 각 항목은 아래 체크박스 그대로 `1/0`으로 판정한다.
- 한 항목 안에 `A + B + C`가 있으면 A/B/C 모두 필요하다.
- `존재` 요구는 누락 시 반드시 실패해야 충족이다.
- `금지` 요구는 금지 입력이 실제로 차단되어야 충족이다.
- `포함 금지` 요구는 응답/저장소/로그 등 노출 경로 전반에서 없어야 충족이다.
## 6. 게이트 체크리스트 (총 66개)
### 6.1 공통 (C1-C6)
- [ ] `C1` 사용자 정보가 필요한 요청에는 `X-Loopers-LoginId`, `X-Loopers-LoginPw` 헤더가 모두 존재
- [ ] `C2` `loginId`, `name`, `email`은 `trim()` 후 검증하며 결과가 빈 문자열이면 실패
- [ ] `C3` `password`는 `trim()` 금지, 공백/개행/제어문자 포함 시 실패
- [ ] `C4` 모든 비즈니스 예외는 `CoreException + ErrorType` 조합으로 처리
- [ ] `C5` 예외 처리 흐름이 `throw CoreException(ErrorType.XXX) -> GlobalExceptionHandler -> ErrorResponse(code, message)`를 충족
- [ ] `C6` `@Valid` 검증 실패는 `BAD_REQUEST(400)`로 반환되며 에러 응답 필드에 `code`, `message`가 존재
### 6.2 회원가입 (S1-S34)
- [ ] `S1` `POST /api/v1/users` 엔드포인트 존재
- [ ] `S2` 요청 필드 `loginId`, `password`, `name`, `birthday`, `email`가 모두 필수
- [ ] `S3` 성공 시 `201 Created` 반환
- [ ] `S4` 성공 응답 필드 `id`, `loginId`, `name`, `birthday`, `email` 반환
- [ ] `S5` 성공 응답에 비밀번호/해시를 포함하지 않음
- [ ] `S6` `loginId` 허용 문자: 영문 대소문자 + 숫자만(`^[a-zA-Z0-9]+$`)
- [ ] `S7` `loginId` 전처리: `trim()` 후 소문자 정규화
- [ ] `S8` `loginId` 길이 `4~20`
- [ ] `S9` `loginId` 중복 검사는 정규화된 값(trim + lowercase) 기준
- [ ] `S10` `loginId` 형식 위반 시 `INVALID_LOGIN_ID_FORMAT(400)` 처리
- [ ] `S11` `loginId` 중복 시 `USER_ALREADY_EXISTS(409)` 처리
- [ ] `S12` `password` 길이 `8~16`
- [ ] `S13` `password` 필수 포함: 영문 대문자/소문자/숫자/ASCII 특수문자 각 1개 이상
- [ ] `S14` `password` 허용 문자: 영문 대소문자 + 숫자 + ASCII 특수문자만
- [ ] `S15` `password` 공백/개행/제어문자 금지
- [ ] `S16` `password`는 `trim()` 적용 금지
- [ ] `S17` `password`에 생년월일 포함 금지(`YYYYMMDD`, `YYYY-MM-DD`)
- [ ] `S18` `password` 저장 시 해시 사용(`SHA-256 + Base64`)
- [ ] `S19` `password` 형식 위반 시 `INVALID_PASSWORD_FORMAT(400)` 처리
- [ ] `S20` `password` 생년월일 포함 시 `PASSWORD_CONTAINS_BIRTHDAY(400)` 처리
- [ ] `S21` `name` 허용 문자: 한글, 영문, 공백
- [ ] `S22` `name`은 `trim()` 적용 후 빈 문자열이면 실패
- [ ] `S23` `name` 길이 `1~50`
- [ ] `S24` `name` 형식 위반 시 `INVALID_NAME_FORMAT(400)` 처리
- [ ] `S25` `birthday` 형식은 `yyyy-MM-dd`(ISO-8601)
- [ ] `S26` `birthday` 범위는 `1900-01-01` 이후, 오늘 이전
- [ ] `S27` `birthday` 위반 시 `INVALID_BIRTHDAY(400)` 처리
- [ ] `S28` `email` 최대 길이 `254`
- [ ] `S29` `email` 형식은 `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$`
- [ ] `S30` `email` 공백/제어문자 금지
- [ ] `S31` `email` 도메인 추가 규칙: 연속 점(`..`) 금지, 시작/끝 점 금지
- [ ] `S32` `email`은 `trim()` 적용 후 빈 문자열이면 실패
- [ ] `S33` `email` 형식 위반 시 `INVALID_EMAIL_FORMAT(400)` 처리
- [ ] `S34` 회원가입 필수 필드 누락 시 `BAD_REQUEST(400)` 반환
### 6.3 내 정보 조회 (M1-M7)
- [ ] `M1` `GET /api/v1/users/me` 엔드포인트 존재
- [ ] `M2` 요청 헤더 `X-Loopers-LoginId`, `X-Loopers-LoginPw`가 모두 필수
- [ ] `M3` 응답 필드 `loginId`, `name`, `birthday`, `email` 반환
- [ ] `M4` 응답에 비밀번호/해시를 포함하지 않음
- [ ] `M5` `name` 필드에 마스킹 규칙이 적용됨
- [ ] `M6` 이름 길이 2자 이상: 마지막 글자를 `*`로 대체
- [ ] `M7` 이름 길이 1자: 전체를 `*`로 대체
### 6.4 비밀번호 수정 (P1-P8)
- [ ] `P1` `PATCH /api/v1/users/me/password` 엔드포인트 존재
- [ ] `P2` 요청 헤더 `X-Loopers-LoginId`, `X-Loopers-LoginPw`가 모두 필수
- [ ] `P3` 요청 필드 `currentPassword`, `newPassword`가 모두 필수
- [ ] `P4` `currentPassword`는 저장된 해시와 일치해야 함
- [ ] `P5` `newPassword`는 `currentPassword`와 동일하면 실패
- [ ] `P6` `newPassword`는 회원가입과 동일한 비밀번호 규칙(길이/문자조합/공백·개행·제어문자 금지/생년월일 포함 금지)을 적용
- [ ] `P7` 새 비밀번호는 해시로 저장
- [ ] `P8` 비밀번호 필드(`currentPassword`, `newPassword`)는 `trim()` 적용 금지
### 6.5 ErrorType 정리 (E1-E11)
- [ ] `E1` `INTERNAL_ERROR` = `500 / Internal Server Error / 일시적인 오류가 발생했습니다.`
- [ ] `E2` `BAD_REQUEST` = `400 / Bad Request / 잘못된 요청입니다.`
- [ ] `E3` `NOT_FOUND` = `404 / Not Found / 존재하지 않는 요청입니다.`
- [ ] `E4` `CONFLICT` = `409 / Conflict / 이미 존재하는 리소스입니다.`
- [ ] `E5` `USER_ALREADY_EXISTS` = `409 / USER_ALREADY_EXISTS / 이미 가입된 로그인 ID입니다.`
- [ ] `E6` `INVALID_PASSWORD_FORMAT` = `400 / INVALID_PASSWORD_FORMAT / 비밀번호는 8~16자이며, 영문 대소문자, 숫자, 특수문자를 모두 포함해야 합니다.`
- [ ] `E7` `PASSWORD_CONTAINS_BIRTHDAY` = `400 / PASSWORD_CONTAINS_BIRTHDAY / 비밀번호에 생년월일을 포함할 수 없습니다.`
- [ ] `E8` `INVALID_LOGIN_ID_FORMAT` = `400 / INVALID_LOGIN_ID_FORMAT / 로그인 ID는 영문과 숫자만 사용 가능하며, 4~20자여야 합니다.`
- [ ] `E9` `INVALID_NAME_FORMAT` = `400 / INVALID_NAME_FORMAT / 이름은 한글, 영문, 공백만 사용 가능하며, 최대 50자입니다.`
- [ ] `E10` `INVALID_EMAIL_FORMAT` = `400 / INVALID_EMAIL_FORMAT / 올바른 이메일 형식이 아닙니다.`
- [ ] `E11` `INVALID_BIRTHDAY` = `400 / INVALID_BIRTHDAY / 생년월일은 1900-01-01 이후, 오늘 이전이어야 합니다.`
## 7. 섹션 운영 규칙
- `ErrorType` 항목은 `HTTP Status + Code + Message` 3요소가 모두 일치해야 충족이다.
- 회원가입/내 정보 조회/비밀번호 수정 항목은 각 API 단위로 독립 채점한다.
- 공통 규칙 위반은 관련 API 섹션 항목과 별개로 공통 섹션에서 추가 감점한다.
## 8. 리포트 작성 형식 (평가 시)
- 항목별로 `ID / 결과(1|0) / 근거 파일:라인 / 판정 사유`를 기록한다.
- 섹션 합계와 총점을 함께 제시한다.
- 마지막에 게이트 결과를 `PASS` 또는 `FAIL`로 명시한다.
'요구사항 기반 CHECKLIST'와 '요구사항' 일치율 결과
• 정량 재평가 결과입니다.
- 기준: CHECKLIST_REQUIREMENTS_ONLY.md의 체크박스 66개를 REQUIREMENTS.md에 역매핑
- 총 일치도: 100.0% (66/66)
- 섹션별: 공통 6/6, 회원가입 34/34, 내 정보 조회 7/7, 비밀번호 수정 8/8, ErrorType 11/11
- 체크리스트 과잉(요구사항에 없는 신규 조건): 0개
- 요구사항 누락(체크리스트에 반영 안 된 조건): 0개
Auto - TDD 구현 재평가 결과
요약
섹션 합계
| 섹션 | 통과/전체 | 섹션 점수 |
|---|---:|---:|
| 공통 | 4/6 | 6.06 |
| 회원가입 | 21/34 | 31.82 |
| 내 정보 조회 | 7/7 | 10.61 |
| 비밀번호 수정 | 7/8 | 10.61 |
| ErrorType | 9/11 | 13.64 |
최종
- 통과 항목: 48/66
- 계산식: (48/66) * 100 = 72.727...
- 최종 점수(반올림): 73점 / 100점
- 게이트 결과: FAIL
상세 결과
# CHECKLIST_REQUIREMENTS_ONLY 기준 평가 결과
## 평가 기준
- `CHECKLIST_REQUIREMENTS_ONLY.md` (총 66개, 항목당 1.515151점)
- 판정 방식: 각 항목 `1/0`
## 6.1 공통 (C1-C6)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| C1 | 1 | 사용자 정보 API에서 두 헤더를 모두 검증 |
| C2 | 0 | `loginId/name/email`에 `trim()` 전처리 로직 없음 |
| C3 | 0 | `password` 공백/개행/제어문자 금지 검사 누락 |
| C4 | 1 | 비즈니스 검증 경로에서 `CoreException + ErrorType` 사용 |
| C5 | 1 | `CoreException -> GlobalExceptionHandler -> ErrorResponse(code,message)` 충족 |
| C6 | 1 | `@Valid` 실패를 400으로 반환, 응답에 `code/message` 포함 |
## 6.2 회원가입 (S1-S34)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| S1 | 1 | `POST /api/v1/users` 존재 |
| S2 | 1 | 5개 필드 모두 필수 검증 |
| S3 | 1 | 성공 시 201 반환 |
| S4 | 1 | 응답 필드 `id/loginId/name/birthday/email` 반환 |
| S5 | 1 | 응답에 password/hash 미포함 |
| S6 | 1 | `loginId` 영문/숫자 패턴 적용 |
| S7 | 0 | `trim()+lowercase` 정규화 누락 |
| S8 | 0 | 길이 하한 4 검증 없음(최대 20만 검증) |
| S9 | 0 | 중복 검사가 정규화 기준(trim+lowercase) 아님 |
| S10 | 1 | 형식 위반 시 `INVALID_LOGIN_ID_FORMAT` |
| S11 | 1 | 중복 시 `USER_ALREADY_EXISTS(409)` |
| S12 | 1 | 비밀번호 길이 8~16 검증 |
| S13 | 1 | 대/소문자/숫자/특수문자 포함 검증 |
| S14 | 0 | 허용 문자 “only” 제한(배타 검증) 없음 |
| S15 | 0 | 공백/개행/제어문자 금지 규칙 누락 |
| S16 | 1 | password에 `trim()` 적용 없음 |
| S17 | 0 | `YYYY-MM-DD` 패턴 검사 누락 |
| S18 | 1 | `SHA-256 + Base64` 해시 저장 |
| S19 | 1 | 형식 위반 시 `INVALID_PASSWORD_FORMAT` |
| S20 | 0 | 생년월일 포함 케이스 전체(`YYYYMMDD`,`YYYY-MM-DD`)를 모두 차단하지 못함 |
| S21 | 0 | `name` 허용 문자에 공백 미포함 |
| S22 | 0 | `name` `trim()` 후 검증 로직 없음 |
| S23 | 0 | 길이 상한이 50이 아니라 100 |
| S24 | 1 | 이름 형식 위반 시 `INVALID_NAME_FORMAT` |
| S25 | 1 | `LocalDate`로 `yyyy-MM-dd` 파싱 경로 |
| S26 | 0 | “오늘 이전/1900-01-01 이후” 경계 조건과 불일치(오늘/1900-01-01 허용) |
| S27 | 0 | S26 경계 조건 불일치로 위반 케이스 전체를 `INVALID_BIRTHDAY`로 처리하지 못함 |
| S28 | 1 | 이메일 최대 길이 254 검증 |
| S29 | 1 | 이메일 기본 정규식 검증 |
| S30 | 1 | 정규식 허용 문자 집합으로 공백/제어문자 차단 |
| S31 | 1 | 도메인 `..`, 시작/끝 점 금지 검증 |
| S32 | 0 | 이메일 `trim()` 후 검증 로직 없음 |
| S33 | 1 | 이메일 위반 시 `INVALID_EMAIL_FORMAT` |
| S34 | 1 | 필수 필드 누락 시 400 반환 |
## 6.3 내 정보 조회 (M1-M7)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| M1 | 1 | `GET /api/v1/users/me` 존재 |
| M2 | 1 | 두 헤더 필수 검증 |
| M3 | 1 | 응답 필드 `loginId/name/birthday/email` |
| M4 | 1 | 응답에 password/hash 미포함 |
| M5 | 1 | `name` 마스킹 적용 |
| M6 | 1 | 2자 이상 마지막 글자 `*` 처리 |
| M7 | 1 | 1자 이름 `*` 처리 |
## 6.4 비밀번호 수정 (P1-P8)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| P1 | 1 | `PATCH /api/v1/users/me/password` 존재 |
| P2 | 1 | 헤더 2개 필수 검증 |
| P3 | 1 | `currentPassword/newPassword` 필수 |
| P4 | 1 | 현재 비밀번호 해시 비교 인증 |
| P5 | 1 | 새 비밀번호가 현재와 같으면 실패 |
| P6 | 0 | 회원가입 비밀번호 규칙 중 공백/제어문자 금지, `YYYY-MM-DD` 검사 미충족 |
| P7 | 1 | 새 비밀번호 해시 저장 |
| P8 | 1 | `currentPassword/newPassword`에 `trim()` 미적용 |
## 6.5 ErrorType (E1-E11)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| E1 | 1 | 500 / Internal Server Error / 메시지 일치 |
| E2 | 1 | 400 / Bad Request / 메시지 일치 |
| E3 | 1 | 404 / Not Found / 메시지 일치 |
| E4 | 1 | 409 / Conflict / 메시지 일치 |
| E5 | 1 | 409 / USER_ALREADY_EXISTS / 메시지 일치 |
| E6 | 1 | 400 / INVALID_PASSWORD_FORMAT / 메시지 일치 |
| E7 | 1 | 400 / PASSWORD_CONTAINS_BIRTHDAY / 메시지 일치 |
| E8 | 0 | 메시지가 `4~20`이 아니라 `1~20` |
| E9 | 0 | 메시지가 “공백 허용/최대 50자”와 불일치(현재 100자, 공백 미허용) |
| E10 | 1 | 400 / INVALID_EMAIL_FORMAT / 메시지 일치 |
| E11 | 1 | 400 / INVALID_BIRTHDAY / 메시지 일치 |
## 섹션 합계
| 섹션 | 통과/전체 | 섹션 점수 |
|---|---:|---:|
| 공통 | 4/6 | 6.06 |
| 회원가입 | 21/34 | 31.82 |
| 내 정보 조회 | 7/7 | 10.61 |
| 비밀번호 수정 | 7/8 | 10.61 |
| ErrorType | 9/11 | 13.64 |
## 최종
- 통과 항목: `48/66`
- 계산식: `(48/66) * 100 = 72.727...`
- 최종 점수(반올림): **73점 / 100점**
- 게이트 결과: **FAIL**
Auto - TLD 구현 재평가 결과
요약
섹션 합계
- 공통: 4/6 (6.06점 / 9.09점)
- 회원가입: 13/34 (19.70점 / 51.52점)
- 내 정보 조회: 7/7 (10.61점 / 10.61점)
- 비밀번호 수정: 7/8 (10.61점 / 12.12점)
- ErrorType: 4/11 (6.06점 / 16.67점)
총점 / 게이트
- 통과 항목 수: 35/66
- 계산: (35/66) * 100 = 53.03
- 최종 점수(반올림): 53점 / 100점
- 게이트 결과: FAIL
상세 결과
# CHECKLIST_REQUIREMENTS_ONLY 기반 정량 평가 결과
## 6.1 공통 (C1-C6)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| C1 | 1 | 사용자 정보 필요 요청에서 두 헤더 누락/blank를 차단함 |
| C2 | 0 | `trim()` 정규화 후 검증 흐름이 없음 |
| C3 | 0 | password 공백/개행/제어문자 금지 검증이 없음 |
| C4 | 1 | 비즈니스 예외를 `CoreException + ErrorType`로 처리 |
| C5 | 1 | `CoreException -> GlobalExceptionHandler -> ErrorResponse(code,message)` 흐름 충족 |
| C6 | 1 | `@Valid` 실패 시 400 + `code`,`message` 포함 |
## 6.2 회원가입 (S1-S34)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| S1 | 1 | `POST /api/v1/users` 존재 |
| S2 | 0 | 필드명이 `birthday`가 아니라 `birthDate` |
| S3 | 1 | 성공 시 201 반환 |
| S4 | 0 | 성공 응답이 `id`,`birthday`가 아니라 `userId`이며 `birthday` 없음 |
| S5 | 1 | 성공 응답에 비밀번호/해시 없음 |
| S6 | 1 | loginId 허용 문자(영문/숫자) 검증 존재 |
| S7 | 0 | `trim()+lowercase` 정규화 없음 |
| S8 | 0 | loginId 최소 길이 4 검증 없음(최대만 검증) |
| S9 | 0 | 중복검사가 정규화 기준이 아님 |
| S10 | 0 | `INVALID_LOGIN_ID_FORMAT` 에러타입 처리 없음 |
| S11 | 1 | 중복 시 `USER_ALREADY_EXISTS(409)` |
| S12 | 1 | 비밀번호 8~16 검증 존재 |
| S13 | 1 | 대/소문자/숫자/특수문자 포함 검증 존재 |
| S14 | 0 | 허용 문자 집합 “only” 검증 없음 |
| S15 | 0 | 공백/개행/제어문자 금지 검증 없음 |
| S16 | 1 | password `trim()` 적용 코드 없음 |
| S17 | 1 | `YYYYMMDD`, `YYYY-MM-DD` 포함 금지 구현됨 |
| S18 | 1 | SHA-256 + Base64 해시 저장 |
| S19 | 0 | `INVALID_PASSWORD_FORMAT` 대신 `INVALID_PASSWORD` 사용 |
| S20 | 0 | `PASSWORD_CONTAINS_BIRTHDAY` 대신 `PASSWORD_CONTAINS_BIRTH_DATE` |
| S21 | 0 | name 허용 문자에서 공백 미허용 |
| S22 | 0 | name `trim()` 후 검증 흐름 없음 |
| S23 | 0 | name 최대 50이 아니라 100 |
| S24 | 0 | `INVALID_NAME_FORMAT(400)` 처리 없음 |
| S25 | 0 | 요구 필드 `birthday` 계약 불일치(`birthDate`) |
| S26 | 0 | `1900-01-01` 경계 처리(동일일 허용) + 계약 불일치 |
| S27 | 0 | `INVALID_BIRTHDAY(400)` 처리 없음 |
| S28 | 1 | email 최대 254 충족 |
| S29 | 0 | 요구 정규식과 동일한 검증 미구현 |
| S30 | 1 | email 공백/제어문자 금지 |
| S31 | 0 | 연속 점/시작점/끝점 금지 규칙 없음 |
| S32 | 0 | email `trim()` 후 검증 흐름 없음 |
| S33 | 0 | `INVALID_EMAIL_FORMAT(400)` 처리 없음 |
| S34 | 1 | 필수 필드 누락 시 400 반환 |
## 6.3 내 정보 조회 (M1-M7)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| M1 | 1 | `GET /api/v1/users/me` 존재 |
| M2 | 1 | 두 헤더 필수(누락/blank 차단) |
| M3 | 1 | 응답 필드 `loginId,name,birthday,email` 반환 |
| M4 | 1 | 비밀번호/해시 미포함 |
| M5 | 1 | name 마스킹 적용 |
| M6 | 1 | 2자 이상 마지막 글자 `*` |
| M7 | 1 | 1자 이름 `*` 처리 |
## 6.4 비밀번호 수정 (P1-P8)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| P1 | 1 | `PATCH /api/v1/users/me/password` 존재 |
| P2 | 1 | 헤더 두 개 필수 |
| P3 | 1 | `currentPassword`,`newPassword` 필수 |
| P4 | 1 | `currentPassword` 해시 일치 검증 |
| P5 | 1 | `newPassword == currentPassword` 실패 처리 |
| P6 | 0 | 동일 규칙 중 공백/개행/제어문자 금지·허용문자 only 규칙 미충족 |
| P7 | 1 | 새 비밀번호 해시 저장 |
| P8 | 1 | password 필드 `trim()` 적용 없음 |
## 6.5 ErrorType 정리 (E1-E11)
| ID | 결과 | 판정 사유 |
|---|---:|---|
| E1 | 1 | 요구값 일치 |
| E2 | 1 | 요구값 일치 |
| E3 | 1 | 요구값 일치 |
| E4 | 1 | 요구값 일치 |
| E5 | 0 | 메시지가 `이미 가입된 로그인 ID입니다.`와 불일치 |
| E6 | 0 | `INVALID_PASSWORD_FORMAT` 항목 없음(코드/메시지 불일치) |
| E7 | 0 | `PASSWORD_CONTAINS_BIRTHDAY` 항목 없음(코드/메시지 불일치) |
| E8 | 0 | `INVALID_LOGIN_ID_FORMAT` 없음 |
| E9 | 0 | `INVALID_NAME_FORMAT` 없음 |
| E10 | 0 | `INVALID_EMAIL_FORMAT` 없음 |
| E11 | 0 | `INVALID_BIRTHDAY` 없음 |
## 섹션 합계
- 공통: `4/6` (6.06점 / 9.09점)
- 회원가입: `13/34` (19.70점 / 51.52점)
- 내 정보 조회: `7/7` (10.61점 / 10.61점)
- 비밀번호 수정: `7/8` (10.61점 / 12.12점)
- ErrorType: `4/11` (6.06점 / 16.67점)
## 총점 / 게이트
- 통과 항목 수: `35/66`
- 계산: `(35/66) * 100 = 53.03`
- 최종 점수(반올림): **53점 / 100점**
- 게이트 결과: **FAIL**
놀랍게도 '평가 기준'을 '요구사항'과 100% 일치시킨 결과, 처음 결과와 반대로 TDD가 TLD보다 더 높은 점수를 받은 것을 알 수 있다.
따라서 앞서 가정한 것 처럼, 'TDD가 TLD보다 요구사항을 더 정확하게 구현한다'를 어느정도 입증할 수 있다.
실험의 한계와 대안
이번 실험을 통해 “TDD가 TLD보다 요구사항을 더 정확하게 구현할 수 있다”는 결과를 어느 정도 확인할 수 있었지만, 동시에 이 결과를 절대적인 결론으로 받아들이기에는 몇 가지 한계가 있었다. 오히려 이번 실험의 핵심 교훈은 “TDD/TLD중 어떤게 더 정확하냐” 이전에, AI Agent 기반 개발에서 어떤 변수가 결과를 뒤집는지가 더 명확하게 드러났다는 점이다.
1) 평가 기준(체크리스트)이 요구사항을 대표하지 못하면 결론이 뒤집힌다
이번 실험에서 가장 큰 변수가 사실 이 부분이었다. 초반에는 TLD가 더 높은 점수를 받았지만, 이후 분석해보니 요구사항 문서와 체크리스트가 서로 불일치하는 항목이 많았고, 그 불일치가 점수에 직접적인 영향을 줬다. 결국 체크리스트를 요구사항과 100% 일치시키자 결과가 반전되었고, 이때부터 비로소 “TDD가 요구사항을 더 정확히 구현한다”는 가설을 의미 있게 검증할 수 있었다.
이 경험은 “평가 기준을 잘 만들면 된다” 수준이 아니라, AI 개발 실험에서 ‘요구사항-평가-구현’의 정합성이 무너지면 실험 결론 자체가 왜곡될 수 있다는 것을 보여준다.
대안은 명확하다. 요구사항을 만든 뒤 사후적으로 체크리스트를 만드는 것이 아니라, 시작 단계에서부터
[REQUIREMENTS.md → REQUIREMENTS 기반 CHECKLIST 생성 → 체크리스트로 평가]
이 파이프라인을 고정하고, 그 다음에 TDD/TLD를 비교해야 한다. 그래야 다른 변수를 통제하고, “방식 차이”만을 비교할 수 있을 것 같다.
2) 단일 실행(1회) 결과는 일반화하기 어렵다
AI Agent는 같은 요구사항이라도 세션 컨텍스트, 진행 순서, 이전에 어떤 실수를 했는지에 따라 결과가 달라진다. 그래서 “TDD 1회 vs TLD 1회”의 비교는 그 자체로 흥미로운 관찰이지만, 통계적으로는 우연의 영향을 배제하기 어렵다.
따라서 최소 3회 이상 반복해 평균과 편차를 비교함으로써 좀 더 정확한 실험 결과를 얻을 수 있을 것이다.
동일한 요구사항, 동일한 초기 커밋 상태, 동일한 프롬프트를 유지한 채 TDD/TLD를 여러 번 실행하면, “이번에는 운이 좋아서”가 아니라 “방식 자체가 가진 경향성”을 더 설득력 있게 말할 수 있기 때문이다.
3) 게이트형 점수는 직관적이지만, 원인 분석에는 부족할 수 있다
게이트형(1/0) 점수는 매우 명확하다. 하지만 점수가 낮게 나왔을 때, “어떤 종류의 요구사항이 AI에게 특히 취약한지”까지 설명하기에는 한계가 있다. 실제로 점수 미충족 항목을 보면, 단순 기능보다도
- “허용 문자 only” 같은 배타 조건
- 공백/개행/제어문자 같은 금지 조건
- trim/lowercase 같은 전처리 규칙
- 에러 타입/메시지 같은 계약 요소
에서 누락이 많았다. 즉, 방식(TDD/TLD)만으로 설명하기보다는 요구사항의 성격 자체가 AI에게 어려운 영역이 존재한다고 보는 편이 자연스럽다.
대안으로는 최종 점수만 보여주기보다,
- TDD에서만 맞춘 항목 Top N
- TLD에서만 맞춘 항목 Top N
- 둘 다 놓친 항목 Top N
처럼 “차이를 만든 항목” 중심으로 정리하면, 결론이 훨씬 설득력 있게 된다. 특히 “둘 다 놓친 항목”은 다음 실험에서 요구사항 문서를 어떻게 보강해야 하는지까지 이어질 수 있다.
정리
결론적으로 이번 실험은 “TDD가 TLD보다 무조건 좋다”를 말하기 위한 실험이라기보다, AI Agent 개발에서 안정성을 좌우하는 핵심 변수들이 무엇인지를 훨씬 명확하게 드러낸 실험에 가깝다. 특히 요구사항과 평가 기준의 정합성이 깨지는 순간 결론이 쉽게 뒤집힐 수 있었고, 정합성을 맞춘 이후에는 TDD가 요구사항 구현 정확도 측면에서 유의미한 우위를 보일 수 있었다.
시간이 된다면 실험의 한계를 극복하기 위해 동일 조건 반복 실험, 요구사항-체크리스트 파이프라인 고정, 게이트형 점수와 Top N 원인 분석을 통해 결론을 강화해볼 예정이다.