Files
clean-architecture-backend-…/jpa-superpowers-package/docs/superpowers/specs/2026-08-11-jpa-persistence-platform-design.md
T

115 KiB
Raw Blame History

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 정책을 소유하고, 플랫폼은 다음 기술적 기반을 제공한다.

도메인 소유
├─ 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

구현자가 이 문서를 읽은 뒤 다시 결정하지 않아야 하는 핵심 질문은 다음과 같다.

어디에 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<T, ID> 재구현이 없고 도메인 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 우선순위

사용자 지시
→ 이 설계서의 명시적 계약
→ 심층 리서치 원본
→ host 저장소의 기존 convention
→ Spring Boot BOM 기본값

기존 저장소 구조가 다르면 경로와 convention plugin 이름은 매핑할 수 있지만, 공개 계약과 불변 조건은 유지한다.


4. 범위

4.1 Stable 범위

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 범위

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 범위

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 명시적 비지원

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. 전체 아키텍처

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 흐름

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 흐름

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 흐름

Application
→ COMMIT 전송
→ PostgreSQL commit 가능
→ 응답 전에 connection loss
→ EvidenceAwareJpaTransactionManager
→ TransactionCompletionUnknown
→ 자동 Retry 금지
→ transactionKey / unique key / outbox / 상태 조회
→ domain-specific reconciliation

6.4 Read 흐름

Application Query
→ QueryName
→ Projection / EntityGraph / Fetch Join / Native Query
→ QueryObservation
→ Statement + Hibernate statistics
→ DTO / Projection 반환

Entity를 Controller에 반환하지 않는다.


7. 모듈 구조

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 의존 방향

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-apijakarta.persistence, Spring, Hibernate, PostgreSQL JDBC, Flyway에 의존하지 않는다.

7.3 ArchUnit 경계

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

일반 애플리케이션이 기본으로 사용한다.

Spring Data Repository
Derived Query
JPQL
DTO / Interface Projection
Application Service @Transactional
@Version Optimistic Lock
Spring Data Auditing opt-in
Page / Slice

도메인 Repository 예시:

public interface OrderRepository
        extends JpaRepository<Order, OrderId>, OrderRepositoryCustom {

    Optional<Order> findByOrderNumber(OrderNumber orderNumber);
}

public interface OrderRepositoryCustom {
    KeysetSlice<OrderSummary, OrderCursor> findRecent(
            OrderSearchCondition condition,
            KeysetPageRequest<OrderCursor> page);
}

8.2 J2 Advanced Persistence

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

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

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

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

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

public interface JpaTransactionExecutor {
    <T> T execute(
            PersistenceOperationName operation,
            TransactionProfile profile,
            Supplier<T> work);
}

일반 Use Case는 @Transactional을 사용할 수 있다. Programmatic retry·동적 profile이 필요한 Use Case는 executor를 사용한다.

9.4 Retry Policy

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

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를 정의하지 않는다. 도메인 모듈이 다음을 소유한다.

@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 외부 노출 금지

다음은 금지한다.

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 규칙

@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

Cascade.ALL
→ 전역 기본값 금지

orphanRemoval
→ Parent가 Child lifecycle을 독점 소유할 때만

ManyToMany
→ 단순 연결 외에는 Join Entity 우선

13.4 양방향 관계

Owning side가 DB 변경을 결정한다. addChild/removeChild helper가 양쪽 in-memory graph를 항상 동기화해야 한다.


14. Persistence Context 계약

Transient
Managed
Detached
Removed

14.1 기본 계약

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 기본 경계

Controller
→ Application Service @Transactional
→ Domain Repository

Repository가 독립 업무 Transaction을 임의로 시작하지 않는다.

15.2 금지 경계

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 상태

public enum TransactionCompletionEvidence {
    NOT_STARTED,
    ACTIVE,
    COMMITTING,
    COMMITTED,
    ROLLED_BACK,
    UNKNOWN
}

17.2 감지

EvidenceAwareJpaTransactionManagerdoCommit 진입 전 evidence를 COMMITTING으로 기록한다. 다음 조건에서 TransactionCompletionUnknownException으로 변환한다.

