From db816e10fc06d309175cae313bd816b1dc475bc3 Mon Sep 17 00:00:00 2001 From: art Date: Tue, 30 Jun 2026 11:23:24 +0900 Subject: [PATCH] =?UTF-8?q?docs(dev):=20MyBatis=20null=20=EC=BA=90?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B7=9C=EC=95=BD=20+=20docker-compose=20?= =?UTF-8?q?override=20--build=20=ED=95=A8=EC=A0=95=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD --- docs/development/local-dev-setup.md | 10 +++++++ docs/development/verification-strategies.md | 30 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/docs/development/local-dev-setup.md b/docs/development/local-dev-setup.md index 36e34f5..ea21ce7 100644 --- a/docs/development/local-dev-setup.md +++ b/docs/development/local-dev-setup.md @@ -53,6 +53,16 @@ docker compose -f docker-compose.yml build app # Dockerfile WAR 굽기 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 은 본 문서 함정 참조). > devtools 자동재시작은 도입하지 않았다 — 컨테이너 `spring-boot:run` 에는 소스 자동 재컴파일러가 없어(IDE 부재) 이득이 작고, `3.5.x-SNAPSHOT` devtools 의 offline 해소가 불안정했다. `restart`/`exec compile` 루프가 더 견고하다. diff --git a/docs/development/verification-strategies.md b/docs/development/verification-strategies.md index 4ff85ee..92a8118 100644 --- a/docs/development/verification-strategies.md +++ b/docs/development/verification-strategies.md @@ -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` | + +**대안**: `` 동적 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` 전역) 대상이다. 본 프로젝트 파일에서 직접 수정하지 않고 기록만 한다.