docs(dev): MyBatis null 캐스트 규약 + docker-compose override --build 함정 기록

verification-strategies.md: MyBatis annotation SQL에서 nullable 파라미터를
#{p} IS NULL 조건으로 사용 시 ::bigint/::timestamptz 명시 캐스트 필수 규약 추가.
컴파일·L1 MockBean 테스트로 탐지 불가 — 코드리뷰 rg 체크 항목 포함.

local-dev-setup.md: docker-compose.override.yml 존재 시 `docker compose up --build`
(base 명시 없이)는 override 의 build: !reset null 로 무효화된다는 경고 추가.
로컬 Java 변경 반영은 `docker compose restart app` 으로 충분함을 명시.

근거: 세션 20260630-105459 포스팅 500 버그 수정 회고.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD
This commit is contained in:
이정수 2026-06-30 11:23:24 +09:00
parent 168671bc14
commit db816e10fc
2 changed files with 40 additions and 0 deletions

View File

@ -53,6 +53,16 @@
docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기 docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기
docker compose -f docker-compose.yml up -d --build app docker compose -f docker-compose.yml up -d --build app
``` ```
> **⚠ 함정**: `docker compose up --build app`(`-f` 없이)을 로컬 dev 환경에서 실행하면
> override 가 `build: !reset null` 로 base 의 Dockerfile 빌드를 비활성화하므로
> **이미지를 새로 구워도 override 가 그 이미지를 무시**한다.
> 로컬 dev 에서 Dockerfile 재빌드가 필요한 경우는 거의 없으나, 필요하다면 반드시
> `-f docker-compose.yml` 로 override 를 명시적으로 제외한다.
> 로컬 Java 코드 변경 반영은 `docker compose restart app` 으로 충분하다.
>
> 근거: 세션 20260630-105459 — `docker compose up --build` 가 "No services to build" 반환.
Dockerfile 은 멀티스테이지(WAR repackage → `eclipse-temurin:21-jre` + `java -jar`)로 운영과 동일한 산출물을 만든다(프록시 CA·`-P dev`·repackage goal 은 본 문서 함정 참조). Dockerfile 은 멀티스테이지(WAR repackage → `eclipse-temurin:21-jre` + `java -jar`)로 운영과 동일한 산출물을 만든다(프록시 CA·`-P dev`·repackage goal 은 본 문서 함정 참조).
> devtools 자동재시작은 도입하지 않았다 — 컨테이너 `spring-boot:run` 에는 소스 자동 재컴파일러가 없어(IDE 부재) 이득이 작고, `3.5.x-SNAPSHOT` devtools 의 offline 해소가 불안정했다. `restart`/`exec compile` 루프가 더 견고하다. > devtools 자동재시작은 도입하지 않았다 — 컨테이너 `spring-boot:run` 에는 소스 자동 재컴파일러가 없어(IDE 부재) 이득이 작고, `3.5.x-SNAPSHOT` devtools 의 offline 해소가 불안정했다. `restart`/`exec compile` 루프가 더 견고하다.

View File

@ -226,6 +226,36 @@ SSRF·zip-slip·경로 boundary·sanitize 같은 보안핵심은 verification-ad
--- ---
### MyBatis + PostgreSQL nullable 파라미터 명시 캐스트 규약
MyBatis annotation SQL(`@Select`)에서 nullable 파라미터를 `#{p} IS NULL` 조건으로 쓸 때, PostgreSQL은 prepared statement `$N`의 타입을 추론하지 못해 `PSQLException: could not determine data type of parameter $1` → 500이 발생한다. 컴파일·L1 단위테스트(MockBean)로는 탐지되지 않는다.
**규약:**
```sql
-- ❌ 잘못된 패턴 — null 전달 시 PSQLException
AND (#{categoryId} IS NULL OR p.category_id = #{categoryId})
-- ✅ 올바른 패턴 — 명시 캐스트
AND (#{categoryId}::bigint IS NULL OR p.category_id = #{categoryId}::bigint)
```
nullable 파라미터 타입별 캐스트:
| Java 타입 | PostgreSQL 캐스트 |
|---|---|
| `Long` / `Integer` | `::bigint` / `::int` |
| `OffsetDateTime` / `LocalDateTime` | `::timestamptz` / `::timestamp` |
| `String` | `::text` |
**대안**: `<if test="p != null">` 동적 XML 분기로 null 케이스를 조건 자체에서 제거(JamsMapper/GamesMapper keyset 패턴). 신규 keyset 페이징 매퍼 작성 시 이 패턴을 우선 권장한다.
**신규 매퍼 코드리뷰 체크**: `rg '#{[^}]+}\s+IS\s+NULL' src/main/java --type java` 로 타입 캐스트 없는 IS NULL 조건을 전수 확인한다.
> 근거: 세션 20260630-105459 — `PostsMapper.listPublishedKeyset` `#{categoryId} IS NULL` 캐스트 누락 → `GET /posts` 500.
---
## 프로토콜 개선 권고 (외부 번들 — 미적용) ## 프로토콜 개선 권고 (외부 번들 — 미적용)
아래 항목은 ATP 플러그인 번들(`~/.claude` 전역) 대상이다. 본 프로젝트 파일에서 직접 수정하지 않고 기록만 한다. 아래 항목은 ATP 플러그인 번들(`~/.claude` 전역) 대상이다. 본 프로젝트 파일에서 직접 수정하지 않고 기록만 한다.