SQLSTATE 40003
OR
commit phase의 connection loss / transport exception
AND
rollback 또는 commit 여부를 driver가 확정하지 못함

일반 query 단계 connection failure를 completion unknown으로 과대 분류하지 않는다.

17.3 오류 계약

public final class TransactionCompletionUnknownException
        extends JpaPersistenceException {

    private final String transactionKey;
    private final TransactionCompletionEvidence evidence;
}

17.4 복구

자동 Retry 금지
→ transactionKey로 상태 조회
→ Unique Constraint / Idempotency Record 확인
→ 업무 Row 확인
→ Outbox 확인
→ 결과 확정 불가 시 Reconciliation Queue

TransactionCompletionResolver는 domain-specific SPI이며 Core가 업무 성공을 추측하지 않는다.


18. 안정 오류 모델과 SQLSTATE

JpaPersistenceException
├─ JpaEntityNotFoundException
├─ OptimisticConflictException
├─ PessimisticLockTimeoutException
├─ DeadlockDetectedException
├─ SerializationFailureException
├─ UniqueConstraintViolationException
├─ ForeignKeyViolationException
├─ CheckConstraintViolationException
├─ QueryTimeoutException
├─ TransactionTimeoutException
├─ ConnectionUnavailableException
├─ SchemaMismatchException
├─ DataCorruptionException
└─ TransactionCompletionUnknownException

18.1 공통 Metadata

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 안전 조건

전체 Use Case가 재계산 가능
AND
외부 irreversible side effect 없음
AND
새 Persistence Context 생성
AND
새 Transaction 생성
AND
deadline 남음
AND
retry budget 남음

19.3 Retry Profile

public record RetryProfile(
        String name,
        int maxAttempts,
        Duration initialBackoff,
        Duration maxBackoff,
        double multiplier,
        JitterMode jitter,
        Set<FailureCategory> retryableFailures) {
}

19.4 Annotation Adapter

@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하지 않는다.
@Entity
public class Order {
    @Version
    private long version;
}

Retry 후에는 최신 Entity를 다시 조회하고 업무 규칙을 다시 계산한다.


21. Pessimistic Lock·PostgreSQL Lock Extension

21.1 표준 Lock

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에만 제공한다.

public interface WorkClaimExecutor<T, K> {
    List<T> claimNextBatch(
            WorkQueueName queue,
            int size,
            Duration lease);
}

21.4 Lock Ordering

여러 Row를 잠글 때 stable key order를 사용한다. deadlock fixture로 규칙을 검증한다.


22. Constraint와 경쟁 조건

22.1 최종 불변식

Bean Validation
→ 조기 사용자 오류

Database Constraint
→ concurrency에서도 지켜지는 최종 invariant

22.2 지원

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 전략

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 정량 지표

statementCount
entityLoadCount
entityFetchCount
collectionLoadCount
collectionFetchCount
returnedParents
hydratedEntities
rowsFromDatabase
executionTime

25.4 Fixture

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에서 다음을 검증한다.

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 계약

public record KeysetPageRequest<C>(
        Optional<C> after,
        int size,
        SortDirection direction) {
}

