# JPA 관계형 영속성 플랫폼 설계서 - 문서 상태: 구현 기준 설계 - 기준일: 2026-08-11 - 대상 저장소: `backend-skeleton` - 설계 경로: `docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md` - 요구사항 원본: `붙여넣은 마크다운(1)(20260811-071252).md` --- ## 1. 문서 목적 이 문서는 Java/Spring Backend Skeleton에서 사용할 JPA 관계형 영속성 플랫폼의 공개 계약, 모듈 경계, 트랜잭션 의미론, Hibernate·PostgreSQL 확장, Flyway 스키마 관리, 오류·Retry·관측성·보안·검증 기준을 구현 가능한 수준으로 확정한다. 이 플랫폼은 `JpaRepository`를 다시 감싸는 CRUD 라이브러리가 아니다. 도메인 모듈이 Entity, Embeddable, Repository, 업무 Query, Index Requirement, Lock·Soft Delete·Audit 정책을 소유하고, 플랫폼은 다음 기술적 기반을 제공한다. ```text 도메인 소유 ├─ Entity / Embeddable ├─ Repository Interface ├─ 도메인 Query ├─ 도메인 Constraint·Index 요구 └─ 도메인 Lock·Soft-delete·Audit 정책 플랫폼 소유 ├─ Persistence Context·Transaction 정책 ├─ SQLSTATE 기반 오류 모델 ├─ 전체 Use Case Retry ├─ Fetch·Query·Pagination 검증 도구 ├─ Hibernate Batch·Statistics 확장 ├─ PostgreSQL Native Capability ├─ Flyway Migration·Schema Gate ├─ 관측성·보안 규칙 └─ PostgreSQL 실제 계약 Testkit ``` 구현자가 이 문서를 읽은 뒤 다시 결정하지 않아야 하는 핵심 질문은 다음과 같다. ```text 어디에 Transaction을 시작하는가? 어떤 실패에서 전체 업무를 다시 실행할 수 있는가? Commit 결과를 모르면 무엇을 하는가? 어떤 Fetch Plan을 선택하고 어떻게 N+1을 검증하는가? 어떤 Query는 JPQL이고 어떤 Query는 Native SQL인가? Batch가 실제 JDBC Batch인지 어떻게 증명하는가? Entity Mapping과 Schema 중 무엇이 Source of Truth인가? PostgreSQL 고유 기능을 어디까지 공개하는가? 어떤 DB 계정이 어떤 권한을 갖는가? 어떤 PostgreSQL 버전에서 Stable을 선언하는가? ``` --- ## 2. 목표와 성공 기준 ### 2.1 목표 1. 도메인 Repository를 보존하면서 JPA·Hibernate·PostgreSQL 사용 규칙을 일관되게 제공한다. 2. Application Use Case 단위 Transaction과 전체 Transaction Retry를 구현한다. 3. Optimistic Conflict, Deadlock, Serialization Failure, Lock Timeout, Constraint Violation, Commit 결과 불명을 안정 오류로 변환한다. 4. OSIV, 전역 EAGER, 전역 Cascade, 전역 Soft Delete, 운영 `ddl-auto=update` 같은 위험한 기본값을 구조적으로 차단한다. 5. EntityGraph, Fetch Join, Projection, Batch Fetch, Keyset Pagination을 Use Case별 Fetch·Query 전략으로 제공한다. 6. JDBC Batch, Bulk DML, StatelessSession, PostgreSQL Native Write를 서로 다른 Capability로 제공한다. 7. Flyway를 운영 Schema 변경의 Source of Truth로 고정하고 빈 DB·이전 Release Snapshot·최장 지원 Snapshot 업그레이드를 검증한다. 8. H2가 아닌 PostgreSQL 16·17·18 실제 의미론으로 Stable을 인증한다. 9. Query Count, Entity/Collection Fetch, Row Load, Query Plan, Pool·Transaction·Retry를 관측한다. 10. 일반 애플리케이션이 Hibernate Session·Native SQL·운영 DDL을 무제한으로 사용하지 못하게 한다. ### 2.2 성공 기준 | 영역 | 완료 기준 | |---|---| | Repository | 플랫폼에 `GenericRepository` 재구현이 없고 도메인 Repository가 Spring Data를 직접 확장할 수 있다. | | Mapping | Field Access, protected no-arg constructor, Entity 직렬화 금지, association 규칙이 정적·통합 테스트로 검증된다. | | Transaction | Application Service 경계, propagation, isolation, timeout, rollback rule이 계약 테스트로 고정된다. | | Retry | 새 Persistence Context와 새 DB Transaction에서 전체 Use Case만 재실행된다. | | Completion Unknown | Commit 단계 연결 손실이 일반 transient 오류와 분리되고 자동 Retry되지 않는다. | | Fetch | N+1, Multiple Collection Cartesian Product, Collection Fetch Pagination을 정량 검증한다. | | Pagination | Page·Slice·Keyset·Scroll의 사용 기준과 stable ordering이 코드로 제공된다. | | Batch | SQL log가 아니라 Hibernate/JDBC 통계로 실제 batch 실행을 증명한다. | | Migration | `Flyway migrate + Hibernate validate`, checksum·missing migration 실패, N-1/oldest snapshot 업그레이드가 CI에 연결된다. | | PostgreSQL | JSONB·Array·Range·`ON CONFLICT`·`NOWAIT`·`SKIP LOCKED`가 PG16·17·18에서 검증된다. | | Security | Runtime·Migration·Admin 역할이 분리되고 Runtime 역할의 DDL이 실패한다. | | Observability | queryName 기반 저카디널리티 지표를 제공하고 SQL parameter·PII를 기록하지 않는다. | | Release | Stable·Advanced·Experimental 경계가 문서, 의존성, CI lane에서 일치한다. | --- ## 3. 입력 자료와 명시적 구현 가정 ### 3.1 요구사항 원본이 확정한 사항 - Java 21을 Stable baseline으로 사용한다. - Spring Boot BOM이 관리하는 Spring Data JPA·Hibernate·Flyway·Hikari 조합을 사용한다. - Spring Data JPA 4.1, Jakarta Persistence 3.2, Hibernate ORM 7.4를 Stable 기준으로 삼는다. - PostgreSQL 16·17·18을 Stable DB Matrix로 삼는다. - H2는 Local Convenience이며 PostgreSQL 호환성 증거가 아니다. - Jakarta Persistence 4.0, Hibernate ORM 8, PostgreSQL 19는 별도 compatibility lane이다. - J1 Standard, J2 Advanced, J3 Provider/DB Extension, J4 Admin/Operations 계층을 사용한다. - 도메인이 Entity와 Repository를 소유하고 플랫폼은 Generic CRUD Repository를 만들지 않는다. - Persistence Context는 transaction-scoped이며 OSIV를 명시적으로 비활성화한다. - Transaction 경계는 Application Service에 둔다. - Optimistic Conflict·Deadlock·Serialization Failure Retry는 전체 Transaction 재실행이다. - Commit 결과 불명은 `TransactionCompletionUnknown`으로 분류하고 자동 Retry하지 않는다. - PostgreSQL write-heavy Entity의 기본 ID 전략은 Sequence이며 IDENTITY는 JDBC Batch 제약 때문에 제한한다. - Fetch 전략은 Use Case별 Fetch Plan으로 관리한다. - Hibernate 7.4의 Collection Fetch Join + Pagination은 과거 금지 규칙을 복사하지 않고 실제 SQL·row amplification을 검증한다. - Flyway가 실제 Schema 변경의 Source of Truth이며 운영 `ddl-auto=update`를 금지한다. - Application·Migration·Admin DB credential을 분리한다. - Multi-tenancy와 Read Replica는 초기 Experimental이다. ### 3.2 실제 저장소가 제공되지 않아 고정한 가정 | 항목 | 설계 가정 | |---|---| | 저장소 | Gradle Kotlin DSL 멀티모듈 `backend-skeleton` | | 모듈 루트 | `modules/jpa` | | Root package | `io.backend.skeleton.jpa` | | Spring Boot | 4.1 계열 BOM. 정확한 patch는 host 저장소 version catalog가 소유한다. | | Runtime DB | PostgreSQL 16 이상 | | 기본 Provider | Hibernate ORM 7.4 | | Migration | Flyway | | Connection Pool | HikariCP | | 테스트 | JUnit 5, AssertJ, ArchUnit, Testcontainers, Toxiproxy | | 관측성 | Micrometer, Spring Observation, OpenTelemetry exporter adapter | | CI | PR: PG16·18, Release: PG16·17·18 | 연구 자료가 범용 numeric timeout, pool size, batch size를 확정하지 않았으므로 플랫폼은 이를 보편 상수로 하드코딩하지 않는다. Production profile은 명시적 값을 요구하고, Testkit만 결정적인 fixture 값을 제공한다. ### 3.3 우선순위 ```text 사용자 지시 → 이 설계서의 명시적 계약 → 심층 리서치 원본 → host 저장소의 기존 convention → Spring Boot BOM 기본값 ``` 기존 저장소 구조가 다르면 경로와 convention plugin 이름은 매핑할 수 있지만, 공개 계약과 불변 조건은 유지한다. --- ## 4. 범위 ### 4.1 Stable 범위 ```text Spring Data domain repository Jakarta Persistence 3.2 Hibernate ORM 7.4 PostgreSQL 16·17·18 REQUIRED transaction READ COMMITTED 기본 isolation read-only·timeout Optimistic Lock 표준 Pessimistic Lock Derived Query·JPQL·Projection EntityGraph·Fetch Join Page·Slice·Keyset JDBC Batch Flyway migrate·validate SQLSTATE 기반 오류 bounded full-transaction retry OSIV off L1 Persistence Context Spring Data auditing opt-in PG Testcontainers contract ``` ### 4.2 Advanced opt-in 범위 ```text MANDATORY·REQUIRES_NEW Specification·Querydsl Query Hint·Scroll·Stream NOWAIT·SKIP LOCKED Batch Fetch·Subselect Fetch Bulk DML StatelessSession PostgreSQL JSONB·Array·Range·INET ON CONFLICT·RETURNING COPY 기반 대량 import Envers Hibernate L2 Cache Concurrent Index migration Query Plan regression ``` ### 4.3 Experimental 범위 ```text Shared schema tenant column PostgreSQL RLS Schema-per-tenant Database-per-tenant Read Replica routing Jakarta Persistence 4.0 Hibernate ORM 8 PostgreSQL 19 ``` Experimental 기능은 별도 모듈과 CI lane에서만 활성화하며 Stable Core의 공개 API를 변경하지 않는다. ### 4.4 명시적 비지원 ```text GenericRepository CRUD 재구현 Entity를 Web/API DTO로 직접 반환 Extended Persistence Context 일반 사용 OSIV 전역 EAGER 전역 Cascade.ALL 전역 implicit Soft Delete 운영 ddl-auto update/create/create-drop Repository method 단위 부분 Retry Commit 결과 불명 자동 Retry Remote distributed transaction 기본화 임의 XA 기본 지원 무제한 findAll 자유로운 raw SQL H2 결과로 PostgreSQL Stable 선언 annotation 하나만으로 Read Replica 자동 routing Hibernate Query Cache 기본 활성화 ``` ### 4.5 Reactive 경계 JPA와 JDBC는 Blocking 기술이다. 이 플랫폼은 Reactor 타입을 공개 API에 넣지 않는다. WebFlux 애플리케이션이 JPA를 사용할 경우 애플리케이션 또는 별도 execution adapter가 bounded blocking executor로 격리해야 하며, Reactor event-loop에서 Repository를 호출하는 것은 금지한다. Reactive relational persistence가 필요하면 별도 R2DBC 모듈을 설계한다. --- ## 5. 핵심 설계 원칙 1. **도메인 소유권 유지:** Entity·Embeddable·Repository·업무 Query·Index Requirement는 도메인이 소유한다. 2. **추상화 중복 금지:** Spring Data의 CRUD 추상화를 다시 감싸지 않는다. 3. **Use Case Transaction:** Transaction은 Application Use Case 단위다. 4. **전체 Transaction Retry:** Retry는 새 Persistence Context와 새 Transaction에서 전체 작업을 다시 실행한다. 5. **불명확성 보존:** Commit 결과를 모르면 성공 또는 실패로 추정하지 않는다. 6. **Fetch Plan 명시:** Mapping annotation 하나로 모든 Use Case의 Fetch를 결정하지 않는다. 7. **Schema Source of Truth 분리:** Entity Mapping은 객체-관계 매핑 계약이고 실제 Schema 변경은 Flyway가 소유한다. 8. **PostgreSQL 실제 검증:** H2나 mock으로 Lock·Constraint·SQLSTATE·Plan 의미론을 증명하지 않는다. 9. **Provider 차이 노출:** Hibernate·PostgreSQL 고유 기능은 J3 Extension으로 명시한다. 10. **위험 기능 opt-in:** REQUIRES_NEW, Native SQL, Bulk DML, StatelessSession, L2 Cache, Envers는 선택 모듈이다. 11. **정량 성능 검증:** Query 수뿐 아니라 rows, hydrated entity, collection fetch, batch, pool wait를 측정한다. 12. **권한 최소화:** Runtime 계정은 DML만, Migration·Admin 계정은 별도다. --- ## 6. 전체 아키텍처 ```text Domain / Application ├─ Entity ├─ Embeddable ├─ Repository Interface ├─ Custom Repository Contract ├─ Projection / Read Model └─ Application Service @Transactional │ ▼ ┌──────────────────────────────────────────────────┐ │ JPA Persistence Platform │ │ │ │ J1 Standard │ │ ├─ Spring Data integration │ │ ├─ Transaction defaults │ │ ├─ Stable error model │ │ └─ Auditing opt-in │ │ │ │ J2 Advanced │ │ ├─ Fetch / Query support │ │ ├─ Keyset / Scroll │ │ ├─ Full-TX retry │ │ ├─ Batch / Bulk │ │ └─ Pessimistic lock │ │ │ │ J3 Provider / DB Extension │ │ ├─ Hibernate Session / Statistics │ │ ├─ StatelessSession │ │ ├─ PostgreSQL types │ │ ├─ ON CONFLICT / RETURNING │ │ └─ NOWAIT / SKIP LOCKED / COPY │ │ │ │ J4 Admin / Operations │ │ ├─ Flyway │ │ ├─ Index / Backfill │ │ ├─ Plan regression │ │ └─ Role / Schema validation │ └───────────────────────┬──────────────────────────┘ │ ▼ PostgreSQL 16~18 ``` ### 6.1 일반 Write 흐름 ```text Controller → Application Service → @Transactional 시작 → Domain Repository → Entity persist/update → flush → DB constraint/lock 검증 → commit → 결과 반환 ``` 외부 HTTP, Object Storage, Messaging 호출은 DB Transaction 밖으로 이동한다. DB 변경과 메시지 발행은 기존 Messaging Platform의 Transactional Outbox를 사용한다. ### 6.2 Retry 흐름 ```text Application Use Case → Attempt 1: 새 EntityManager + 새 Transaction → OptimisticConflict / Deadlock / SerializationFailure → Retry Policy 분류 → bounded backoff + jitter → Attempt 2: 새 EntityManager + 새 Transaction → commit ``` 부분 SQL만 다시 실행하거나 동일 Persistence Context를 재사용하지 않는다. ### 6.3 Completion Unknown 흐름 ```text Application → COMMIT 전송 → PostgreSQL commit 가능 → 응답 전에 connection loss → EvidenceAwareJpaTransactionManager → TransactionCompletionUnknown → 자동 Retry 금지 → transactionKey / unique key / outbox / 상태 조회 → domain-specific reconciliation ``` ### 6.4 Read 흐름 ```text Application Query → QueryName → Projection / EntityGraph / Fetch Join / Native Query → QueryObservation → Statement + Hibernate statistics → DTO / Projection 반환 ``` Entity를 Controller에 반환하지 않는다. --- ## 7. 모듈 구조 ```text backend-skeleton/ ├── modules/jpa/ │ ├── jpa-core-api/ │ ├── jpa-transaction/ │ ├── jpa-spring-data/ │ ├── jpa-querydsl/ │ ├── jpa-hibernate/ │ ├── jpa-postgresql/ │ ├── jpa-postgresql-copy/ │ ├── jpa-migration-flyway/ │ ├── jpa-auditing/ │ ├── jpa-envers/ │ ├── jpa-cache-hibernate/ │ ├── jpa-observability/ │ ├── jpa-security/ │ ├── jpa-spring-boot-starter/ │ ├── jpa-testkit/ │ ├── jpa-testkit-postgresql/ │ ├── jpa-testkit-migration/ │ └── jpa-testkit-queryplan/ ├── modules/jpa-experimental/ │ ├── jpa-multitenancy-column/ │ ├── jpa-multitenancy-rls/ │ ├── jpa-multitenancy-schema/ │ ├── jpa-multitenancy-database/ │ ├── jpa-read-replica/ │ └── jpa-next-compatibility/ ├── infra/jpa/ │ ├── postgres/ │ ├── toxiproxy/ │ └── roles/ └── docs/jpa/ ├── entity-mapping-guide.md ├── transaction-guide.md ├── query-fetch-guide.md ├── migration-guide.md ├── postgresql-extensions.md ├── observability.md ├── security.md ├── support-matrix.md └── runbooks.md ``` ### 7.1 모듈 책임 | 모듈 | 책임 | |---|---| | `jpa-core-api` | Spring/JPA 비종속 안정 오류·Transaction Profile·Query Name·Capability 계약 | | `jpa-transaction` | Spring Transaction Adapter, full-TX retry, completion evidence | | `jpa-spring-data` | Custom Fragment 기반 지원, Safe Sort, Projection·EntityGraph helper | | `jpa-querydsl` | 선택 Querydsl integration | | `jpa-hibernate` | Statistics, Fetch·Batch·Bulk·StatelessSession extension | | `jpa-postgresql` | SQLSTATE, JSONB·Array·Range, native write, lock extension | | `jpa-postgresql-copy` | J4 대량 import/backfill COPY | | `jpa-migration-flyway` | Migration policy, validate, snapshot upgrade gate | | `jpa-auditing` | Spring Data auditing opt-in | | `jpa-envers` | Entity history opt-in | | `jpa-cache-hibernate` | Hibernate L2 Cache opt-in; Query Cache 기본 비활성 | | `jpa-observability` | queryName·transaction·retry·Hibernate statistics 관측 | | `jpa-security` | ArchUnit rule, DB role/search_path validation, log redaction | | `jpa-spring-boot-starter` | AutoConfiguration·Properties·Actuator·startup guard | | `jpa-testkit*` | PostgreSQL·Migration·Query Plan·Concurrency 계약 테스트 | ### 7.2 의존 방향 ```text jpa-core-api ↑ ├─ jpa-transaction ├─ jpa-spring-data ├─ jpa-hibernate ├─ jpa-postgresql ├─ jpa-migration-flyway ├─ jpa-auditing ├─ jpa-observability └─ jpa-security jpa-spring-boot-starter → 위 Stable 모듈 조합 jpa-testkit* → 테스트 대상 모듈 ``` `jpa-core-api`는 `jakarta.persistence`, Spring, Hibernate, PostgreSQL JDBC, Flyway에 의존하지 않는다. ### 7.3 ArchUnit 경계 ```text jpa-core-api → provider/framework dependency 금지 platform → domain Entity 정의 금지 domain → org.hibernate 직접 의존 금지 web/controller → @Entity 반환 금지 @Entity → web DTO annotation 금지 application → J4 admin package 접근 금지 ``` --- ## 8. 공개 계층 J1~J4 ### 8.1 J1 Standard Persistence 일반 애플리케이션이 기본으로 사용한다. ```text Spring Data Repository Derived Query JPQL DTO / Interface Projection Application Service @Transactional @Version Optimistic Lock Spring Data Auditing opt-in Page / Slice ``` 도메인 Repository 예시: ```java public interface OrderRepository extends JpaRepository, OrderRepositoryCustom { Optional findByOrderNumber(OrderNumber orderNumber); } public interface OrderRepositoryCustom { KeysetSlice findRecent( OrderSearchCondition condition, KeysetPageRequest page); } ``` ### 8.2 J2 Advanced Persistence ```text Specification Querydsl EntityGraph Query Hint Pessimistic Lock Keyset / Scroll / Stream JDBC Batch Bulk DML Full Transaction Retry ``` J2 사용은 명시적 모듈 의존성과 Query Name 등록을 요구한다. ### 8.3 J3 Provider / Database Extension ```text Hibernate Session Hibernate Fetch Profile StatelessSession PostgreSQL JSONB·Array·Range·INET ON CONFLICT·RETURNING NOWAIT·SKIP LOCKED Native SQL ``` J3 API는 `io.backend.skeleton.jpa.postgresql` 또는 `io.backend.skeleton.jpa.hibernate` package에 격리하고 application service가 provider type을 직접 받지 않게 한다. ### 8.4 J4 Admin / Operations ```text Flyway migrate·validate·repair 승인 Concurrent Index Backfill COPY Partition Maintenance SQL Schema Drift Plan Regression Role Verification ``` J4는 일반 Runtime credential로 실행하지 않는다. `repair`, purge, destructive migration은 operation ID, operator, reason, dry-run 또는 승인 절차를 요구한다. --- ## 9. Core 공개 계약 ### 9.1 Operation Name ```java public record PersistenceOperationName(String value) { public PersistenceOperationName { if (value == null || !value.matches("[a-z][a-z0-9.-]{2,95}")) { throw new IllegalArgumentException("invalid persistence operation name"); } } } ``` Operation Name은 metric·trace·retry policy의 bounded key이다. 동적 SQL이나 Entity ID를 넣지 않는다. ### 9.2 Transaction Profile ```java public record TransactionProfile( String name, PropagationMode propagation, IsolationLevel isolation, Duration timeout, boolean readOnly, RetryProfile retryProfile) { } public enum PropagationMode { REQUIRED, MANDATORY, REQUIRES_NEW } public enum IsolationLevel { DEFAULT, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE } ``` Stable 기본은 `REQUIRED + READ_COMMITTED`. `REQUIRES_NEW`는 별도 opt-in profile과 pool pressure test를 요구한다. ### 9.3 Transaction Executor ```java public interface JpaTransactionExecutor { T execute( PersistenceOperationName operation, TransactionProfile profile, Supplier work); } ``` 일반 Use Case는 `@Transactional`을 사용할 수 있다. Programmatic retry·동적 profile이 필요한 Use Case는 executor를 사용한다. ### 9.4 Retry Policy ```java public interface JpaRetryPolicy { RetryDecision classify( JpaPersistenceException failure, TransactionAttempt attempt); } public record RetryDecision( RetryDisposition disposition, Duration delay, String reason) { } public enum RetryDisposition { RETRY_FULL_TRANSACTION, RECONCILE, FAIL } ``` ### 9.5 Query Observation ```java public interface QueryObservation { QueryScope start(QueryName queryName); } public interface QueryScope extends AutoCloseable { void rows(long count); void failure(Throwable failure); @Override void close(); } ``` --- ## 10. Entity 소유권과 Mapping 규칙 ### 10.1 소유권 플랫폼은 업무 Entity를 정의하지 않는다. 도메인 모듈이 다음을 소유한다. ```text @Table 이름 @Column 의미 PK·FK·Unique·Check 요구 Association Cascade Soft Delete Audit Index Requirement Lock 정책 ``` 플랫폼은 규칙, annotation helper, test fixture, static check만 제공한다. ### 10.2 기본 규칙 | 항목 | 기본 계약 | |---|---| | Access | Field Access | | Constructor | `protected` no-arg | | Entity class | non-final | | Persistent field | proxy 호환성을 해치지 않게 설계 | | API 반환 | Entity 금지, DTO·Projection 사용 | | `toString` | LAZY association 제외 | | equals/hashCode | mutable association·mutable business field 제외 | | Callback | 외부 HTTP·Messaging·File I/O 금지 | | BaseEntity | 전역 강제 금지 | | Soft Delete | 전역 강제 금지 | | Audit | opt-in | ### 10.3 equals/hashCode ID가 DB 생성이면 transient 상태에서 ID가 없음을 고려한다. mutable generated ID를 hash-based collection에 넣은 뒤 hashCode가 바뀌는 설계를 피한다. 권장 패턴은 domain-assigned immutable ID 또는 class + stable immutable key를 사용하되 각 Aggregate가 계약을 명시하는 것이다. ### 10.4 Entity 외부 노출 금지 다음은 금지한다. ```text Controller method 반환형이 @Entity Entity에 Jackson API contract annotation 사용 Lazy collection을 JSON serializer가 탐색 Entity를 Message payload로 직접 사용 Entity를 Redis value로 직접 Java serialize ``` --- ## 11. ID 생성 전략 ### 11.1 기본 선택 | 전략 | 등급 | 계약 | |---|---|---| | PostgreSQL Sequence | Stable 기본 | write-heavy Entity, JDBC Batch와 호환 | | JPA UUID | Stable | 분산 ID, insert 전 identity 확보 | | Application-assigned UUID/UUIDv7 | Stable | PG16~18 공통 방식 | | PostgreSQL 18 `uuidv7()` | J3 PG18 전용 | Stable Matrix 공통 기본으로 사용하지 않음 | | IDENTITY | 제한 | insert batching 제약; 소규모 write만 | | Composite ID | Domain-specific | 실제 composite identity일 때만 | | Natural ID | 별도 unique index | PK와 혼동하지 않음 | ### 11.2 Sequence 규칙 ```java @SequenceGenerator( name = "order_seq", sequenceName = "order_seq", allocationSize = 50 ) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq") ``` `allocationSize=50`은 universal constant가 아니라 reference profile이다. 실제 workload benchmark와 sequence increment가 일치해야 하며 플랫폼은 mismatch를 테스트한다. ### 11.3 UUIDv7 PG16·17·18 공통 지원을 위해 application-generated UUIDv7을 기본 extension으로 제공할 수 있다. DB-generated PG18 UUIDv7은 별도 Capability로 노출한다. --- ## 12. Value Mapping | 타입 | 기본 계약 | |---|---| | `Instant` | 서버 간 절대 시점 | | `OffsetDateTime` | offset 자체가 업무 의미일 때 | | `LocalDate` | 날짜 | | `LocalDateTime` | timezone 없는 업무 시간에만 | | `Duration` | converter/provider mapping contract test | | `UUID` | Stable | | Enum | STRING 또는 명시적 converter; ordinal 금지 | | Money | Embeddable value object | | Record Embeddable | JPA 3.2 Stable, provider round-trip test 필수 | | JSONB·Array·Range·INET | `jpa-postgresql` | | LOB | 일반 목록 fetch에서 제한 | | 암호화 값 | key rotation·queryability 포함 별도 capability | ### 12.1 Converter 규칙 - Converter는 null, unknown version, malformed value를 명확히 처리한다. - Java class name을 wire/schema 값으로 저장하지 않는다. - Enum rename은 DB migration 없이 수행하지 않는다. - `AttributeConverter` 내부에서 외부 I/O를 수행하지 않는다. --- ## 13. Association·Cascade·Collection ### 13.1 ToOne - 기본적으로 명시적 LAZY를 검토한다. - 실제 lazy proxy 동작을 Hibernate contract test로 보증한다. - FK nullable과 `optional`을 일치시킨다. - 목록 조회에서 필요한 ToOne은 EntityGraph·Fetch Join·Projection으로 가져온다. ### 13.2 ToMany - LAZY가 기본이다. - `List`, `Set`, `Map` 선택은 중복·순서 의미를 반영한다. - `List` 두 개를 동시에 join fetch하는 설계를 피한다. - collection 전체를 항상 필요한 aggregate가 아니면 Projection 또는 별도 Query를 사용한다. ### 13.3 Cascade ```text Cascade.ALL → 전역 기본값 금지 orphanRemoval → Parent가 Child lifecycle을 독점 소유할 때만 ManyToMany → 단순 연결 외에는 Join Entity 우선 ``` ### 13.4 양방향 관계 Owning side가 DB 변경을 결정한다. `addChild/removeChild` helper가 양쪽 in-memory graph를 항상 동기화해야 한다. --- ## 14. Persistence Context 계약 ```text Transient Managed Detached Removed ``` ### 14.1 기본 계약 ```text persist != merge find != getReference save != immediate INSERT flush != commit Entity mutation != immediate UPDATE ``` ### 14.2 Scope - transaction-scoped Persistence Context만 Stable이다. - Extended Persistence Context는 지원하지 않는다. - EntityManager는 thread-safe로 취급하지 않는다. - OSIV는 false다. - Lazy association 접근은 Application Transaction 내부에서만 허용한다. ### 14.3 Flush - Query 전에 AUTO flush가 발생할 수 있다. - 명시적 flush는 SQL 동기화 지점이지 commit 증거가 아니다. - Batch는 chunk마다 flush·clear한다. - Bulk DML 전 flush, 후 clear 또는 refresh한다. ### 14.4 Merge `merge()` 반환값이 managed instance다. 전달한 detached instance가 managed로 변한다고 가정하지 않는다. 신규 Entity 판정과 ID strategy를 이해하지 못한 무분별한 `save()` 사용을 코드리뷰 규칙으로 제한한다. --- ## 15. Transaction 경계 ### 15.1 기본 경계 ```text Controller → Application Service @Transactional → Domain Repository ``` Repository가 독립 업무 Transaction을 임의로 시작하지 않는다. ### 15.2 금지 경계 ```text Controller 전체 요청 Transaction Entity Listener가 새 Transaction 시작 동일 Bean self-invocation으로 Propagation 기대 DB Transaction 안에서 장시간 HTTP/Object Storage/Messaging 대기 ``` ### 15.3 Rollback Rule RuntimeException·Error 기본 rollback을 사용한다. Checked exception rollback이 필요하면 안정 application exception hierarchy 또는 `rollbackFor`를 명시한다. ### 15.4 Timeout 모든 write Transaction profile은 유한 timeout을 요구한다. read-only query도 long-running admin query가 아니라면 timeout을 지정한다. 숫자는 환경 SLO가 소유한다. --- ## 16. Propagation·Isolation ### 16.1 Propagation | Mode | 등급 | 규칙 | |---|---|---| | REQUIRED | Stable 기본 | Use Case Transaction | | MANDATORY | Advanced | 상위 Transaction 필수 내부 write service | | SUPPORTS | 제한 | read helper | | REQUIRES_NEW | Advanced 위험 | 별도 physical connection, pool capacity test 필수 | | NESTED | J3/JDBC savepoint | portable JPA로 광고하지 않음 | | NOT_SUPPORTED | Advanced | 긴 외부 I/O 분리 등에 제한 | ### 16.2 Isolation | Isolation | 기본 사용 | |---|---| | READ COMMITTED | 일반 업무 기본 | | REPEATABLE READ | transaction snapshot 일관성 필요 시 | | SERIALIZABLE | 좁은 핵심 invariant, abort/retry 전제 | | READ UNCOMMITTED | PostgreSQL profile에서 공개하지 않음 | ### 16.3 Self-invocation `this.method()` 호출은 Spring transaction proxy를 통과하지 않는다. Retry·REQUIRES_NEW method는 별도 Bean의 public method 또는 programmatic executor로 구성한다. --- ## 17. Commit 결과 불명확성 ### 17.1 상태 ```java public enum TransactionCompletionEvidence { NOT_STARTED, ACTIVE, COMMITTING, COMMITTED, ROLLED_BACK, UNKNOWN } ``` ### 17.2 감지 `EvidenceAwareJpaTransactionManager`가 `doCommit` 진입 전 evidence를 `COMMITTING`으로 기록한다. 다음 조건에서 `TransactionCompletionUnknownException`으로 변환한다. ```text SQLSTATE 40003 OR commit phase의 connection loss / transport exception AND rollback 또는 commit 여부를 driver가 확정하지 못함 ``` 일반 query 단계 connection failure를 completion unknown으로 과대 분류하지 않는다. ### 17.3 오류 계약 ```java public final class TransactionCompletionUnknownException extends JpaPersistenceException { private final String transactionKey; private final TransactionCompletionEvidence evidence; } ``` ### 17.4 복구 ```text 자동 Retry 금지 → transactionKey로 상태 조회 → Unique Constraint / Idempotency Record 확인 → 업무 Row 확인 → Outbox 확인 → 결과 확정 불가 시 Reconciliation Queue ``` `TransactionCompletionResolver`는 domain-specific SPI이며 Core가 업무 성공을 추측하지 않는다. --- ## 18. 안정 오류 모델과 SQLSTATE ```text JpaPersistenceException ├─ JpaEntityNotFoundException ├─ OptimisticConflictException ├─ PessimisticLockTimeoutException ├─ DeadlockDetectedException ├─ SerializationFailureException ├─ UniqueConstraintViolationException ├─ ForeignKeyViolationException ├─ CheckConstraintViolationException ├─ QueryTimeoutException ├─ TransactionTimeoutException ├─ ConnectionUnavailableException ├─ SchemaMismatchException ├─ DataCorruptionException └─ TransactionCompletionUnknownException ``` ### 18.1 공통 Metadata ```java public record JpaFailureContext( PersistenceOperationName operation, String sqlState, String constraintName, int transactionAttempt, boolean retryable, boolean completionUnknown, Duration elapsed, String traceId) { } ``` SQL parameter, Entity ID, Tenant ID, 전체 SQL 원문, PII는 exception message에 넣지 않는다. ### 18.2 SQLSTATE 분류 | 분류 | 대표 코드 | |---|---| | Serialization Failure | `40001` | | Statement Completion Unknown | `40003` | | Deadlock | `40P01` | | Unique Violation | `23505` | | Foreign Key Violation | `23503` | | Check Violation | `23514` | | Not Null Violation | `23502` | | Lock Not Available | `55P03` | 문자열 오류 메시지를 parsing하지 않고 SQLSTATE와 structured server error field를 사용한다. --- ## 19. Retry 정책 ### 19.1 Retry 대상 | 오류 | 기본 | |---|---| | Optimistic Conflict | 조건부 전체 Transaction Retry | | Serialization Failure | bounded 전체 Transaction Retry | | Deadlock | bounded 전체 Transaction Retry | | Lock Timeout | deadline·업무 정책에 따라 | | Transaction 시작 전 Connection 실패 | 제한적 Retry | | Unique Violation | 기본 Retry 금지; idempotent create면 기존 결과 조회 | | FK·Check Violation | Retry 금지 | | Query Timeout | 기본 Retry 금지 | | Schema Mismatch | Retry 금지 | | Completion Unknown | 자동 Retry 금지, reconcile | ### 19.2 안전 조건 ```text 전체 Use Case가 재계산 가능 AND 외부 irreversible side effect 없음 AND 새 Persistence Context 생성 AND 새 Transaction 생성 AND deadline 남음 AND retry budget 남음 ``` ### 19.3 Retry Profile ```java public record RetryProfile( String name, int maxAttempts, Duration initialBackoff, Duration maxBackoff, double multiplier, JitterMode jitter, Set retryableFailures) { } ``` ### 19.4 Annotation Adapter ```java @RetryableJpaTransaction(profile = "order-write") @Transactional public OrderId place(PlaceOrder command) { ... } ``` Retry interceptor는 Transaction interceptor보다 바깥에서 실행되어 각 attempt가 새 transaction을 생성해야 한다. 같은 클래스 self-invocation은 지원하지 않는다. --- ## 20. Optimistic Lock - mutable aggregate에는 `@Version` 사용을 기본 검토한다. - version은 API update command에 전달하거나 서버가 re-read 후 검증한다. - Conflict는 flush 또는 commit 시점에 나타날 수 있다. - Bulk DML은 version을 자동 검증하지 않는다. - 일부 Repository method만 Retry하지 않는다. ```java @Entity public class Order { @Version private long version; } ``` Retry 후에는 최신 Entity를 다시 조회하고 업무 규칙을 다시 계산한다. --- ## 21. Pessimistic Lock·PostgreSQL Lock Extension ### 21.1 표준 Lock ```text PESSIMISTIC_READ PESSIMISTIC_WRITE PESSIMISTIC_FORCE_INCREMENT ``` Transaction timeout, lock timeout, deadlock을 구분한다. ### 21.2 NOWAIT 대기 없이 즉시 실패해야 하는 use case에서 J3 Native Query로 제공한다. 일반 Repository API에 전역 옵션으로 넣지 않는다. ### 21.3 `FOR UPDATE SKIP LOCKED` 일반 일관된 조회가 아니라 work queue claim에만 제공한다. ```java public interface WorkClaimExecutor { List claimNextBatch( WorkQueueName queue, int size, Duration lease); } ``` ### 21.4 Lock Ordering 여러 Row를 잠글 때 stable key order를 사용한다. deadlock fixture로 규칙을 검증한다. --- ## 22. Constraint와 경쟁 조건 ### 22.1 최종 불변식 ```text Bean Validation → 조기 사용자 오류 Database Constraint → concurrency에서도 지켜지는 최종 invariant ``` ### 22.2 지원 ```text PRIMARY KEY FOREIGN KEY NOT NULL UNIQUE CHECK EXCLUSION Partial Unique Index NULLS NOT DISTINCT ``` ### 22.3 Exists-before-insert `exists()`는 UX 검증일 뿐 경쟁을 차단하지 않는다. Unique Constraint 위반을 안정 오류로 변환한다. ### 22.4 Constraint Catalog Constraint name을 bounded registry에 등록해 `user-email-active-unique` 같은 안정 code로 변환한다. raw table·column·value는 외부 오류에 노출하지 않는다. --- ## 23. Repository와 Query 선택 ### 23.1 Query 등급 | 등급 | 방식 | |---|---| | Q1 | Derived Query, JPQL, DTO/Interface Projection | | Q2 | Specification, Criteria, Querydsl, EntityGraph | | Q3 | Native SQL, Hibernate Query API, PostgreSQL CTE·Window·JSONB | | Q4 | Backfill, Maintenance, Bulk/Admin SQL | ### 23.2 선택 규칙 - Derived method가 업무 의미보다 SQL 구조를 설명하기 시작하면 Custom Query로 승격한다. - 고정 query는 JPQL과 DTO Projection을 우선한다. - optional filter 조합은 Specification 또는 Querydsl을 사용한다. - PostgreSQL plan·syntax 제어가 필요하면 J3 Native Query를 사용한다. - 모든 nontrivial query에는 `QueryName`을 등록한다. ### 23.3 Custom Fragment 플랫폼은 `BaseRepository`를 강제하지 않는다. 도메인이 `OrderRepositoryCustom`을 정의하고 구현에서 플랫폼 helper를 사용한다. ### 23.4 Dynamic Sort 사용자 문자열을 `JpaSort.unsafe()`에 연결하지 않는다. `SafeSortRegistry`가 허용된 field enum을 실제 JPA path로 변환한다. --- ## 24. Projection ### 24.1 DTO Projection 목록·read model의 기본 후보다. Entity 전체 hydration과 Lazy association을 줄인다. ### 24.2 Interface Projection 간단한 projection에 사용하되 nested association이 추가 query를 유발하는지 검증한다. ### 24.3 Dynamic Projection public API에서 임의 class를 입력받지 않는다. 등록된 projection catalog만 사용한다. ### 24.4 Entity 직접 반환 Application 내부 aggregate mutation use case에만 Entity를 사용하고 Web/API boundary에서는 DTO로 변환한다. --- ## 25. Fetch Plan과 N+1 ### 25.1 전략 ```text Mapping → 최소 graph Use Case Query → EntityGraph / Fetch Join / Projection / Batch Fetch ``` ### 25.2 선택표 | 상황 | 우선 선택 | |---|---| | 단일 aggregate 상세 | EntityGraph / Fetch Join | | 여러 ToOne | Fetch Join / EntityGraph | | 하나의 bounded ToMany | Fetch Join 검토 | | 여러 ToMany | DTO / 분할 Query / Batch Fetch | | 목록 화면 | DTO Projection | | 대규모 read model | Native Projection | | 반복 LAZY N+1 | explicit fetch plan 또는 batch fetch | ### 25.3 정량 지표 ```text statementCount entityLoadCount entityFetchCount collectionLoadCount collectionFetchCount returnedParents hydratedEntities rowsFromDatabase executionTime ``` ### 25.4 Fixture ```text 0 child 1 child 10~100 children shared ToOne multiple collections Zipf skew ``` SQL 1개라는 이유만으로 좋은 Query로 판정하지 않는다. --- ## 26. Hibernate 7.4 Collection Fetch Pagination 과거 Hibernate의 collection fetch join + pagination 전체 로드 문제를 영구 금지 규칙으로 복사하지 않는다. Stable baseline인 Hibernate 7.4 + PostgreSQL 16~18에서 다음을 검증한다. ```text generated SQL에 DB limit/subquery가 적용되는가 반환 parent 수가 정확한가 hydrated row 수가 허용 범위인가 count query가 정확한가 여러 collection Cartesian amplification이 없는가 ``` `hibernate.query.fail_on_pagination_over_collection_fetch`는 호환성 lane에서 회귀 감지를 위해 사용하되, 7.4 지원 경로를 무조건 차단하지 않는다. --- ## 27. Pagination·Cursor·Scroll ### 27.1 사용 기준 | 방식 | 용도 | |---|---| | Page | 작은 관리자 목록, total count 필요 | | Slice | count 불필요 일반 목록 | | Offset | 작은 데이터·얕은 page | | Keyset/Cursor | 대규모·시간순 목록 | | Scroll/Stream | batch/read processing | ### 27.2 Keyset 계약 ```java public record KeysetPageRequest( Optional after, int size, SortDirection direction) { } public record KeysetSlice( List items, Optional nextCursor, boolean hasNext) { } ``` 정렬이 `created_at DESC, id DESC`이면 Cursor도 두 값을 모두 포함한다. ### 27.3 Cursor 보안 Cursor는 versioned JSON을 Base64URL로 encoding하고 HMAC signature를 선택적으로 제공한다. raw SQL fragment를 포함하지 않는다. ### 27.4 Stream Stream은 transaction과 ResultSet 수명을 가진다. try-with-resources와 fetch size를 강제하고 Web/API에 그대로 반환하지 않는다. --- ## 28. JDBC Batch ### 28.1 의미 ```text saveAll != one SQL JDBC Batch != one SQL IDENTITY != batch-friendly ``` ### 28.2 Profile ```yaml backend: jpa: batch-profiles: order-import: jdbc-batch-size: 50 order-inserts: true order-updates: true flush-size: 50 clear-size: 50 ``` 숫자는 profile이 소유한다. Platform은 batch size와 flush/clear invariant를 검증한다. ### 28.3 Verification Hibernate statistics와 datasource proxy를 통해 실제 `executeBatch` 횟수와 statement 수를 확인한다. --- ## 29. Bulk DML ### 29.1 계약 ```text flush → JPQL / Native Bulk DML → clear → 필요 시 재조회 ``` ### 29.2 제한 - Bulk DML은 Entity callback과 optimistic version check를 자동 실행하지 않는다. - 도메인 invariant를 우회할 수 있으므로 Q4 또는 명시적 J2 API에서만 사용한다. - 영향 Row 수를 반환하고 예상 범위를 검증한다. ```java public interface BulkDmlExecutor { int execute(BulkOperationName operation, Runnable bulkStatement); } ``` --- ## 30. StatelessSession·COPY ### 30.1 StatelessSession Persistence Context·dirty checking이 없는 Hibernate extension이다. 일반 Repository를 대체하지 않고 대량 import/backfill에만 사용한다. ### 30.2 PostgreSQL COPY `jpa-postgresql-copy`는 JDBC connection을 명시적으로 unwrap해 COPY를 실행한다. J4 credential·operation name·row/byte cap·transaction policy를 요구한다. ### 30.3 선택표 ```text 일반 업무 write → JPA Entity 수천~수만 rows → JPA JDBC Batch 대규모 import/backfill → StatelessSession / COPY ``` --- ## 31. PostgreSQL Extension ### 31.1 Stable J3 ```text JSONB Array Range UUID ON CONFLICT RETURNING NOWAIT SKIP LOCKED Work Claim Window Function ``` ### 31.2 Advanced ```text INET Native Enum CTE / Recursive CTE Advisory Lock Generated Column Full-text Search ``` ### 31.3 Admin ```text Partial / Expression / INCLUDE Index Partition RLS Policy Extension 설치 ``` ### 31.4 Native SQL 제한 - 등록된 Query Name 필수 - 값은 parameter binding - 동적 table/column 문자열 금지 - row mapping 명시 - PG16·17·18 Contract Test 필수 --- ## 32. ON CONFLICT·RETURNING Upsert 의미를 단순 `save()`로 숨기지 않는다. ```java public interface PostgreSqlUpsertExecutor { R execute( NativeWriteName operation, C command, UpsertConflictTarget target); } ``` Conflict target, update columns, version semantics, returned columns을 호출 계약으로 고정한다. 동일 업무에 JPA Entity update와 Native Upsert를 섞을 때 Persistence Context를 clear하거나 해당 Entity를 다시 조회한다. --- ## 33. Flyway와 Schema Source of Truth ### 33.1 환경 정책 | 환경 | Flyway | Hibernate DDL | |---|---|---| | local PostgreSQL | migrate | validate | | H2 convenience | 선택 create/drop | 호환성 증거 아님 | | test | migrate | validate | | dev | migrate | validate | | staging | deployment migration | validate | | prod | 별도 migration role/process | validate | ### 33.2 금지 ```text prod ddl-auto update/create/create-drop runtime credential DDL 적용 완료 Versioned Migration 수정 startup auto repair ``` ### 33.3 Validation ```text checksum mismatch → fail missing migration → fail schema mismatch → fail unsupported DB version → fail ``` ### 33.4 Repair Flyway repair는 J4 승인 operation이다. 자동 실행하지 않고 operator, reason, before/after report를 남긴다. --- ## 34. 무중단 Migration ```text Expand → 새 nullable column/table/index Migrate → chunked backfill / dual read·write Contract → old column/index 제거, constraint 강화 ``` ### 34.1 Concurrent Index PostgreSQL `CREATE INDEX CONCURRENTLY`는 transaction block 밖에서 실행해야 하므로 non-transactional Flyway migration으로 명시한다. 실패한 invalid index 정리 runbook을 제공한다. ### 34.2 Snapshot Gate ```text empty → latest N-1 release → latest oldest supported snapshot → latest checksum modified → validation failure missing migration → validation failure failed non-transactional migration → documented recovery ``` --- ## 35. Constraint·Index·Query Plan ### 35.1 Index Requirement 각 도메인 Query는 다음 문서를 소유한다. ```text queryName predicate sort expected cardinality data distribution required index representative parameters expected plan shape ``` ### 35.2 Query Plan Testkit `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)`을 Test/Admin 계정으로 실행한다. 모든 Seq Scan을 실패시키지 않고 기대 node, row estimate ratio, sort spill, execution time budget을 Query별로 검증한다. ### 35.3 Plan Snapshot PostgreSQL minor version과 statistics에 따라 plan이 달라질 수 있으므로 raw JSON 전체 byte snapshot보다 normalized structural expectation을 사용한다. --- ## 36. Auditing·History·Soft Delete ### 36.1 Auditing `createdAt`, `createdBy`, `modifiedAt`, `modifiedBy`를 opt-in Embeddable 또는 annotation set으로 제공한다. 전역 BaseEntity 상속을 강제하지 않는다. ### 36.2 구분 ```text Technical Auditing != Business Audit != Entity History != Security Audit ``` ### 36.3 Envers 별도 모듈이며 Entity별 opt-in이다. 대용량 audit table, relation revision, 개인정보 보존 정책을 검토한 뒤 활성화한다. ### 36.4 Soft Delete 전역 filter를 제공하지 않는다. 도메인 상태 또는 `deletedAt`을 명시하고 필요하면 Flyway partial unique index를 사용한다. 물리 삭제·개인정보 파기와 복구 가능한 삭제를 구분한다. --- ## 37. Cache ### 37.1 기본 ```text L1 Persistence Context → 항상 L2 Cache → Entity별 opt-in Query Cache → OFF Application Cache → Redis 플랫폼 ``` ### 37.2 L2 Gate - `ENABLE_SELECTIVE` - Cache Region 명시 - 외부 DB writer가 있을 때 invalidation 정책 - Bulk DML 후 eviction - cluster node 일관성 - hit/miss/stale metric Redis application cache와 Hibernate L2 Cache는 같은 기능으로 취급하지 않는다. --- ## 38. Multi-tenancy·Replica Experimental ### 38.1 Multi-tenancy ```text Shared schema + tenant column PostgreSQL RLS Schema per tenant Database per tenant ``` Stable Core는 tenant context를 강제하지 않는다. Experimental module이 query, connection, cache, async propagation, admin cross-tenant access를 별도 검증한다. ### 38.2 Read Replica `readOnly=true`만으로 routing하지 않는다. Read-after-write, replica lag, transaction pinning, lock query primary 강제, consistency token을 설계한 뒤 별도 module에서 제공한다. --- ## 39. Connection Pool과 Hikari ### 39.1 관측 ```text active idle pending max acquire duration timeout connection lifetime transaction duration ``` ### 39.2 규칙 - pool size를 무작정 크게 하지 않는다. - `REQUIRES_NEW`는 outer + inner connection을 동시에 요구할 수 있다. - long transaction과 external I/O를 제거한다. - pending/acquire latency가 alert의 핵심이다. - DB max connections와 인스턴스 수를 함께 계산한다. ### 39.3 Startup Validation Production profile은 maximumPoolSize, connectionTimeout, maxLifetime 등의 명시 여부를 검사할 수 있다. Universal numeric default를 플랫폼 상수로 고정하지 않는다. --- ## 40. Observability ### 40.1 Metric ```text jdbc.connections.* hikaricp.* jpa.transaction.count duration rollback timeout retry completion-unknown jpa.query.count duration rows lock-wait jpa.fetch.entity collection jpa.batch.execute jpa.constraint.failure jpa.migration.duration ``` ### 40.2 Low-cardinality Tag 허용: ```text persistenceUnit operationName bounded entityType queryName outcome failureCategory isolation attemptBucket ``` 금지: ```text entityId userId tenantId 원문 SQL parameter 전체 동적 SQL PII constraint value ``` ### 40.3 Query Name 등록된 `QueryName`을 metric·trace의 primary key로 사용한다. SQL fingerprint는 secure diagnostic에서만 사용하고 metric label로 raw SQL을 사용하지 않는다. ### 40.4 Logging SQL parameter logging은 production 기본 OFF다. exception message에 parameter와 Entity state를 넣지 않는다. --- ## 41. Security ### 41.1 DB 역할 ```text Application Role ├─ SELECT ├─ INSERT ├─ UPDATE ├─ DELETE └─ required sequence usage Migration Role ├─ CREATE ├─ ALTER ├─ DROP └─ index / constraint / schema Read-only Role └─ bounded SELECT Admin Role └─ approved operations ``` ### 41.2 search_path Application role의 `search_path`를 고정하고 untrusted schema의 object resolution을 차단한다. startup verifier가 current_user, current_schema, search_path, schema CREATE privilege를 검사한다. ### 41.3 Injection 방어 ```text JPQL/Native values → parameter binding Dynamic sort → allowlist Dynamic table/column → enum/catalog mapping만 Entity → API mass binding 금지 ``` ### 41.4 Secret DB password는 secret manager/workload identity에서 주입하고 config·log·metric에 기록하지 않는다. --- ## 42. Spring Boot AutoConfiguration ### 42.1 Properties ```yaml backend: jpa: enabled: true require-postgresql: true open-in-view: false schema-management: VALIDATE transaction-profiles: {} retry-profiles: {} observability: hibernate-statistics: true sql-parameters: false security: verify-runtime-role: true verify-search-path: true ``` ### 42.2 Startup Failures ```text spring.jpa.open-in-view=true prod ddl-auto != validate/none unsupported PostgreSQL version runtime role has DDL privilege migration checksum mismatch required transaction profile timeout missing Experimental module enabled without feature flag ``` ### 42.3 Actuator ```text jpaPlatform ├─ database version ├─ provider version ├─ schema version ├─ OSIV state ├─ DDL mode ├─ role verification ├─ retry profile count └─ capability list ``` 민감 URL·username·schema secrets는 노출하지 않는다. --- ## 43. Test Architecture ### 43.1 층위 ```text Pure Unit → domain logic / classifier @DataJpaTest → quick mapping / repository wiring PostgreSQL Testcontainers → real semantics PG16·17·18 Matrix → release compatibility Toxiproxy / DB restart → failure evidence Migration Snapshot → real upgrade path ``` ### 43.2 공통 Fixture ```text JpaTestEntity VersionedEntity Parent / Child TwoCollectionsAggregate SkewedFeedFixture UniqueConstraintFixture WorkQueueFixture BatchEntity JSONB / Array / Range Entity ``` 공용 fixture만 testkit에 두고 업무 Entity를 플랫폼 production module에 넣지 않는다. ### 43.3 계약 목록 ```text Mapping Lifecycle Transaction Propagation Isolation Optimistic Lock Pessimistic Lock Deadlock Serialization Failure Constraint Race Query / Projection Fetch / N+1 Pagination Batch Bulk PostgreSQL Extension Flyway Security Pool Completion Unknown Observability ``` --- ## 44. Failure Injection ### 44.1 Deterministic Deadlock 두 transaction이 서로 반대 순서로 row를 잠그게 해 `40P01`을 재현한다. ### 44.2 Serialization Failure SERIALIZABLE에서 동일 invariant를 변경하는 transaction을 경쟁시켜 `40001`을 재현한다. ### 44.3 Completion Unknown DB proxy가 COMMIT 전, COMMIT 전송 중, server commit 후 response 전에 connection을 끊는 세 지점을 구분한다. 마지막 경우 자동 Retry가 발생하지 않고 `TransactionCompletionUnknownException`이 기록돼야 한다. ### 44.4 DB Restart Transaction 시작 전, query 중, commit 중 PostgreSQL restart를 구분한다. --- ## 45. 성능 인증 ### 45.1 Query ```text p50 / p95 / p99 statement count rows entity hydration collection fetch plan node buffer hit/read sort spill ``` ### 45.2 Write ```text records/sec JDBC batch count statement count flush count Persistence Context size heap allocation transaction duration ``` ### 45.3 Pool ```text active pending acquire p95/p99 REQUIRES_NEW saturation connection timeout ``` ### 45.4 Gate 성능 숫자는 workload별 문서가 소유한다. Platform release는 bounded memory, actual batching, no unbounded query, pool recovery, no retry storm을 증명한다. --- ## 46. 지원 Matrix와 Release Lane | Lane | 실행 | |---|---| | PR | PostgreSQL 16·18, mapping/query/transaction/migration smoke | | Nightly | PG16·17·18, failure injection, query plan, batch, security | | Release | 전체 Stable Contract, upgrade snapshots, performance, role separation | | Experimental | JPA4/Hibernate8, PG19, multitenancy, replica | ### 46.1 H2 H2는 빠른 local smoke에만 사용한다. H2-only test가 release gate를 대체하지 않는다. ### 46.2 Upgrade Spring Boot BOM patch 변경 시 Hibernate generated SQL, collection pagination, SQLSTATE mapping, Flyway validate, metrics 이름을 회귀 검증한다. --- ## 47. 완료 정의 다음 질문에 모두 구현·테스트 증거로 답할 수 있어야 한다. ```text 도메인이 Entity와 Repository를 소유하는가? 플랫폼이 GenericRepository를 만들지 않았는가? OSIV가 모든 운영 profile에서 꺼져 있는가? Transaction 경계가 Application Service인가? Retry가 새 Persistence Context에서 전체 Use Case를 실행하는가? Commit 결과 불명에서 자동 Retry가 금지되는가? SQLSTATE로 오류를 안정 분류하는가? Unique 경쟁을 DB Constraint가 최종 보장하는가? N+1과 Cartesian amplification을 정량 검증하는가? Hibernate 7.4 collection fetch pagination SQL을 실제 PG에서 검증하는가? Keyset cursor가 tie-breaker를 포함하는가? saveAll과 JDBC Batch를 구분하는가? Bulk DML 후 Persistence Context가 정리되는가? Flyway가 Schema Source of Truth인가? 운영 Runtime 계정으로 DDL이 실패하는가? PG16·17·18에서 Stable Contract를 통과하는가? Metric과 로그에 SQL parameter·PII가 없는가? Experimental 기능이 Stable dependency에 유입되지 않는가? ``` --- ## 48. ADR 목록 ```text ADR-JPA-001 Domain owns entities and repositories ADR-JPA-002 No generic repository wrapper ADR-JPA-003 Application service transaction boundary ADR-JPA-004 Full transaction retry only ADR-JPA-005 Transaction completion unknown is first-class ADR-JPA-006 OSIV disabled ADR-JPA-007 Use-case fetch plans ADR-JPA-008 Flyway owns schema changes ADR-JPA-009 PostgreSQL real-service contract tests ADR-JPA-010 PostgreSQL extensions are J3 ADR-JPA-011 L2 cache and Envers are opt-in ADR-JPA-012 Multitenancy and replicas are experimental ``` --- ## 49. 단계별 구현 순서 ```text Foundation → Error / Transaction Semantics → Mapping / Repository Rules → Query / Fetch / Pagination → Concurrency / Constraint → Batch / Bulk → PostgreSQL Extension → Flyway / Migration → Observability / Security → Advanced Opt-in → PostgreSQL Matrix / Failure / Performance → Experimental Expansion ``` Stable 계획의 Task가 모두 끝난 뒤 Experimental 계획으로 이동한다. --- ## 50. 요구사항 추적표 | 조사 결론 | 설계 위치 | 구현 계획 | |---|---|---| | GenericRepository 금지 | 1, 5, 7, 8 | Task 1, 19, 53 | | J1~J4 계층 | 8 | Task 1, 53 | | Entity Mapping | 10~13 | Task 13~16 | | Persistence Context | 14 | Task 11, 16, 19 | | Application TX | 15~16 | Task 5~9 | | Completion Unknown | 17 | Task 6, 10, 50 | | SQLSTATE Error | 18 | Task 3~4, 29~32 | | Full-TX Retry | 19 | Task 7~9 | | Optimistic/Pessimistic | 20~21 | Task 29~31 | | Query·Projection | 23~24 | Task 18~21 | | Fetch·N+1 | 25~26 | Task 22~25 | | Pagination | 27 | Task 26~28 | | Batch·Bulk | 28~30 | Task 33~36 | | PostgreSQL Extension | 31~32 | Task 30~31, 37~40 | | Flyway | 33~34 | Task 41~43 | | Query Plan | 35 | Task 44 | | Audit·Cache | 36~37 | Task 17, 46~47 | | Multitenancy·Replica | 38 | Experimental Plan | | Pool | 39 | Task 12, 51 | | Observability | 40 | Task 48 | | Security | 41 | Task 45 | | Test·Release | 43~46 | Task 49~53 | --- ## 51. 구현 시 금지되는 즉흥 결정 ```text 새 BaseRepository를 만들어 모든 Repository가 상속하게 한다. Entity를 Controller 응답에 바로 사용한다. OSIV를 편의를 위해 켠다. Deadlock에서 Repository method 하나만 retry한다. Commit 응답 유실을 connection transient로 보고 자동 retry한다. 모든 ToOne을 EAGER로 바꾼다. Collection Fetch Join + Pagination을 버전 검증 없이 무조건 금지하거나 허용한다. saveAll 호출만 보고 batching을 완료로 판정한다. Flyway migration 대신 ddl-auto update를 켠다. H2 테스트 통과로 PostgreSQL 지원을 선언한다. Native SQL 문자열에 사용자 입력 sort/column을 연결한다. Runtime DB 사용자에게 DDL 권한을 준다. ReadOnly annotation만 보고 replica로 routing한다. 모든 Entity에 Soft Delete나 Envers를 강제한다. ``` --- ## 52. 설계 승인 상태 이 설계는 첨부 심층 리서치와 사용자가 반복적으로 확정한 Backend Skeleton 방향을 기준으로 작성됐다. 구현자는 Stable 계획을 순서대로 수행하고, 각 Task의 계약 테스트가 통과하기 전 다음 Task의 의미론을 임의로 완화하지 않는다. --- # 부록 A. 심층 리서치 원문 보존본 > 아래 내용은 설계 판단의 원본 근거를 보존하기 위해 첨부 파일을 변경 없이 수록한 것이다. 상단 설계 본문이 구현 계약이며, 충돌 시 상단 설계 본문을 따른다. # JPA 관계형 영속성 플랫폼 심층 리서치 이번 조사의 결론부터 정리하면, `jpa`는 **`JpaRepository`를 한 번 더 감싸는 공통 Repository 라이브러리로 설계해서는 안 됩니다.** Spring Data JPA 자체가 이미 Repository, Query Method, Pagination, Auditing, Custom Repository, Querydsl 통합 등을 제공하고 있으므로, 공통 플랫폼이 다시 CRUD 추상화를 만드는 것은 기능 중복과 추상화 누수를 동시에 만듭니다. 현재 Spring Data JPA 공식 프로젝트 페이지의 안정 버전은 `4.1.0`입니다. citeturn20view0 따라서 권장 구조는 다음과 같습니다. ```text Domain / Application ├─ Entity ├─ Embeddable ├─ Repository Interface ├─ Domain Query ├─ Index Requirement └─ Domain-specific Lock / Soft-delete / Audit policy │ ▼ JPA Persistence Platform ├─ jpa-core │ ├─ transaction policy │ ├─ persistence-context policy │ ├─ error model │ └─ observability contract ├─ jpa-spring-data │ ├─ repository fragments │ ├─ specification │ ├─ projection │ └─ auditing support ├─ jpa-hibernate │ ├─ batching │ ├─ fetch extensions │ ├─ statistics │ └─ StatelessSession ├─ jpa-postgresql │ ├─ PostgreSQL types │ ├─ native write/query │ ├─ lock extensions │ └─ keyset pagination ├─ jpa-migration-flyway │ ├─ migration │ ├─ validation │ └─ schema release gate └─ jpa-testkit ├─ PostgreSQL Testcontainers ├─ query-count assertions ├─ concurrency fixtures ├─ migration fixtures └─ failure injection ``` 핵심 설계 질문도 사용자께서 제시한 방향이 맞습니다. > **현재 EntityManager 안에서 성공했는가가 아니라, 데이터베이스에 어떤 상태가 확정되었는지, 충돌·Deadlock·Serialization Failure 뒤 전체 업무 트랜잭션을 다시 실행해도 되는지, Commit 결과조차 알 수 없을 때 어떤 증거로 복구할지를 플랫폼 계약으로 만들어야 합니다.** ## 지원 기준과 공개 계층 **기술 기준선.** 2026년 8월 기준 Spring Data JPA 공식 페이지는 `4.1.0`을 표시하고 있으며, Spring Boot `4.1.0`의 dependency management를 사용하는 것이 개별 Hibernate/Flyway/Hikari 버전을 임의로 조립하는 것보다 안전한 기준선입니다. Boot 4.1 BOM은 HikariCP `7.0.2`를 포함하고 있으며, 같은 BOM이 Spring Data JPA, Hibernate ORM, Flyway 등 Spring 생태계의 검증된 조합을 관리합니다. citeturn20view0turn20view1 Hibernate ORM의 현재 안정 계열은 **7.4**이며, Hibernate의 7.4 문서는 현재 `7.4.6.Final`을 기준으로 제공되고 있습니다. Jakarta Persistence의 완성된 현재 규격은 **3.2**이고, Persistence 4.0은 아직 개발 중이며 2026년 후반을 목표로 하고 있으므로 Stable 계약으로 고정하면 안 됩니다. citeturn13search0turn7search2turn7search1 따라서 지원 매트릭스는 다음이 적절합니다. | 구성요소 | 권장 등급 | 기준 | |---|---|---| | Java 21 | **Stable baseline** | 플랫폼 언어 기준선 | | Spring Boot BOM | **Stable baseline** | 개별 dependency 임의 조합 금지 | | Spring Data JPA 4.1 | **Stable** | Repository·Projection·Specification·Auditing의 기본 진입점 citeturn20view0 | | Jakarta Persistence 3.2 | **Stable** | 표준 JPA 계약 citeturn7search2turn17search0 | | Hibernate ORM 7.4 | **Stable provider** | 기본 JPA Provider citeturn13search0 | | Hibernate Validator | **Stable** | Bean-level early validation | | Flyway | **Stable migration** | 실제 Schema 변경 Source of Truth | | HikariCP | **Stable pool** | Boot-managed pool | | PostgreSQL 16·17·18 | **Stable DB matrix** | 세 버전 모두 공식 지원 기간 내이며 PostgreSQL은 일반적으로 major 버전을 약 5년 지원 citeturn0search3turn13search5 | | H2 | **Local Convenience** | PostgreSQL 호환성 증명에 사용하지 않음 | | Testcontainers PostgreSQL | **Required** | 실제 PostgreSQL 의미론을 검증하는 Contract 환경 | | Jakarta Persistence 4.0 | **Experimental** | 아직 개발 중 citeturn7search1 | | Hibernate ORM 8 | **Experimental** | 7.4 Stable 이후 차세대 호환성 lane | | MySQL·MariaDB·Oracle | **Future Profile** | 초기 공통 계약 밖 | PostgreSQL 18이 현재 정식 문서의 current 버전이고 PostgreSQL 19는 2026년 8월 현재 beta 단계이므로, **PG19를 Stable에 포함해서는 안 됩니다.** PostgreSQL 공식 문서는 현재 18을 Current로 표시하고 19 Beta 문서를 별도로 제공합니다. citeturn13search5 **H2의 위치도 명확해야 합니다.** H2는 빠른 로컬 개발이나 순수 Mapping smoke test에는 쓸 수 있지만, PostgreSQL의 locking, SQLSTATE, partial index, `NULLS NOT DISTINCT`, JSONB, Array, Range, `SKIP LOCKED`, isolation, query planner 동작을 증명하지 못합니다. Stable 선언은 실제 PostgreSQL 테스트를 통해서만 이루어져야 합니다. 공개 계층은 다음처럼 나누는 것이 가장 자연스럽습니다. | 계층 | 공개 범위 | 대표 기능 | 정책 | |---|---|---|---| | **J1 Standard Persistence** | 일반 애플리케이션 | Spring Data Repository, JPQL, Projection, 기본 Transaction, `@Version` | 기본 경로 | | **J2 Advanced Persistence** | 명시적 고급 사용 | Specification, EntityGraph, Query Hint, Pessimistic Lock, Batch, Scrolling | 공통 정책 적용 | | **J3 Provider / DB Extension** | 제한형 | Hibernate Session, StatelessSession, JSONB, `ON CONFLICT`, `SKIP LOCKED`, Native SQL | 별도 모듈·명시적 의존성 | | **J4 Admin / Operations** | 운영 계층 | Flyway, Index 생성, Backfill, Partition, maintenance SQL | 일반 서비스 코드에서 금지 | Spring Data의 `CrudRepository.save()` 자체도 Entity가 신규인지 판단해 `EntityManager.persist()` 또는 `merge()`를 호출합니다. 즉 `GenericRepository.save()`를 한 계층 더 추가해도 JPA의 `persist`/`merge` 차이를 없애지 못하며 오히려 숨길 뿐입니다. citeturn9search0 **권장 공개 구조는 따라서 다음입니다.** ```java // Domain owns this public interface OrderRepository extends JpaRepository, OrderRepositoryCustom { Optional findByOrderNumber(OrderNumber orderNumber); } // Domain-specific custom query contract public interface OrderRepositoryCustom { Slice findRecentOrders(OrderCursor cursor, int size); } // J3 implementation may internally use: // EntityManager // Hibernate Session // PostgreSQL native SQL // // but those types do not leak into application services. ``` `EntityManager`를 금지할 필요는 없습니다. 다만 **애플리케이션 전체에 자유롭게 노출하는 것이 아니라 Custom Repository 구현 또는 J3 Extension 내부에서 사용**하는 것이 좋습니다. Spring Data 역시 단순 Repository를 넘는 데이터 접근 코드를 custom fragment로 결합할 수 있도록 설계되어 있습니다. citeturn20view0 ## Entity Mapping과 Persistence Context 계약 Jakarta Persistence 3.2는 Entity가 top-level 또는 static nested class여야 하고, public/protected no-arg constructor가 필요하며, portable Entity는 non-final class와 non-final persistent members를 사용하도록 규정합니다. Field access와 property access는 annotation 위치에 의해 결정되고, 계층 안에서 이를 암묵적으로 뒤섞으면 동작이 정의되지 않으므로 접근 전략을 일관되게 유지해야 합니다. citeturn17search0 따라서 Entity Mapping 기본 규칙은 다음이 적절합니다. | 항목 | 플랫폼 기본 정책 | |---|---| | Access | **Field Access 기본**, 특별한 이유가 있을 때만 `@Access(PROPERTY)` | | Entity final | 금지 | | no-arg constructor | `protected` 권장 | | Entity API 직렬화 | 기본 금지 | | Controller 반환 | DTO / Projection 사용 | | `toString()` | LAZY association 포함 금지 | | `equals/hashCode` | mutable association·mutable business field 포함 금지 | | Entity callback | 데이터 정규화·감사 필드 같은 로컬 작업만; HTTP/Messaging 등 외부 I/O 금지 | | BaseEntity | 전역 강제 상속 금지 | | Soft Delete | 전역 강제 금지 | | Audit | Opt-in capability | | Association | 기본적으로 use-case fetch plan과 분리 | Jakarta Persistence 3.2에서는 `Instant`, `Year`, `UUID` 등이 표준 basic type에 포함되고, **Java record를 Embeddable로 사용할 수 있습니다.** 반면 record는 Entity가 될 수 없습니다. 따라서 record Embeddable은 Stable JPA 3.2 기능으로 볼 수 있지만, 실제 Boot-managed Hibernate 조합의 round-trip·dirty checking·nested embeddable 계약 테스트를 통과하는 것을 release gate로 두는 것이 안전합니다. citeturn17search0turn7search2 **Value Mapping 권고안은 다음과 같습니다.** | Java/domain type | 권장 | |---|---| | `Instant` | Stable, 서버 간 절대 시점 | | `OffsetDateTime` | Stable, offset 자체가 업무적으로 필요한 경우 | | `LocalDate` | Stable | | `LocalDateTime` | timezone 없는 업무 시간에만 사용 | | `Duration` | Converter 또는 provider mapping 검증 | | `UUID` | Stable | | Enum | 기본은 STRING 또는 명시적 converter; ordinal 금지 권고 | | Money | Embeddable/value object | | JSONB | `jpa-postgresql` | | Array | `jpa-postgresql` | | Range | `jpa-postgresql` | | INET | Advanced PostgreSQL extension | | LOB | 일반 Entity 조회에서 신중하게 사용 | | Encrypted value | AttributeConverter만으로 끝내지 말고 key rotation·queryability 포함 별도 capability | **ID 생성 전략에서 PostgreSQL용 기본값은 `SEQUENCE`가 가장 안전합니다.** Hibernate 7.4는 `IDENTITY` 사용 시 INSERT JDBC batching을 수행할 수 없다고 명시하며, `IDENTITY`는 `persist()` 시 식별자를 얻기 위해 INSERT가 즉시 필요할 수 있습니다. 반대로 sequence 계열은 insert 전에 ID를 확보해 batching과 write-behind를 유지하기 쉽습니다. citeturn14view0turn14view1 | ID 전략 | Batch | 분산 생성 | Insert 전 ID | 권장 범위 | |---|---:|---:|---:|---| | `SEQUENCE` | 좋음 | DB 의존 | 가능 | **PostgreSQL 기본 추천** | | `IDENTITY` | 나쁨 | DB 의존 | 불가 | 소규모 write에 한정 | | JPA `UUID` | 좋음 | 가능 | 가능 | Stable | | Application-assigned UUID | 좋음 | 가능 | 가능 | Stable | | UUIDv7 | 좋음 | 가능 | 가능 | PG16~18 공통 생성 방식을 별도 정의 | | Composite ID | 상황별 | 상황별 | 가능 | 도메인이 실제 composite identity인 경우만 | | Natural ID | 별도 index 필요 | 상황별 | 보통 가능 | PK와 혼동하지 않음 | PostgreSQL 18은 native `uuidv7()`을 제공하지만 PostgreSQL 16·17 Stable 범위 전체에서 공통으로 사용할 수 있는 기능은 아닙니다. 따라서 DB-generated UUIDv7을 J1 표준으로 만들지 말고, **application-generated UUIDv7 또는 PostgreSQL 18 전용 extension**으로 분류해야 합니다. 또한 JPA의 `GenerationType.UUID`가 곧 UUIDv7을 뜻하지도 않습니다. citeturn8search0turn8search12turn17search0 Sequence를 쓸 때는 `allocationSize`를 명시적으로 관리해야 합니다. 값은 글로벌 상수 하나보다 write profile에 맞춰 benchmark해야 하며, 여러 프로세스가 같은 sequence를 이용하는 경우 allocation 동작도 실제 PostgreSQL에서 검증해야 합니다. **Association 정책은 FetchType보다 Fetch Plan이 더 중요합니다.** JPA에서 `EAGER`는 반드시 eager fetch 해야 하는 요구이고 `LAZY`는 provider에 대한 hint입니다. EntityGraph는 query/find 단위 fetch plan을 표현하기 위한 표준 기능입니다. 따라서 mapping에서 연관관계를 무조건 EAGER로 만들어 use case마다 필요 없는 graph를 끌고 오는 것보다, 최소 graph + explicit fetch plan을 기본으로 삼는 것이 적절합니다. citeturn17search0 권장 Association 계약은 다음과 같습니다. ```text ToOne → 기본적으로 명시적 LAZY를 검토 → 실제 proxy/lazy 동작을 Hibernate Contract Test로 보증 ToMany → LAZY → List 화면에서는 DTO Projection / EntityGraph / Fetch Join 선택 Cascade → lifecycle이 실제로 동일한 aggregate 내부에서만 Cascade.ALL → 전역 기본값 금지 orphanRemoval → child lifecycle을 parent가 독점 소유할 때만 ManyToMany → 단순 연결 외에는 join entity 우선 검토 ``` JPA 규격상 양방향 관계에서 persistence 동작에 중요한 것은 owning side이며, 양쪽 in-memory 객체 graph를 서로 맞추는 책임은 애플리케이션에게 있습니다. 따라서 양방향 association에는 `addChild/removeChild` 같은 편의 메서드 계약을 두는 것이 좋습니다. citeturn12view0 **Persistence Context 계약도 API 문서보다 중요합니다.** `persist`, `merge`, `flush`, `commit`은 서로 다른 의미를 가집니다. `merge()`는 detached instance 자체를 managed로 바꾸는 것이 아니라 그 state를 managed instance에 복사하는 방식이고, `flush()`는 Persistence Context를 DB와 동기화하지만 transaction commit과 동일하지 않습니다. citeturn12view0 플랫폼 계약은 아래처럼 고정하는 것이 좋습니다. ```text Persistence Context → transaction-scoped Extended Persistence Context → Stable 비지원 EntityManager → thread-safe로 간주하지 않음 OSIV → 명시적으로 false Lazy loading → application transaction 내부 Web/API → Entity 직접 반환 금지 flush() → SQL 반영 시점 제어 → commit 보장 아님 clear() → managed state 제거 refresh() → DB state 재조회 Bulk DML → flush → bulk DML → clear 또는 필요한 entity refresh ``` JPA Bulk UPDATE/DELETE는 persistence context를 자동으로 동기화하지 않고 optimistic locking check도 자동 적용하지 않습니다. 따라서 Bulk DML 후 이미 managed 상태인 Entity를 계속 사용하는 것은 stale-state 오류의 직접 원인이 됩니다. citeturn12view1 `Open Session in View`는 **플랫폼 차원에서 명시적으로 비활성화**하는 것이 좋습니다. 중요한 것은 Spring Boot의 특정 버전 기본값에 의존하지 않고 다음 invariant를 만드는 것입니다. ```properties spring.jpa.open-in-view=false ``` 그 결과 `LazyInitializationException`은 Web serialization에서 우연히 발생하는 production 장애가 아니라, use case에 필요한 Fetch Plan을 Repository 계층에서 빠뜨렸다는 **개발 시점 계약 위반**으로 취급할 수 있습니다. ## Transaction·Lock·Retry와 Commit 불명확성 Spring Data JPA도 여러 Repository를 묶는 unit of work에서는 service/facade 수준에 transaction boundary를 두는 방식을 권장합니다. 외부 transaction이 있으면 내부 Repository 설정보다 외부 unit-of-work transaction이 실제 경계를 결정합니다. citeturn15search1 따라서 기본 계약은 다음입니다. ```text Controller │ ▼ Application Service ← @Transactional boundary │ ├─ Repository A ├─ Repository B └─ Domain operation ``` 그리고 다음 구조는 피해야 합니다. ```text @Transactional DB UPDATE → 3초 HTTP 호출 → Object Storage 전송 → Kafka publish → DB COMMIT ``` Spring Framework는 transaction context가 일반적인 remote call까지 전파되는 모델이 아니며, 긴 외부 작업을 로컬 DB transaction 내부에 넣으면 connection과 row lock의 보유 시간이 외부 시스템 latency에 종속됩니다. DB 변경과 메시지 발행을 연계해야 한다면 XA처럼 보이게 숨기기보다 Transactional Outbox를 사용하는 것이 더 안전한 경계입니다. citeturn15search7 **Propagation 정책은 다음 정도로 강하게 제한하는 것이 좋습니다.** | Propagation | 등급 | 플랫폼 규칙 | |---|---|---| | `REQUIRED` | 기본 | Application use case 기본 | | `MANDATORY` | 선택 Stable | 반드시 상위 transaction이 필요한 내부 write service | | `SUPPORTS` | 제한 | read helper 정도 | | `REQUIRES_NEW` | 주의 | 명시적 독립 commit이 업무적으로 필요한 경우만 | | `NESTED` | Advanced | JPA portable 기능처럼 취급하지 않고 savepoint 호환성 검증 | | `NOT_SUPPORTED` | Advanced | 긴 외부 I/O 분리 등에 제한적으로 사용 | Spring의 `REQUIRES_NEW`는 별도의 physical transaction과 resource를 사용합니다. 외부 transaction이 connection을 붙잡은 채 내부 transaction이 또 다른 connection을 요구하므로, 동시 호출이 많으면 pool exhaustion 또는 deadlock으로 이어질 수 있다고 Spring 문서가 명시적으로 경고합니다. `NESTED`는 JDBC savepoint를 기반으로 하는 의미론입니다. citeturn15search0 또한 Spring의 기본 proxy transaction model에서는 **self-invocation이 transactional interception을 거치지 않습니다.** 따라서 같은 클래스 안에서 `this.someRequiresNewMethod()`를 호출하고 별도 transaction이 생성된다고 가정하는 코드는 금지 대상이 되어야 합니다. citeturn15search6turn15search9 Spring `@Transactional`의 기본값은 `REQUIRED`, isolation `DEFAULT`, read-write이며, 기본 rollback 규칙은 `RuntimeException`과 `Error`입니다. Checked exception까지 rollback해야 하는 업무에서는 `rollbackFor` 또는 안정적인 application exception hierarchy를 명시해야 합니다. citeturn15search6 **Isolation은 PostgreSQL 실제 의미론을 기준으로 계약해야 합니다.** | Isolation | PostgreSQL 관점 | 권장 | |---|---|---| | `READ COMMITTED` | 기본 isolation | 일반 업무 기본 | | `REPEATABLE READ` | snapshot 내 일관성 강화; concurrent update 시 serialization failure 가능 | 명시적 use case | | `SERIALIZABLE` | serial execution과 동등한 결과를 목표로 하며 abort/retry 가능 | 좁은 핵심 invariant | | `READ UNCOMMITTED` | PostgreSQL에서는 실질적으로 READ COMMITTED 의미 | 공개 profile로 권장하지 않음 | PostgreSQL은 Repeatable Read/Serializable에서 concurrency anomaly를 해결하기 위해 transaction을 abort시킬 수 있으며, Serializable 문서는 실패한 경우 **transaction 전체를 처음부터 다시 실행**해야 한다고 명시합니다. citeturn13search9turn8search6 이 때문에 Retry 단위는 다음과 같아야 합니다. ```text 잘못된 방식 @Transactional service() repository.update() // 실패 retry(repository.update) // 일부 SQL만 재실행 권장 방식 retryTransaction( () -> applicationUseCase() ) ``` 즉 **새 Persistence Context와 새 DB transaction에서 전체 use case를 재실행**해야 합니다. **Optimistic Lock은 기본 동시성 제어의 첫 번째 선택지**로 두는 것이 적절합니다. ```java @Version private long version; ``` JPA는 optimistic version check가 flush 또는 commit 시점까지 지연될 수 있음을 허용하며, 충돌 시 `OptimisticLockException`을 발생시킵니다. 즉 update method 호출 직후 충돌이 반드시 드러난다고 가정하면 안 됩니다. citeturn12view2 Optimistic retry는 다음 조건을 모두 만족해야 합니다. ```text 전체 application transaction을 다시 계산할 수 있음 AND 외부 irreversible side effect가 없음 AND 업무 deadline이 남아 있음 AND retry 횟수가 제한됨 ``` **Pessimistic Lock은 다음 계약으로 제한**하는 것이 좋습니다. | 기능 | 용도 | 위험 | |---|---|---| | `PESSIMISTIC_READ` | shared-style lock 요구 | 장시간 transaction | | `PESSIMISTIC_WRITE` | 쓰기 경쟁 직렬화 | lock wait·deadlock | | `PESSIMISTIC_FORCE_INCREMENT` | version까지 증가 | contention | | `NOWAIT` | 기다리지 않고 즉시 실패 | 실패율 증가 | | `SKIP LOCKED` | work queue형 competing worker | 일반 조회에는 inconsistent view | JPA의 pessimistic lock은 transaction 종료까지 유지되어야 하며, database transaction rollback 수준의 lock 실패와 statement 수준 timeout을 `PessimisticLockException`/`LockTimeoutException`으로 구분합니다. PostgreSQL의 `SKIP LOCKED`는 일관된 일반 조회 view를 제공하지 않기 때문에 queue-like consumer에 적합하다고 공식 문서가 명시합니다. citeturn12view3turn8search4turn8search5 따라서 `SKIP LOCKED`를 `findAllUnlocked()` 같은 공통 Repository API로 제공해서는 안 되고, ```text jpa-postgresql └─ WorkClaimExtension └─ claimNextBatch(...) ``` 처럼 semantics가 드러나는 API로 한정하는 것이 좋습니다. **DB Constraint는 최종 불변식입니다.** 다음 코드는 경쟁을 막지 못합니다. ```java if (!repository.existsByEmail(email)) { repository.save(new User(email)); } ``` 동시에 두 transaction이 `false`를 읽을 수 있기 때문입니다. 최종 uniqueness는 `UNIQUE` constraint/index가 담당하고 애플리케이션의 `exists` 검사는 빠른 UX validation 정도로만 사용해야 합니다. PostgreSQL은 unique constraint/primary key에 unique index를 자동 생성하며, `NULLS NOT DISTINCT`를 사용해 NULL도 동일 값처럼 취급하는 unique semantics를 제공할 수 있습니다. citeturn17search1 PostgreSQL 전용 partial unique index가 필요하다면 Entity annotation에 억지로 추상화하지 말고 Flyway migration으로 관리합니다. ```sql CREATE UNIQUE INDEX uq_user_active_email ON users (email) WHERE deleted_at IS NULL; ``` 이는 Soft Delete와 Unique Constraint 충돌을 해결하는 대표적인 PostgreSQL extension 패턴입니다. **Commit 결과 불명확성은 별도 오류로 모델링해야 합니다.** 예를 들어: ```text Application │ │ COMMIT ▼ PostgreSQL │ │ 실제 commit 완료 X TCP connection loss │ Application └─ commit 결과를 받지 못함 ``` 이때 같은 업무를 자동 재실행하면 이미 commit된 INSERT나 상태 변경을 두 번 실행할 수 있습니다. PostgreSQL의 SQLSTATE 체계 자체에도 `40003 statement_completion_unknown`이라는 별도 completion-unknown condition이 정의되어 있고, SQLSTATE는 문자열 오류 메시지보다 안정적인 기계 판독 기준으로 사용하도록 PostgreSQL이 권고합니다. citeturn13search1 따라서 플랫폼에는 JPA 표준 exception이 아닌 **플랫폼 고유 분류**로 다음을 두는 것을 권장합니다. ```java final class TransactionCompletionUnknown extends JpaPersistenceException { String operationName; String transactionKey; String sqlState; boolean commitAttempted; String traceId; } ``` 이 오류에 대한 정책은 명확해야 합니다. ```text TransactionCompletionUnknown → 자동 Retry 금지 → 동일 업무 key로 상태 재조회 → Unique Constraint / Idempotency Record 확인 → Outbox / transaction record 확인 → 결과 확정 불가 시 reconciliation ``` 즉 error taxonomy는 단순히 “transient/non-transient” 두 종류로 나누면 부족합니다. ## Query·Fetch·Pagination과 Write 성능 Spring Data JPA는 derived query, custom query, pagination, custom repository, Querydsl integration 등을 공식 지원하므로, 플랫폼의 역할은 이를 하나의 API로 대체하는 것이 아니라 **어떤 레벨에서 무엇을 쓸지 결정하는 것**입니다. citeturn20view0 권장 Query 등급은 다음과 같습니다. | 등급 | 방식 | 사용 기준 | |---|---|---| | Q1 | Derived Query | 짧고 명확한 equality/range 조회 | | Q1 | JPQL `@Query` | 고정 query, domain repository 안에서 읽기 쉬운 경우 | | Q1 | DTO Projection | 목록·read model 기본 후보 | | Q2 | Specification | optional filter 조합 | | Q2 | Criteria | framework-level dynamic query | | Q2 | Querydsl | 복잡한 type-safe dynamic query의 선택 capability | | Q2 | EntityGraph | use-case fetch plan | | Q3 | Native SQL | PostgreSQL 기능·계획 통제가 필요한 경우 | | Q3 | Hibernate Query API | provider 기능 필요 시 | | Q4 | Bulk/Admin SQL | backfill, maintenance | Derived query에 “최대 단어 수” 같은 임의 숫자를 플랫폼에 박는 것은 좋지 않습니다. 대신 **method name이 업무 의미보다 SQL 구조를 설명하기 시작하면 custom query로 승격한다**는 코드리뷰 규칙이 더 안정적입니다. Dynamic sort는 field allowlist가 필요합니다. Spring Data는 일반적인 domain property 기반 `Sort`와 명시적으로 unsafe한 expression sort를 구분하기 때문에, 사용자 입력 문자열을 `JpaSort.unsafe()` 등에 직접 연결하지 않는 정책이 필요합니다. citeturn18search12 **Fetch 전략은 Mapping이 아니라 Use Case 계약으로 관리**해야 합니다. | 상황 | 우선 선택 | |---|---| | 단일 aggregate 상세 | EntityGraph / Fetch Join | | 여러 ToOne | Fetch Join 또는 EntityGraph | | 하나의 필요한 ToMany | Fetch Join 검토 | | 여러 ToMany | DTO / 다단계 query / batch fetch | | 목록 화면 | DTO Projection | | 페이지형 parent + collection | Hibernate 버전과 SQL plan 검증 | | 대규모 read model | Projection / Native Query | | 반복 LAZY N+1 | Batch Fetch 또는 explicit fetch plan | Hibernate는 여러 to-one fetch를 한 query에서 사용하는 것은 비교적 안전하지만, 여러 collection을 병렬 join fetch하면 DB 레벨 Cartesian product가 발생해 row 수와 hydration cost가 크게 증가할 수 있음을 문서화하고 있습니다. citeturn13search14 여기에는 **2026년 기준 중요한 변경점**이 있습니다. 기존 Hibernate 6 또는 초기 Hibernate 7에서는 collection fetch join과 pagination을 조합하면 limit이 JVM에서 적용되어 전체 결과를 읽어버리는 심각한 문제가 있었습니다. 그러나 **Hibernate ORM 7.4에서는 PostgreSQL처럼 subquery 안의 limit/offset을 지원하는 DB에서 이 문제가 해결되었습니다.** Hibernate 7.4의 “What’s New”가 이를 명시적으로 새 기능으로 소개합니다. citeturn13search0turn13search11 따라서 기존 규칙인 ```text Collection Fetch Join + Pagination → 무조건 금지 ``` 는 현재 baseline에서는 너무 강합니다. 정확한 규칙은 다음이어야 합니다. ```text Hibernate 7.4 + PostgreSQL 16~18 → 지원 가능 → generated SQL / rows / count query / cartesian amplification을 Contract Test Hibernate 이전 버전 또는 다른 provider → capability 재검증 여러 collection fetch → pagination 해결 여부와 별개로 Cartesian 위험 때문에 기본 제한 ``` 이 부분은 반드시 회귀 테스트에 넣어야 합니다. “과거 성능 장애 사례”와 “현재 지원 기능”을 구분하지 않으면 JPA 플랫폼이 이미 수정된 Hibernate 제한을 영구 정책으로 굳히게 됩니다. citeturn13search0turn13search2 **N+1 테스트는 SQL 개수 하나만 보면 부족합니다.** ```text statementCount entityLoadCount entityFetchCount collectionFetchCount returnedParents hydratedEntities rowsFromDatabase duration ``` 를 함께 보아야 합니다. 예컨대 SQL 1개라도 100 parent × 100 child × 20 second-child Cartesian product가 만들어지면 좋은 Fetch Plan이 아닙니다. 테스트 fixture 역시: ```text 0 child 1 child 10~100 children shared ToOne 두 개 이상의 collection skewed distribution ``` 을 포함해야 합니다. **Pagination 계약은 세 종류로 나누는 것이 좋습니다.** | 방식 | 장점 | 단점 | 기본 용도 | |---|---|---|---| | `Page` | total count 제공 | count query 비용 | 작은 관리자 화면 | | `Slice` | count 불필요 | 전체 개수 없음 | 일반 목록 | | Offset | 구현 간단 | 깊은 페이지 비용·삽입 시 이동 | 작은 데이터 | | Keyset/Cursor | 큰 데이터에 유리 | stable ordering·cursor 설계 필요 | 일반 대규모 목록 | | Stream/Scroll | 전체 적재 회피 | transaction/resource lifetime | batch/read processing | Spring Data의 Scroll API는 offset/keyset scrolling을 지원하지만 query 방식에 따라 지원 범위가 다르며, 공식 문서는 string-based `@Query`나 stored procedure에서 scrolling을 지원하지 않는 제한을 명시합니다. citeturn18search12turn9search8 Keyset cursor에는 반드시 전체 순서를 결정하는 tie-breaker가 필요합니다. ```sql ORDER BY created_at DESC, id DESC ``` 라면 cursor도: ```text (createdAt, id) ``` 두 값을 모두 저장해야 합니다. `created_at` 하나만 cursor로 쓰면 같은 timestamp를 가진 row가 누락되거나 반복될 수 있습니다. **Batch Write는 `saveAll()`과 동일하지 않습니다.** Hibernate의 JDBC batching은 `hibernate.jdbc.batch_size`가 0 이하이면 꺼져 있고, batching 활성화 뒤에도 ID generator와 SQL shape에 따라 실제 batch 여부가 달라집니다. Hibernate 7.4는 `order_inserts`, `order_updates`를 제공하지만 이 옵션 역시 overhead가 있으므로 benchmark를 권고합니다. citeturn14view1 권장 write profile은 다음입니다. ```yaml jpa: write-profiles: default: batch-size: 0 batch: jdbc-batch-size: 50 order-inserts: true order-updates: true flush-size: 50 clear-size: 50 ``` 정확한 50이라는 값 자체가 universal optimum이라는 뜻은 아니며 프로파일 기본 예시입니다. 실제 완료 조건은 “configured batch size가 SQL/JDBC batch로 관찰됨”입니다. Hibernate는 대량 Entity를 하나의 stateful Session에 계속 넣으면 Persistence Context에 Entity가 누적되고 장기 transaction이 connection pool을 오래 점유한다고 설명하며, batch loop에서 주기적인 `flush()`와 `clear()`를 권장합니다. citeturn14view1 ```java for (int i = 0; i < records.size(); i++) { entityManager.persist(records.get(i)); if (i > 0 && i % batchSize == 0) { entityManager.flush(); entityManager.clear(); } } ``` **대규모 Backfill은 JPA Entity lifecycle 자체가 필요하지 않을 수도 있습니다.** Hibernate `StatelessSession`은 Persistence Context와 연결되지 않은 detached-like object를 반환하고 insert/update/delete가 DB row에 직접 작용하는 다른 semantics를 갖습니다. 따라서 일반 Repository 대체가 아니라 J3/J4 대량 작업 extension으로 분류해야 합니다. citeturn14view4 권장 계층은 다음과 같습니다. ```text 일반 업무 write → JPA Entity 수천~수만 row → JPA + JDBC batch + chunk flush/clear 대규모 migration/backfill → StatelessSession / JdbcTemplate / PostgreSQL COPY 운영 대량 수정 → J4 Job ``` **Bulk DML**은 더 엄격합니다. ```text flush → JPQL/Native Bulk UPDATE → clear → 필요 시 재조회 ``` 가 기본 계약입니다. Bulk JPQL/Criteria DML은 managed entity state를 자동 동기화하지 않으며 optimistic locking도 자동 적용하지 않습니다. citeturn12view1 **Cache 정책도 단순하게 가져가는 편이 안전합니다.** ```text First-level Cache → JPA 기본, 항상 존재 Second-level Cache → 기본 Opt-out / Entity별 명시 Opt-in Query Cache → 기본 OFF Application Cache → 별도 Redis/cache 플랫폼 ``` Hibernate 7.4는 query cache 기본값이 false이고, shared cache mode에서는 `ENABLE_SELECTIVE`를 기본·권장하여 명시적으로 cacheable인 Entity만 second-level cache에 넣도록 설명합니다. 또한 외부 애플리케이션이 DB를 변경하면 Hibernate cache가 이를 자동 인지하지 못한다는 제한도 있습니다. citeturn14view3 즉 Redis application cache와 Hibernate L2 cache를 “같은 Cache 기능”으로 묶으면 안 됩니다. ## PostgreSQL·Schema Migration·확장 정책 JPA Mapping은 **애플리케이션의 object-relational mapping 계약**이고, 실제 schema 변경 Source of Truth는 **Flyway migration**으로 두는 것이 적절합니다. 권장 환경 정책은 다음입니다. | 환경 | Flyway | Hibernate DDL | |---|---|---| | local PostgreSQL | migrate | `validate` | | H2 convenience | 선택적 create/drop | 실제 호환성 증명 아님 | | test | migrate | `validate` | | dev | migrate | `validate` | | staging | deployment migration | `validate` | | prod | 별도 권한/배포 주체로 migration | `validate` | 운영에서 다음은 기본 금지로 두는 것이 좋습니다. ```text hibernate.ddl-auto=update hibernate.ddl-auto=create hibernate.ddl-auto=create-drop application runtime credential의 DDL 권한 적용 완료된 Versioned Migration 수정 startup 시 자동 Flyway repair ``` Flyway `validate`는 적용된 migration과 로컬 migration의 name/type/checksum 등을 비교하고 불일치나 누락을 실패로 보고합니다. SQL migration checksum은 현재 문서 기준 CRC32로 저장됩니다. citeturn19search0 Versioned migration은 순서대로 한 번 적용하고 이미 영구 환경에 적용한 파일은 수정하지 않고 새 migration으로 roll-forward하는 것이 Flyway가 권장하는 방식입니다. Repeatable migration은 checksum이 변경될 때 다시 실행됩니다. citeturn19search3turn19search6 `repair`는 단순한 “검증 복구” 기능이 아닙니다. 실패 migration 기록 제거, checksum/description/type 재정렬, missing migration을 deleted로 표시하는 등의 변경을 수행하며, DB에 남은 user object는 수동으로 정리해야 할 수 있습니다. 따라서 J4 승인 작업으로 두어야 합니다. citeturn19search1 **무중단 Migration의 기본 패턴은 Expand → Migrate → Contract입니다.** ```text Release A ADD nullable column ADD new table/index Application can handle old + new schema ↓ Backfill chunked data migration ↓ Release B new column becomes authoritative ↓ Release C old column/index/API removed constraint tightened ``` 큰 테이블에서 index를 만드는 경우 PostgreSQL의 `CREATE INDEX CONCURRENTLY`를 별도 migration 유형으로 취급해야 합니다. PostgreSQL은 concurrent index build를 transaction block 안에서 실행할 수 없다고 명시하므로, Flyway의 일반 transaction wrapping과 충돌하지 않도록 해당 migration을 non-transactional로 명시적으로 분리해야 합니다. citeturn17search3turn19search16 Flyway의 `group=true`는 여러 pending migrations를 한 transaction에 묶는 옵션이지만, DDL transaction을 적절히 지원하는 DB에서만 권장되며 기본은 false입니다. 무조건 활성화할 설정이 아닙니다. citeturn19search13 **Constraint 정책은 아래처럼 나누는 것이 좋습니다.** ```text Bean Validation → 빠른 입력/객체 검증 → 사용자 친화적 오류 Database Constraint → concurrency 하에서도 지켜져야 하는 최종 invariant ``` | Constraint | DB 필수성 | |---|---| | Primary Key | 필수 | | Foreign Key | 관계 불변식에 기본 | | `NOT NULL` | 실제 non-null invariant이면 DB에도 적용 | | Unique | 경쟁 가능 uniqueness는 DB가 최종 보장 | | Check | DB 자체로 표현 가능한 invariant에 적극 검토 | | Exclusion | PostgreSQL 고유 overlap 등 고급 invariant | **Index 역시 Entity field에 자동 생성하는 문제가 아닙니다.** PostgreSQL은 B-tree, GiST, GIN, BRIN, multicolumn, expression, partial, covering `INCLUDE` 등 다양한 index 기능을 제공합니다. 특히 multicolumn index는 실제 predicate, sort와 data distribution을 기준으로 설계해야 합니다. citeturn17search2turn8search9 플랫폼은 “자동 Index 생성기”보다 다음을 제공하는 것이 더 유용합니다. ```text Query Name → representative parameters → EXPLAIN / EXPLAIN ANALYZE → estimated rows / actual rows → scan type → sort → temporary spill → buffers → execution time → expected index document ``` **PostgreSQL extension 지원표**는 다음이 적절합니다. | 기능 | 등급 | 비고 | |---|---|---| | JSONB | **P1 Stable Extension** | PostgreSQL-native value/query | | Array | **P1 Stable Extension** | 타입별 contract test | | Range | **P1 Stable Extension** | 기간·구간 도메인 | | UUID | **P1 Stable** | standard/native | | INET | P2 Advanced | networking domain | | Native Enum | P2 Advanced | migration coupling 큼 | | `ON CONFLICT` | **P1 Native Write Extension** | 명시적 upsert semantics | | `RETURNING` | **P1 Native Write Extension** | native write 최적화 | | Window Function | P1/P2 Query Extension | read model | | CTE | P2 | 복잡한 read/write | | Recursive CTE | P2 | 제한된 use case | | `NOWAIT` | **P1 Lock Extension** | fast-fail lock | | `SKIP LOCKED` | **P1 Worker Extension** | queue-like use case만 citeturn8search4 | | Advisory Lock | P2 Advanced | transaction/session scope를 명시 | | Partial Index | **J4 Migration** | query-specific | | Expression Index | J4 Migration | query-specific | | `NULLS NOT DISTINCT` | **J4 Stable Migration** | PG unique semantics citeturn17search1 | | Generated Column | P2/J4 | mapping·migration 검증 | | Full-text Search | P2 | 전문 검색 규모에서는 별도 검색 플랫폼과 비교 | | Partitioning | **J4 Admin** | application Repository가 생성·삭제하지 않음 | | Row-Level Security | Experimental/Admin | tenant context·connection reuse까지 검증 필요 | **Auditing은 강제 BaseEntity보다 선택형이 낫습니다.** Spring Data JPA는 created/modified user/time을 기록하는 auditing 기능을 이미 제공하므로 공통 플랫폼은 이를 활성화할 수 있는 primitive만 제공하고, 도메인이 필요한 Entity에 선택적으로 적용하도록 해야 합니다. citeturn20view0turn18search5 ```text Technical Auditing createdAt / createdBy / modifiedAt / modifiedBy ≠ Business Audit “누가 주문 상태를 왜 취소했는가” ≠ Entity History 과거 row revision ≠ Security Audit 관리자 권한·DDL·replay ``` Hibernate Envers는 Entity History 선택 기능으로 둘 수 있지만 J1 기본 기능으로 만들 필요는 없습니다. **Soft Delete 역시 global 기능으로 제공하지 않는 것이 좋습니다.** ```text Global @Where deleted=false → 비추천 Domain-specific status/deletedAt → 필요 도메인에만 복구 가능한 삭제 → 도메인 계약 법적/개인정보 물리 삭제 → 별도 lifecycle ``` Soft Delete를 공통 필터로 숨기면 unique constraint, FK, admin query, archive, restore, 개인정보 삭제가 모두 암묵적 semantics에 묶입니다. PostgreSQL partial unique index 같은 기능이 필요한 이유도 이 경계 때문입니다. **Multi-tenancy는 초기 Stable Core에서 제외하는 것이 안전합니다.** | 모델 | 권장 초기 등급 | |---|---| | 단일 DB·schema | Stable | | Shared schema + tenant column | Experimental capability | | Schema per tenant | Experimental | | DB per tenant | Experimental | | RLS 기반 | Experimental | | Multi DataSource | Advanced/Experimental | | Read Replica routing | Experimental | Read replica는 `@Transactional(readOnly=true)`만 보고 자동 routing해서는 안 됩니다. replica lag 때문에 같은 사용자 흐름의 직전 write가 보이지 않을 수 있고 lock query는 primary가 필요하기 때문입니다. Stable Core에는 transaction read-only hint까지만 포함하고 routing은 별도 profile로 두는 것이 적절합니다. ## 오류·보안·관측성·테스트 계약 Spring의 exception translation과 PostgreSQL SQLSTATE를 활용하되 애플리케이션에 provider/vendor exception을 그대로 노출하지 않는 것이 좋습니다. PostgreSQL 공식 문서는 오류 판단 시 locale에 따라 달라지는 message text가 아니라 SQLSTATE를 검사하라고 권장하며, integrity violation에서는 constraint name 같은 structured field도 전달합니다. citeturn13search1 권장 오류 모델은 다음과 같습니다. ```text JpaPersistenceException ├─ EntityNotFound ├─ OptimisticConflict ├─ PessimisticLockTimeout ├─ DeadlockDetected ├─ SerializationFailure ├─ UniqueConstraintViolation ├─ ForeignKeyViolation ├─ CheckConstraintViolation ├─ QueryTimeout ├─ TransactionTimeout ├─ ConnectionUnavailable ├─ SchemaMismatch ├─ DataCorruption └─ TransactionCompletionUnknown ``` PostgreSQL SQLSTATE를 활용하면 대표적으로 serialization failure `40001`, deadlock `40P01`, 그리고 completion unknown 계열을 문자열 parsing 없이 분류할 수 있습니다. Constraint violation도 class 23을 기준으로 구조화할 수 있습니다. citeturn13search1 **Retry 판정표는 다음처럼 두는 것이 좋습니다.** | 오류 | 자동 Retry | 단위 | 조건 | |---|---|---|---| | `OptimisticConflict` | 조건부 | 전체 use case transaction | 재계산 가능, side effect 없음 | | `SerializationFailure` | 조건부 | 전체 transaction | bounded attempts + jitter | | `DeadlockDetected` | 조건부 | 전체 transaction | bounded attempts | | Lock timeout | 조건부 | 전체 use case | deadline과 업무 정책 확인 | | Connection acquire 전 실패 | 제한적 | 전체 use case | DB에 작업이 시작되지 않았음이 확실 | | Unique violation | 기본 금지 | — | idempotent create라면 기존 record 재조회 가능 | | FK violation | 금지 | — | 업무 순서/데이터 오류 | | Check violation | 금지 | — | 업무 invariant 오류 | | Query timeout | 기본 금지 | — | 동일 부하에서 반복하면 부하만 증폭 | | Schema mismatch | 금지 | — | 배포 오류 | | Commit 결과 불명 | **금지** | reconciliation | 중복 실행 위험 | 모든 retry에는: ```text maxAttempts maxElapsedTime exponentialBackoff jitter transaction deadline retry metrics ``` 가 있어야 합니다. **보안 정책은 Repository API보다 DB credential과 dynamic query 제한이 중요합니다.** ```text Application Role ├─ SELECT ├─ INSERT ├─ UPDATE ├─ DELETE └─ 필요한 sequence 사용 Migration Role ├─ CREATE ├─ ALTER ├─ DROP └─ index / constraint / schema Read-only Role └─ 필요한 SELECT Admin Role └─ 승인된 운영 작업 ``` 운영 application credential에는 `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, extension 설치 권한을 주지 않는 것이 적절합니다. PostgreSQL은 `search_path`에 CREATE 권한을 가진 신뢰하지 않는 schema가 들어가면 object resolution이 보안 문제가 될 수 있음을 문서화하고 있으며, 안전한 schema privilege 패턴을 별도로 설명합니다. 따라서 migration schema를 명확히 하고 application role의 `search_path`를 고정·검증해야 합니다. citeturn21search6 추가 보안 규칙은 다음처럼 고정하는 것이 좋습니다. ```text JPQL → parameter binding Native SQL → J3 내부 → 값 문자열 연결 금지 Dynamic sort → allowlist Dynamic table/column → 원칙적 금지 → 불가피하면 enum/catalog mapping Entity → API request mass binding 금지 SQL parameter logging → production 기본 OFF Tenant ID → metric tag / raw log 금지 DB password → secret manager / workload identity 경로 ``` **관측성은 이미 Boot에서 상당 부분 제공됩니다.** Spring Boot는 DataSource에 `jdbc.connections` active/idle/max/min gauge를 만들고 Hikari-specific `hikaricp` metrics도 제공합니다. `hibernate-micrometer`가 있고 Hibernate statistics를 활성화하면 Hibernate metrics를, Spring Data Repository 호출에는 `spring.data.repository.invocations`를 제공합니다. citeturn21search0 공통 관측 계약은 이를 다음처럼 확장하는 것이 적절합니다. | 계층 | 필수 관측 | |---|---| | Pool | active, idle, pending, max, acquire latency, timeout | | Transaction | count, latency, rollback, timeout, isolation, retry, completion-unknown | | Query | queryName, count, latency, rows, timeout, lock wait | | Fetch | statement count, entity load/fetch, collection fetch | | Batch | batch count, batch size, flushed entities | | Lock | optimistic conflict, pessimistic timeout, deadlock | | Migration | version, validate result, migration duration | | Retry | reason, attempt, elapsed | | Cache | L2/query hit/miss when enabled | Metric cardinality는 낮게 유지합니다. **허용:** ```text persistenceUnit operationName bounded entityType bounded queryName outcome failureCategory isolation ``` **금지:** ```text entityId userId tenantId 원문 SQL parameter Email / Phone / PII 임의 SQL text dynamic WHERE clause ``` SQL 전체 문자열을 metric dimension으로 쓰는 대신 정규화된 query fingerprint 또는 등록된 `queryName`을 사용합니다. SQL parameter logging은 production에서 기본 비활성화해야 합니다. HikariCP의 pool size 자체도 무작정 키우면 안 됩니다. Hikari는 maximum pool size 도달 시 connection 반환을 기다리다가 `connectionTimeout` 이후 실패하는 모델을 사용하므로, 관측해야 할 핵심은 단순 active count가 아니라 **pending/acquire latency와 transaction duration**입니다. citeturn11search2turn21search0 **테스트는 H2 중심이 아니라 PostgreSQL Contract 중심으로 설계해야 합니다.** 테스트 피라미드는 다음이 적절합니다. ```text Pure Unit → domain logic @DataJpaTest → repository wiring / quick mapping PostgreSQL Testcontainers → real persistence semantics PostgreSQL 16 / 17 / 18 Matrix → release compatibility Fault Injection → lock / network / commit ambiguity Migration Snapshot → real upgrade path ``` Testcontainers는 실제 PostgreSQL image를 실행할 수 있으므로 DB 고유 기능에 의존하는 integration test를 H2 대체 구현이 아니라 실제 DB에서 수행하는 기반으로 적합합니다. citeturn11search0turn11search10 필수 Contract Test 목록은 다음과 같습니다. | 범주 | Release Gate | |---|---| | Mapping | ID, Embeddable, record Embeddable, Enum, time, converter, association | | Lifecycle | persist, merge, dirty check, flush, clear, detach, refresh | | Transaction | commit, rollback, checked/unchecked rollback rule, `REQUIRES_NEW`, self-invocation | | Concurrency | optimistic conflict, pessimistic timeout, deadlock, serialization failure | | Constraint | unique race, FK, check, partial unique | | Query | derived, JPQL, projection, specification, native | | Fetch | N+1, graph, fetch join, multiple collections, statement count | | Pagination | Page, Slice, keyset, duplicate sort values, concurrent insertion | | Hibernate 7.4 | **collection fetch join + pagination regression** | | Batch | actual JDBC batching, IDENTITY no-batch, sequence batch, flush/clear | | Bulk | bulk update 뒤 stale Entity | | PostgreSQL | JSONB, Array, Range, `ON CONFLICT`, `SKIP LOCKED` | | Flyway | empty DB, previous release snapshot, repeatable, checksum mismatch | | Security | restricted application role, dynamic sort injection, SQL log masking | | Pool | saturation, acquire timeout, `REQUIRES_NEW` pressure | | Failure | process kill, network loss, DB restart, transaction retry | | Commit ambiguity | COMMIT 전/중/후 connection loss simulation | | Observability | cardinality, PII masking, queryName/failureCategory | PostgreSQL version matrix는 PR마다 최소 oldest/current인 `16 + 18`, release branch에서 `16 + 17 + 18` 전체를 실행하는 방식이 비용과 호환성 검증의 균형점입니다. 다만 “16·17·18 Stable”이라고 선언하려면 release gate에서는 세 버전을 모두 통과해야 합니다. Migration은 단순히 **빈 DB → latest**만 테스트하면 부족합니다. ```text empty → latest previous release N-1 → latest oldest supported upgrade snapshot → latest checksum modified → validation must fail missing migration → validation must fail failed non-transactional migration → known recovery procedure ``` 를 함께 검증해야 합니다. Flyway가 checksum/name/type/missing migration을 validation 대상으로 삼기 때문입니다. citeturn19search0turn19search4 **실무 실패 사례를 플랫폼 규칙으로 변환하면 다음과 같습니다.** | 상황 | 직접 원인 | 설계 규칙 | 회귀 테스트 | |---|---|---|---| | OSIV 뒤에서 N+1 발생 | Web serialization 중 LAZY load | OSIV off, DTO/fetch plan | Controller 밖 Entity access 실패 | | EAGER 폭증 | mapping이 use case fetch plan을 결정 | 최소 mapping + query fetch plan | SQL/row count | | 여러 collection fetch | Cartesian product | DTO/분할 조회 | skewed collection fixture | | Fetch Join + Page 전체 load | **구 Hibernate 동작** | 7.4+ PG에서는 새 SQL behavior 검증 | Hibernate 7.4 pagination regression citeturn13search0 | | `saveAll()`인데 batch 없음 | JDBC batch 미설정/IDENTITY | actual batch 관측 | statement/batch count | | IDENTITY batch 실패 | ID 얻기 위해 즉시 insert | write-heavy Entity는 sequence | ID strategy benchmark citeturn14view0 | | Bulk update 후 stale | PC 미동기화 | flush → DML → clear | stale entity assertion citeturn12view1 | | TX 안에서 API 대기 | DB resource 장기 보유 | 외부 I/O TX 밖 | pool pressure test | | `REQUIRES_NEW` 고갈 | outer+inner connection 동시 점유 | 제한 + pool capacity test | concurrent nested tx citeturn15search0 | | Optimistic 부분 Retry | stale PC에서 일부 코드 재실행 | 전체 unit-of-work retry | conflict fixture | | Deadlock 무한 Retry | retry budget 없음 | bounded full-TX retry | deterministic deadlock | | DDL auto update | runtime schema 변경 | Flyway only | app role DDL deny | | H2만 통과 | DB semantics 차이 | PG contract mandatory | PG16~18 | | Entity JSON 반환 | lazy graph serialization | DTO/projection | detached serialization | | Soft Delete unique 충돌 | deleted row도 unique에 존재 | domain policy + partial index | recreate-after-delete | | Replica stale read | replication lag | replica experimental | read-after-write lag | | Commit 응답 유실 | 결과 모호 | no auto retry, reconciliation | protocol failure injection | ## Stable 범위와 구현 순서 최종적인 **Stable / Experimental / 비지원 범위**는 다음이 현실적입니다. | 영역 | Stable | Experimental / Advanced | 초기 비지원 | |---|---|---|---| | Repository | Spring Data domain repository | custom fragments | GenericRepository 재구현 | | JPA | Persistence 3.2 | Persistence 4.0 compatibility | Extended PC 일반 사용 | | Provider | Hibernate 7.4 | Hibernate 8 lane | 임의 provider 동일 보장 선언 | | DB | PostgreSQL 16·17·18 | PG19 compatibility | MySQL/Oracle 호환 선언 | | Local DB | H2 convenience | — | H2를 PG 증명으로 사용 | | Transaction | REQUIRED, read-only, timeout | MANDATORY, REQUIRES_NEW | remote distributed transaction 기본화 | | Lock | Optimistic, standard pessimistic | NOWAIT/SKIP LOCKED extension | generic distributed lock | | Query | Derived, JPQL, projection | Specification, Querydsl, native | 자유로운 raw SQL | | Fetch | EntityGraph, fetch join, projection | batch/subselect fetch | global EAGER | | Pagination | Page, Slice, keyset | Scroll/Stream | 무제한 findAll | | Batch | JDBC batch | StatelessSession/COPY | `saveAll`을 batch guarantee로 정의 | | Migration | Flyway migrate/validate | non-transactional/admin migration | prod ddl-auto update | | Audit | Spring Data auditing opt-in | Envers | 모든 Entity 강제 history | | Soft Delete | domain-specific | helper capability | global implicit soft delete | | Cache | L1 | L2 opt-in | Query cache 기본 활성화 | | Multi-tenancy | single tenant baseline | tenant column/RLS/schema/db | 투명 자동 multi-tenant | | Replica | primary | read replica experimental | annotation만으로 자동 routing | | Retry | bounded full-TX retry | domain-specific policy | repository-method retry | | Completion unknown | error + reconciliation | domain-specific resolver | 자동 retry | 이 조사에서 가장 중요한 결정은 **JPA 플랫폼이 많은 API를 제공하는 것보다 잘못된 사용을 어렵게 만드는 것**입니다. 권장 핵심 API는 거대한 Repository가 아니라 다음과 같은 작은 기술 primitive입니다. ```java public interface JpaTransactionExecutor { T execute(TransactionProfile profile, Supplier work); } public record TransactionProfile( String name, IsolationLevel isolation, Duration timeout, boolean readOnly, RetryProfile retryProfile ) {} public interface JpaRetryPolicy { RetryDecision classify(JpaPersistenceException error); } public interface QueryObservation { QueryScope start(String queryName); } public interface PostgreSqlExtension { // marker / capability boundary } ``` 다만 평범한 application service는 이런 저수준 API조차 직접 다루지 않고 보통 Spring `@Transactional` + domain repository를 사용하게 하는 편이 좋습니다. ```java @Service @RequiredArgsConstructor public class PlaceOrderService { private final OrderRepository orders; private final OutboxRepository outbox; @Transactional public OrderId place(PlaceOrder command) { Order order = Order.place(command); orders.save(order); outbox.save(OutboxMessage.from(order)); return order.getId(); } } ``` **단계별 구현 순서와 완료 조건**은 다음과 같이 잡는 것이 좋습니다. | 단계 | 구현 | 완료 조건 | |---|---|---| | Foundation | `jpa-core`, Boot BOM, PostgreSQL profile, Hikari, OSIV off | PG16·17·18 bootstrap 및 기본 CRUD contract 통과 | | Mapping | Entity/ID/association/value 규칙, test fixtures | Mapping rule 문서 + ArchUnit/static check + PG round trip | | Transaction | profile, boundaries, propagation, timeout | rollback/self-invocation/REQUIRES_NEW tests | | Concurrency | version, lock, SQLSTATE error mapper | optimistic/deadlock/serialization/lock timeout 재현 | | Error/Retry | common exception + full-TX retry | retryable/non-retryable matrix 자동 테스트 | | Query | projection/specification/custom fragments | query startup validation + queryName 체계 | | Fetch | EntityGraph/fetch join/query-count toolkit | N+1 및 Cartesian regression gate | | Pagination | Slice/keyset/cursor | duplicate sort·concurrent insert contract | | Batch | sequence profile, JDBC batch, flush/clear | 실제 JDBC batching 관측 | | PostgreSQL Extension | JSONB/Array/Range, ON CONFLICT, lock extension | PG16·17·18 native capability tests | | Migration | Flyway, validation, snapshots | empty + N-1 + oldest-supported migration 통과 | | Observability | pool/tx/query/retry metrics | cardinality·PII tests | | Security | DB role separation, log masking | app credential로 DDL 실패 보장 | | Advanced | Envers, L2 cache, StatelessSession | 기능별 opt-in contract | | Experimental | multi-tenancy, replica, JPA4/Hibernate8 | 별도 compatibility suite 통과 전 Stable 승격 금지 | 최종적으로 이번 조사에서 요구된 산출물은 다음과 같이 귀결됩니다. | 요구 산출물 | 조사 결론 | |---|---| | Java·Spring Data·Hibernate·PG 지원 매트릭스 | Java 21 + Boot BOM + JPA 3.2 + Hibernate 7.4 + PG16~18 | | J1~J4 계층 | Standard / Advanced / Provider Extension / Admin | | Entity Mapping | Field access 중심, Entity 외부 직렬화 금지, domain ownership | | ID 전략 | PG 기본 Sequence, UUID stable, IDENTITY write-heavy 제한 | | Association | global cascade/eager 금지, lifecycle 명시 | | Persistence Context | transaction-scoped, OSIV off | | Transaction | Application Service boundary | | Commit Unknown | 별도 `TransactionCompletionUnknown`, 자동 retry 금지 | | Optimistic/Pessimistic | optimistic 우선, lock extension 제한 | | Query | Derived → JPQL/Projection → Dynamic → Native 단계화 | | Fetch | use-case fetch plan, quantitative regression | | Pagination | Page/Slice/Keyset 역할 분리 | | Batch | saveAll과 JDBC batch 구분 | | Migration | Flyway가 schema change source of truth | | Constraint/Index | DB invariant + query-driven index | | PostgreSQL Extension | 별도 `jpa-postgresql` | | Auditing/Soft Delete/History | 각각 별개 capability | | Cache | L1 기본, L2 opt-in, query cache off | | Multi-tenancy/Replica | 초기 Experimental | | 오류/Retry | SQLSTATE 기반 안정 오류 + full-TX retry | | Metric/Trace/Logging | queryName 기반, parameter·PII 배제 | | Security | runtime/migration/admin credential 분리 | | Tests | PG Testcontainers + 실제 version matrix | | Stable/Experimental | JPA4/Hibernate8/multitenancy/replica 분리 | | 구현 순서 | Foundation → semantics → performance → operations | 가장 중요한 최종 설계 규칙은 여섯 가지로 압축됩니다. **첫째**, 도메인이 Entity와 Repository를 소유하며 JPA 플랫폼은 `GenericRepository`를 만들지 않습니다. Spring Data JPA가 이미 그 추상화를 제공하기 때문입니다. citeturn20view0 **둘째**, transaction은 Repository method가 아니라 **Application Use Case** 단위이며, Optimistic conflict·Deadlock·Serialization Failure의 retry도 새 Persistence Context에서 전체 transaction을 다시 실행합니다. PostgreSQL Serializable 역시 transaction 재실행을 전제로 합니다. citeturn13search9 **셋째**, DB에 요청을 보냈다는 사실과 commit이 확정됐다는 사실을 구분합니다. Commit 결과가 모호하면 `TransactionCompletionUnknown`으로 올리고 자동 retry하지 않습니다. PostgreSQL도 completion-unknown을 SQLSTATE에서 별도 condition으로 정의합니다. citeturn13search1 **넷째**, Fetch 전략은 Entity annotation의 EAGER/LAZY만으로 결정하지 않고 **use-case-specific Fetch Plan**으로 관리합니다. 특히 Hibernate 7.4에서 PostgreSQL의 collection fetch join + pagination 동작이 과거 버전과 달라졌으므로, 오래된 금지 규칙을 그대로 복사하지 말고 현재 버전 SQL을 contract test해야 합니다. citeturn13search0turn13search11 **다섯째**, Entity Mapping이 schema의 Source of Truth가 아닙니다. 운영 schema는 Flyway가 소유하고 Hibernate는 `validate` 역할을 맡으며, `repair`, concurrent index, backfill, partition 같은 작업은 J4 Admin 영역으로 분리합니다. citeturn19search0turn19search1turn17search3 **여섯째**, `H2에서 된다`를 호환성 증거로 쓰지 않습니다. **PostgreSQL 16·17·18의 실제 locking, constraint, batch, migration, query plan, SQLSTATE를 통과하는 것**을 이 플랫폼의 Stable 완료 조건으로 삼는 것이 적절합니다. citeturn0search3turn13search5