bibimbap/.atp/work-session/20260630-105459/report.md

176 lines
11 KiB
Markdown

---
schema_version: "2"
sid: "20260630-105459"
started_at: "2026-06-30T10:54:59"
ended_at: "2026-06-30T11:32:00"
user_request: "포스팅 메뉴 진입시 500 Error 발생해"
Summary: |
PostsMapper.listPublishedKeyset SQL에서 null 파라미터를 타입 없이 PostgreSQL에 전달해
PSQLException: could not determine data type of parameter $1 → 500 발생.
::bigint / ::timestamptz 명시 캐스트 추가로 수정.
update() SQL category_id 누락 버그 함께 수정. GET /posts 200 확인.
Invocations:
- agent: research-advisor
result: "포스팅 Controller/Mapper 구조 파악 + 500 원인 후보 목록 작성"
- agent: verification-advisor
result: "AC 4항목 전체 pass (HTTP 200, PSQLException 0건, SQL 캐스트 확인, category_id 확인)"
Decisions:
- "PSQLException $1 타입 미결정 → ::bigint / ::timestamptz 캐스트로 수정 (테이블 미존재 가설은 DDL 멱등 적용 후 NOTICE already-exists로 기각)"
- "update SQL category_id 누락 → 동일 파일 수정에 포함"
verified_by_me:
- "L1: compile pass (spring-boot:run 재시작 컴파일)"
- "L2: GET /posts → HTTP 200"
- "log scan: PSQLException 0건 (clean)"
needs_user_verification: "(없음)"
graph_refresh: "skip: no-graphify"
user_signals:
positive: []
negative: []
open_items:
- "회귀 테스트 미추가 (버그 수정 규약 미준수): PostControllerTest에 'null cursor 파라미터로 GET /posts 200' 시나리오 추가 필요. 후속 세션에서 처리."
commit: "168671b"
---
# Advisor Invocation Decision Log
# 각 advisor 호출/스킵 판단 즉시 1줄 append
- advisor: requirements-advisor
decision: skip
rationale: '요청 명확 — 포스팅 메뉴 진입 시 500 에러. 추가 요구사항 분해 불필요.'
checked_at: "2026-06-30T10:54:59"
- advisor: graphify-lookup-advisor
decision: skip
rationale: 'graphify add-on 없음. research-advisor 직접 호출.'
checked_at: "2026-06-30T10:54:59"
- advisor: design-advisor
decision: skip
rationale: '원인 확정 후 수정 경로 단일 — 파라미터 캐스트 + SQL 컬럼 추가. 설계 분기 없음.'
checked_at: "2026-06-30T11:20:00"
- advisor: implementation-advisor
decision: skip
rationale: '단일 파일 2-hunk 수정. orchestrator 직접 수행.'
checked_at: "2026-06-30T11:20:00"
---
Retrospective:
signals:
positive: []
negative: []
what_went_well:
- "초기 가설(테이블 미존재) DDL 멱등 적용 → NOTICE already-exists 로 빠르게 기각하고,
앱 로그 확인으로 실제 원인(PSQLException $1 타입 미결정)을 2단계 이내에 특정했다.
'가설 → DDL 검증 → 로그 확인' 순서가 500 버그 디버깅 효율 측면에서 유효했음."
- "단일 파일 2-hunk 수정(캐스트 추가 + category_id 누락)에 implementation-advisor를 거치지 않고
orchestrator가 직접 수행 — 불필요한 위임 없이 세션 시간을 절약."
- "두 번째 버그(update SQL category_id 누락)를 동일 파일 수정에 포함해 커밋 경계를 깔끔하게 유지."
what_to_improve:
- "PostsMapper.listPublishedKeyset 는 '#{param}::cast IS NULL' 패턴으로 null을 처리하는 반면,
JamsMapper·GamesMapper의 keyset 쿼리는 '<if test=\"... != null\">' 동적 XML 분기로 null을 회피한다.
두 패턴이 혼재하며, '::cast IS NULL' 패턴은 PostgreSQL이 prepared statement $1의 타입을 추론할 때
null 리터럴을 타입 미지정으로 전달하면 PSQLException이 발생한다는 함정이 있다.
이 패턴을 사용하는 매퍼는 반드시 명시 캐스트(::bigint / ::timestamptz 등)가 있어야 하며,
docs에 규약으로 기록하지 않으면 신규 매퍼 작성 시 동일 패턴 재발이 높다."
- "버그 재현 테스트(회귀 테스트) 없이 커밋됐다. verification-strategies.md 에는
'버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다'고 명시돼 있으나 이번 세션은 적용하지 않았다."
- "docker-compose.override.yml 존재 시 '도커 이미지 재빌드가 필요한 것 아닌가' 라는 혼선이 발생했다
(회고 포인트 3). override가 base image를 교체하기 때문에 docker compose up --build 는 override 환경에서
이미지 rebuild 후에도 override가 그 이미지를 다시 무시한다. local-dev-setup.md에 이 함정을
명시적 경고로 추가하지 않으면 재발 가능."
memory_candidates:
- name: mybatis-postgres-null-param-explicit-cast
type: feedback
description: "MyBatis + PostgreSQL에서 nullable 파라미터를 '#{p}::type IS NULL' 패턴으로 쓸 때 명시 캐스트 필수 — 미지정 시 PSQLException $N 타입 미결정"
body_draft: |
## Why
PostgreSQL은 prepared statement에서 null 리터럴의 타입을 추론할 수 없다.
MyBatis가 null 파라미터를 바인딩하면 $N의 타입이 결정되지 않아
`PSQLException: could not determine data type of parameter $1` 이 발생한다.
## 패턴 및 규약
nullable Long/OffsetDateTime 파라미터를 '#{p} IS NULL' 조건으로 쓸 때:
- **필수**: `#{p}::bigint`, `#{p}::timestamptz` 등 명시 캐스트 추가
- **대안**: `<if test="p != null">` 동적 XML 분기로 null 케이스 분리
현재 bibimbap 매퍼에는 두 패턴이 혼재함:
- PostsMapper.listPublishedKeyset → '::cast IS NULL' 패턴 (캐스트 추가로 수정됨 commit 168671b)
- JamsMapper.listVisibleKeyset, GamesMapper.listVisibleKeyset/searchVisibleKeyset → '<if test>' 분기 패턴
신규 keyset 페이징 매퍼 작성 시 '<if test>' 분기를 우선 권장.
'::cast IS NULL' 패턴을 쓴다면 명시 캐스트 누락 여부를 code review에서 확인한다.
## How to apply
1. nullable 파라미터를 IS NULL 조건으로 쓰는 MyBatis SQL을 rg로 전수 확인:
`rg '#{[^}]+}\s+IS\s+NULL' src/main/java --type java`
2. 각 라인에 `::type` 캐스트가 없으면 추가.
3. 또는 해당 조건 블록을 `<if test="p != null">` 으로 재작성.
rationale_for_saving: "동일 패턴 재발 가능성 높음 — 신규 페이징 매퍼 추가 시마다 잠재적으로 발생. 코드에서 유도 불가(컴파일 시점에 탐지 안 됨, L1 단위테스트도 @MockBean으로 회피)."
signal_source: observation
docs_sync_target: "/Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md"
memory_optional: true
- name: docker-compose-override-masks-build
type: feedback
description: "docker-compose.override.yml 존재 시 'docker compose up --build' 로 Dockerfile 이미지를 새로 구워도 override가 base image를 교체하므로 빌드가 로컬 dev에 반영되지 않는다"
body_draft: |
## Why
`docker compose up --build app` 은 base `docker-compose.yml``build:` 지시를 실행해
Dockerfile로 WAR를 굽는다. 그러나 `docker-compose.override.yml`이 존재하면
override가 `app` 서비스의 image/command/volumes를 재정의하므로
방금 구운 이미지는 override에 의해 무시된다.
## 규약
- 로컬 dev: `docker compose up -d app` (--build 불필요/무효)
코드 변경 반영: `docker compose restart app`
- 배포 이미지 빌드: override를 제외하고 base만 명시
`docker compose -f docker-compose.yml up -d --build app`
## How to apply
`--build` 를 쓰기 전에 `docker-compose.override.yml` 존재 여부 확인:
```bash
ls docker-compose.override.yml
```
존재하면 로컬 dev 경로(`restart`)를 사용하고,
Dockerfile 이미지가 필요한 경우 `-f docker-compose.yml` 로 override를 명시적으로 제외한다.
rationale_for_saving: "override 존재 사실 자체는 local-dev-setup.md에 있으나 '--build 무효' 함정이 명시되지 않아 재발 가능. 코드에서 유도 불가."
signal_source: observation
docs_sync_target: "/Users/wemadeplay/workspace/stz/bibimbap/docs/development/local-dev-setup.md"
memory_optional: true
- name: bug-fix-regression-test-required
type: feedback
description: "버그 수정 커밋에 재현 테스트 포함 의무(verification-strategies.md 기존 규약) — 이번 세션에서 미적용"
body_draft: |
## Why
verification-strategies.md §회귀 테스트 의무:
"버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다.
revert 시 테스트가 실패하고, 수정 후엔 통과해야 한다."
이번 세션(20260630-105459)에서 PSQLException 재현 테스트를 추가하지 않았다.
MyBatis + PostgreSQL 통합 계층이라 L1 단위테스트만으로는 재현이 어렵지만,
최소한 PostControllerTest에 "null cursor 파라미터로 GET /posts 200" 시나리오를 추가해야 함.
## How to apply
버그 수정 세션 종료 전 체크:
1. 버그 재현 조건(null cursor)을 트리거하는 테스트가 존재하는가?
2. 없으면 unit 레벨(mock) 테스트라도 추가 — 매퍼 호출 시 null 파라미터 경로 포함.
3. open_items에 "회귀 테스트 추가" 를 명시하고 후속 세션에서 처리.
rationale_for_saving: "기존 규약이 있음에도 이번 세션에서 미준수. open_items에도 없음 — 다음 세션이 이를 모르면 영구 누락 가능."
signal_source: observation
docs_sync_target: null
memory_optional: false
protocol_feedback:
- "버그 수정 세션에서 verification-advisor가 회귀 테스트 존재 여부를 AC 항목으로 체크하지 않았다.
verification-advisor 체크리스트에 '버그 수정 커밋 시 재현 테스트 존재 여부 확인' 항목을 추가 권고.
(verification-strategies.md §회귀 테스트 의무 조항이 verification-advisor의 실행 체크리스트에 연결되지 않음 — 구조적 갭.)"
- "연구 후보 가설을 DDL로 검증하는 과정(already-exists NOTICE로 기각)은 효율적이었지만,
프로토콜상 '초기 가설 검증 우선순위: 앱 로그 → DDL 검증' 순서를 명시하는 규약이 없다.
버그 디버깅 세션 체크리스트에 '먼저 앱 로그/에러 스택 확인 후 외부 가설(DB 스키마 등) 검증' 순서를 추가하면
DDL 적용 같은 부작용이 있는 검증 단계를 줄일 수 있다."
applied_changes: []