public record KeysetSlice<T, C>(
        List<T> items,
        Optional<C> 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 의미

saveAll != one SQL
JDBC Batch != one SQL
IDENTITY != batch-friendly

28.2 Profile

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 계약

flush
→ JPQL / Native Bulk DML
→ clear
→ 필요 시 재조회

29.2 제한

  • Bulk DML은 Entity callback과 optimistic version check를 자동 실행하지 않는다.
  • 도메인 invariant를 우회할 수 있으므로 Q4 또는 명시적 J2 API에서만 사용한다.
  • 영향 Row 수를 반환하고 예상 범위를 검증한다.
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 선택표

일반 업무 write → JPA Entity
수천~수만 rows → JPA JDBC Batch
대규모 import/backfill → StatelessSession / COPY

31. PostgreSQL Extension

31.1 Stable J3

JSONB
Array
Range
UUID
ON CONFLICT
RETURNING
NOWAIT
SKIP LOCKED Work Claim
Window Function

31.2 Advanced

INET
Native Enum
CTE / Recursive CTE
Advisory Lock
Generated Column
Full-text Search

31.3 Admin

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()로 숨기지 않는다.

public interface PostgreSqlUpsertExecutor<C, R> {
    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 금지

prod ddl-auto update/create/create-drop
runtime credential DDL
적용 완료 Versioned Migration 수정
startup auto repair

33.3 Validation

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

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

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는 다음 문서를 소유한다.

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 구분

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 기본

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

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 관측

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

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

허용:

persistenceUnit
operationName
bounded entityType
queryName
outcome
failureCategory
isolation
attemptBucket

금지:

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 역할

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 방어

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

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

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

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 층위

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

JpaTestEntity
VersionedEntity
Parent / Child
TwoCollectionsAggregate
SkewedFeedFixture
UniqueConstraintFixture
WorkQueueFixture
BatchEntity
JSONB / Array / Range Entity

공용 fixture만 testkit에 두고 업무 Entity를 플랫폼 production module에 넣지 않는다.

43.3 계약 목록

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

p50 / p95 / p99
statement count
rows
entity hydration
collection fetch
plan node
buffer hit/read
sort spill

45.2 Write

records/sec
JDBC batch count
statement count
flush count
Persistence Context size
heap allocation
transaction duration

45.3 Pool

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. 완료 정의

다음 질문에 모두 구현·테스트 증거로 답할 수 있어야 한다.

도메인이 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 목록

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. 단계별 구현 순서

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 34, 2932
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 3031, 3740
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. 구현 시 금지되는 즉흥 결정

새 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 관계형 영속성 플랫폼 심층 리서치

이번 조사의 결론부터 정리하면, jpaJpaRepository를 한 번 더 감싸는 공통 Repository 라이브러리로 설계해서는 안 됩니다. Spring Data JPA 자체가 이미 Repository, Query Method, Pagination, Auditing, Custom Repository, Querydsl 통합 등을 제공하고 있으므로, 공통 플랫폼이 다시 CRUD 추상화를 만드는 것은 기능 중복과 추상화 누수를 동시에 만듭니다. 현재 Spring Data JPA 공식 프로젝트 페이지의 안정 버전은 4.1.0입니다. citeturn20view0

따라서 권장 구조는 다음과 같습니다.

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

권장 공개 구조는 따라서 다음입니다.

// Domain owns this
public interface OrderRepository extends JpaRepository<Order, UUID>,
        OrderRepositoryCustom {
    Optional<Order> findByOrderNumber(OrderNumber orderNumber);
}

// Domain-specific custom query contract
public interface OrderRepositoryCustom {
    Slice<OrderSummary> 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을 수행할 수 없다고 명시하며, IDENTITYpersist() 시 식별자를 얻기 위해 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 계약은 다음과 같습니다.

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

플랫폼 계약은 아래처럼 고정하는 것이 좋습니다.

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를 만드는 것입니다.

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

따라서 기본 계약은 다음입니다.

Controller
    │
    ▼
Application Service  ← @Transactional boundary
    │
    ├─ Repository A
    ├─ Repository B
    └─ Domain operation

그리고 다음 구조는 피해야 합니다.

@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 규칙은 RuntimeExceptionError입니다. 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 단위는 다음과 같아야 합니다.

잘못된 방식

@Transactional
service()
  repository.update()       // 실패
  retry(repository.update)  // 일부 SQL만 재실행


권장 방식

retryTransaction(
    () -> applicationUseCase()
)

새 Persistence Context와 새 DB transaction에서 전체 use case를 재실행해야 합니다.

Optimistic Lock은 기본 동시성 제어의 첫 번째 선택지로 두는 것이 적절합니다.

@Version
private long version;

JPA는 optimistic version check가 flush 또는 commit 시점까지 지연될 수 있음을 허용하며, 충돌 시 OptimisticLockException을 발생시킵니다. 즉 update method 호출 직후 충돌이 반드시 드러난다고 가정하면 안 됩니다. citeturn12view2

Optimistic retry는 다음 조건을 모두 만족해야 합니다.

전체 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 LOCKEDfindAllUnlocked() 같은 공통 Repository API로 제공해서는 안 되고,

jpa-postgresql
└─ WorkClaimExtension
   └─ claimNextBatch(...)

처럼 semantics가 드러나는 API로 한정하는 것이 좋습니다.

DB Constraint는 최종 불변식입니다. 다음 코드는 경쟁을 막지 못합니다.

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으로 관리합니다.

CREATE UNIQUE INDEX uq_user_active_email
ON users (email)
WHERE deleted_at IS NULL;

이는 Soft Delete와 Unique Constraint 충돌을 해결하는 대표적인 PostgreSQL extension 패턴입니다.

Commit 결과 불명확성은 별도 오류로 모델링해야 합니다.

예를 들어:

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이 아닌 플랫폼 고유 분류로 다음을 두는 것을 권장합니다.

final class TransactionCompletionUnknown
        extends JpaPersistenceException {

    String operationName;
    String transactionKey;
    String sqlState;
    boolean commitAttempted;
    String traceId;
}

이 오류에 대한 정책은 명확해야 합니다.

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의 “Whats New”가 이를 명시적으로 새 기능으로 소개합니다. citeturn13search0turn13search11

따라서 기존 규칙인

Collection Fetch Join + Pagination
→ 무조건 금지

는 현재 baseline에서는 너무 강합니다.

정확한 규칙은 다음이어야 합니다.

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 개수 하나만 보면 부족합니다.

statementCount
entityLoadCount
entityFetchCount
collectionFetchCount
returnedParents
hydratedEntities
rowsFromDatabase
duration

를 함께 보아야 합니다. 예컨대 SQL 1개라도 100 parent × 100 child × 20 second-child Cartesian product가 만들어지면 좋은 Fetch Plan이 아닙니다.

테스트 fixture 역시:

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가 필요합니다.

ORDER BY created_at DESC, id DESC

라면 cursor도:

(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은 다음입니다.

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

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

권장 계층은 다음과 같습니다.

일반 업무 write
→ JPA Entity

수천~수만 row
→ JPA + JDBC batch + chunk flush/clear

대규모 migration/backfill
→ StatelessSession / JdbcTemplate / PostgreSQL COPY

운영 대량 수정
→ J4 Job

Bulk DML은 더 엄격합니다.

flush
→ JPQL/Native Bulk UPDATE
→ clear
→ 필요 시 재조회

가 기본 계약입니다. Bulk JPQL/Criteria DML은 managed entity state를 자동 동기화하지 않으며 optimistic locking도 자동 적용하지 않습니다. citeturn12view1

Cache 정책도 단순하게 가져가는 편이 안전합니다.

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

운영에서 다음은 기본 금지로 두는 것이 좋습니다.

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입니다.

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 정책은 아래처럼 나누는 것이 좋습니다.

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 생성기”보다 다음을 제공하는 것이 더 유용합니다.

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

Technical Auditing
createdAt / createdBy / modifiedAt / modifiedBy

≠

Business Audit
“누가 주문 상태를 왜 취소했는가”

≠

Entity History
과거 row revision

≠

Security Audit
관리자 권한·DDL·replay

Hibernate Envers는 Entity History 선택 기능으로 둘 수 있지만 J1 기본 기능으로 만들 필요는 없습니다.

Soft Delete 역시 global 기능으로 제공하지 않는 것이 좋습니다.

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

권장 오류 모델은 다음과 같습니다.

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에는:

maxAttempts
maxElapsedTime
exponentialBackoff
jitter
transaction deadline
retry metrics

가 있어야 합니다.

보안 정책은 Repository API보다 DB credential과 dynamic query 제한이 중요합니다.

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

추가 보안 규칙은 다음처럼 고정하는 것이 좋습니다.

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는 낮게 유지합니다.

허용:

persistenceUnit
operationName
bounded entityType
bounded queryName
outcome
failureCategory
isolation

금지:

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 중심으로 설계해야 합니다.

테스트 피라미드는 다음이 적절합니다.

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만 테스트하면 부족합니다.

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입니다.

public interface JpaTransactionExecutor {
    <T> T execute(TransactionProfile profile, Supplier<T> 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를 사용하게 하는 편이 좋습니다.

@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