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:
parent
168671bc14
commit
db816e10fc
|
|
@ -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` 루프가 더 견고하다.
|
||||||
|
|
|
||||||
|
|
@ -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` 전역) 대상이다. 본 프로젝트 파일에서 직접 수정하지 않고 기록만 한다.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue