115 KiB
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 목표
- 도메인 Repository를 보존하면서 JPA·Hibernate·PostgreSQL 사용 규칙을 일관되게 제공한다.
- Application Use Case 단위 Transaction과 전체 Transaction Retry를 구현한다.
- Optimistic Conflict, Deadlock, Serialization Failure, Lock Timeout, Constraint Violation, Commit 결과 불명을 안정 오류로 변환한다.
- OSIV, 전역 EAGER, 전역 Cascade, 전역 Soft Delete, 운영
ddl-auto=update같은 위험한 기본값을 구조적으로 차단한다. - EntityGraph, Fetch Join, Projection, Batch Fetch, Keyset Pagination을 Use Case별 Fetch·Query 전략으로 제공한다.
- JDBC Batch, Bulk DML, StatelessSession, PostgreSQL Native Write를 서로 다른 Capability로 제공한다.
- Flyway를 운영 Schema 변경의 Source of Truth로 고정하고 빈 DB·이전 Release Snapshot·최장 지원 Snapshot 업그레이드를 검증한다.
- H2가 아닌 PostgreSQL 16·17·18 실제 의미론으로 Stable을 인증한다.
- Query Count, Entity/Collection Fetch, Row Load, Query Plan, Pool·Transaction·Retry를 관측한다.
- 일반 애플리케이션이 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. 핵심 설계 원칙
- 도메인 소유권 유지: Entity·Embeddable·Repository·업무 Query·Index Requirement는 도메인이 소유한다.
- 추상화 중복 금지: Spring Data의 CRUD 추상화를 다시 감싸지 않는다.
- Use Case Transaction: Transaction은 Application Use Case 단위다.
- 전체 Transaction Retry: Retry는 새 Persistence Context와 새 Transaction에서 전체 작업을 다시 실행한다.
- 불명확성 보존: Commit 결과를 모르면 성공 또는 실패로 추정하지 않는다.
- Fetch Plan 명시: Mapping annotation 하나로 모든 Use Case의 Fetch를 결정하지 않는다.
- Schema Source of Truth 분리: Entity Mapping은 객체-관계 매핑 계약이고 실제 Schema 변경은 Flyway가 소유한다.
- PostgreSQL 실제 검증: H2나 mock으로 Lock·Constraint·SQLSTATE·Plan 의미론을 증명하지 않는다.
- Provider 차이 노출: Hibernate·PostgreSQL 고유 기능은 J3 Extension으로 명시한다.
- 위험 기능 opt-in: REQUIRES_NEW, Native SQL, Bulk DML, StatelessSession, L2 Cache, Envers는 선택 모듈이다.
- 정량 성능 검증: Query 수뿐 아니라 rows, hydrated entity, collection fetch, batch, pool wait를 측정한다.
- 권한 최소화: 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-api는 jakarta.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 감지
EvidenceAwareJpaTransactionManager가 doCommit 진입 전 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 3 |
| 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 |
| 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 관계형 영속성 플랫폼 심층 리서치
이번 조사의 결론부터 정리하면, jpa는 JpaRepository를 한 번 더 감싸는 공통 Repository 라이브러리로 설계해서는 안 됩니다. Spring Data JPA 자체가 이미 Repository, Query Method, Pagination, Auditing, Custom Repository, Querydsl 통합 등을 제공하고 있으므로, 공통 플랫폼이 다시 CRUD 추상화를 만드는 것은 기능 중복과 추상화 누수를 동시에 만듭니다. 현재 Spring Data JPA 공식 프로젝트 페이지의 안정 버전은 4.1.0입니다. citeturn20view0
따라서 권장 구조는 다음과 같습니다.
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 생태계의 검증된 조합을 관리합니다. citeturn20view0turn20view1
Hibernate ORM의 현재 안정 계열은 7.4이며, Hibernate의 7.4 문서는 현재 7.4.6.Final을 기준으로 제공되고 있습니다. Jakarta Persistence의 완성된 현재 규격은 3.2이고, Persistence 4.0은 아직 개발 중이며 2026년 후반을 목표로 하고 있으므로 Stable 계약으로 고정하면 안 됩니다. citeturn13search0turn7search2turn7search1
따라서 지원 매트릭스는 다음이 적절합니다.
| 구성요소 | 권장 등급 | 기준 |
|---|---|---|
| Java 21 | Stable baseline | 플랫폼 언어 기준선 |
| Spring Boot BOM | Stable baseline | 개별 dependency 임의 조합 금지 |
| Spring Data JPA 4.1 | Stable | Repository·Projection·Specification·Auditing의 기본 진입점 citeturn20view0 |
| Jakarta Persistence 3.2 | Stable | 표준 JPA 계약 citeturn7search2turn17search0 |
| Hibernate ORM 7.4 | Stable provider | 기본 JPA Provider citeturn13search0 |
| 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년 지원 citeturn0search3turn13search5 |
| H2 | Local Convenience | PostgreSQL 호환성 증명에 사용하지 않음 |
| Testcontainers PostgreSQL | Required | 실제 PostgreSQL 의미론을 검증하는 Contract 환경 |
| Jakarta Persistence 4.0 | Experimental | 아직 개발 중 citeturn7search1 |
| 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 문서를 별도로 제공합니다. citeturn13search5
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 차이를 없애지 못하며 오히려 숨길 뿐입니다. citeturn9search0
권장 공개 구조는 따라서 다음입니다.
// 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로 결합할 수 있도록 설계되어 있습니다. citeturn20view0
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 위치에 의해 결정되고, 계층 안에서 이를 암묵적으로 뒤섞으면 동작이 정의되지 않으므로 접근 전략을 일관되게 유지해야 합니다. citeturn17search0
따라서 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로 두는 것이 안전합니다. citeturn17search0turn7search2
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를 유지하기 쉽습니다. citeturn14view0turn14view1
| 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을 뜻하지도 않습니다. citeturn8search0turn8search12turn17search0
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을 기본으로 삼는 것이 적절합니다. citeturn17search0
권장 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 같은 편의 메서드 계약을 두는 것이 좋습니다. citeturn12view0
Persistence Context 계약도 API 문서보다 중요합니다. persist, merge, flush, commit은 서로 다른 의미를 가집니다. merge()는 detached instance 자체를 managed로 바꾸는 것이 아니라 그 state를 managed instance에 복사하는 방식이고, flush()는 Persistence Context를 DB와 동기화하지만 transaction commit과 동일하지 않습니다. citeturn12view0
플랫폼 계약은 아래처럼 고정하는 것이 좋습니다.
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 오류의 직접 원인이 됩니다. citeturn12view1
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이 실제 경계를 결정합니다. citeturn15search1
따라서 기본 계약은 다음입니다.
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를 사용하는 것이 더 안전한 경계입니다. citeturn15search7
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를 기반으로 하는 의미론입니다. citeturn15search0
또한 Spring의 기본 proxy transaction model에서는 self-invocation이 transactional interception을 거치지 않습니다. 따라서 같은 클래스 안에서 this.someRequiresNewMethod()를 호출하고 별도 transaction이 생성된다고 가정하는 코드는 금지 대상이 되어야 합니다. citeturn15search6turn15search9
Spring @Transactional의 기본값은 REQUIRED, isolation DEFAULT, read-write이며, 기본 rollback 규칙은 RuntimeException과 Error입니다. Checked exception까지 rollback해야 하는 업무에서는 rollbackFor 또는 안정적인 application exception hierarchy를 명시해야 합니다. citeturn15search6
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 전체를 처음부터 다시 실행해야 한다고 명시합니다. citeturn13search9turn8search6
이 때문에 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 호출 직후 충돌이 반드시 드러난다고 가정하면 안 됩니다. citeturn12view2
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에 적합하다고 공식 문서가 명시합니다. citeturn12view3turn8search4turn8search5
따라서 SKIP LOCKED를 findAllUnlocked() 같은 공통 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를 제공할 수 있습니다. citeturn17search1
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이 권고합니다. citeturn13search1
따라서 플랫폼에는 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로 대체하는 것이 아니라 어떤 레벨에서 무엇을 쓸지 결정하는 것입니다. citeturn20view0
권장 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() 등에 직접 연결하지 않는 정책이 필요합니다. citeturn18search12
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가 크게 증가할 수 있음을 문서화하고 있습니다. citeturn13search14
여기에는 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”가 이를 명시적으로 새 기능으로 소개합니다. citeturn13search0turn13search11
따라서 기존 규칙인
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 제한을 영구 정책으로 굳히게 됩니다. citeturn13search0turn13search2
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을 지원하지 않는 제한을 명시합니다. citeturn18search12turn9search8
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를 권고합니다. citeturn14view1
권장 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()를 권장합니다. citeturn14view1
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으로 분류해야 합니다. citeturn14view4
권장 계층은 다음과 같습니다.
일반 업무 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도 자동 적용하지 않습니다. citeturn12view1
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가 이를 자동 인지하지 못한다는 제한도 있습니다. citeturn14view3
즉 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로 저장됩니다. citeturn19search0
Versioned migration은 순서대로 한 번 적용하고 이미 영구 환경에 적용한 파일은 수정하지 않고 새 migration으로 roll-forward하는 것이 Flyway가 권장하는 방식입니다. Repeatable migration은 checksum이 변경될 때 다시 실행됩니다. citeturn19search3turn19search6
repair는 단순한 “검증 복구” 기능이 아닙니다. 실패 migration 기록 제거, checksum/description/type 재정렬, missing migration을 deleted로 표시하는 등의 변경을 수행하며, DB에 남은 user object는 수동으로 정리해야 할 수 있습니다. 따라서 J4 승인 작업으로 두어야 합니다. citeturn19search1
무중단 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로 명시적으로 분리해야 합니다. citeturn17search3turn19search16
Flyway의 group=true는 여러 pending migrations를 한 transaction에 묶는 옵션이지만, DDL transaction을 적절히 지원하는 DB에서만 권장되며 기본은 false입니다. 무조건 활성화할 설정이 아닙니다. citeturn19search13
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을 기준으로 설계해야 합니다. citeturn17search2turn8search9
플랫폼은 “자동 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만 citeturn8search4 |
| 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 citeturn17search1 |
| 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에 선택적으로 적용하도록 해야 합니다. citeturn20view0turn18search5
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도 전달합니다. citeturn13search1
권장 오류 모델은 다음과 같습니다.
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을 기준으로 구조화할 수 있습니다. citeturn13search1
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를 고정·검증해야 합니다. citeturn21search6
추가 보안 규칙은 다음처럼 고정하는 것이 좋습니다.
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를 제공합니다. citeturn21search0
공통 관측 계약은 이를 다음처럼 확장하는 것이 적절합니다.
| 계층 | 필수 관측 |
|---|---|
| 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입니다. citeturn11search2turn21search0
테스트는 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에서 수행하는 기반으로 적합합니다. citeturn11search0turn11search10
필수 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 대상으로 삼기 때문입니다. citeturn19search0turn19search4
실무 실패 사례를 플랫폼 규칙으로 변환하면 다음과 같습니다.
| 상황 | 직접 원인 | 설계 규칙 | 회귀 테스트 |
|---|---|---|---|
| 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 citeturn13search0 |
saveAll()인데 batch 없음 |
JDBC batch 미설정/IDENTITY | actual batch 관측 | statement/batch count |
| IDENTITY batch 실패 | ID 얻기 위해 즉시 insert | write-heavy Entity는 sequence | ID strategy benchmark citeturn14view0 |
| Bulk update 후 stale | PC 미동기화 | flush → DML → clear | stale entity assertion citeturn12view1 |
| TX 안에서 API 대기 | DB resource 장기 보유 | 외부 I/O TX 밖 | pool pressure test |
REQUIRES_NEW 고갈 |
outer+inner connection 동시 점유 | 제한 + pool capacity test | concurrent nested tx citeturn15search0 |
| 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가 이미 그 추상화를 제공하기 때문입니다. citeturn20view0
둘째, transaction은 Repository method가 아니라 Application Use Case 단위이며, Optimistic conflict·Deadlock·Serialization Failure의 retry도 새 Persistence Context에서 전체 transaction을 다시 실행합니다. PostgreSQL Serializable 역시 transaction 재실행을 전제로 합니다. citeturn13search9
셋째, DB에 요청을 보냈다는 사실과 commit이 확정됐다는 사실을 구분합니다. Commit 결과가 모호하면 TransactionCompletionUnknown으로 올리고 자동 retry하지 않습니다. PostgreSQL도 completion-unknown을 SQLSTATE에서 별도 condition으로 정의합니다. citeturn13search1
넷째, Fetch 전략은 Entity annotation의 EAGER/LAZY만으로 결정하지 않고 use-case-specific Fetch Plan으로 관리합니다. 특히 Hibernate 7.4에서 PostgreSQL의 collection fetch join + pagination 동작이 과거 버전과 달라졌으므로, 오래된 금지 규칙을 그대로 복사하지 말고 현재 버전 SQL을 contract test해야 합니다. citeturn13search0turn13search11
다섯째, Entity Mapping이 schema의 Source of Truth가 아닙니다. 운영 schema는 Flyway가 소유하고 Hibernate는 validate 역할을 맡으며, repair, concurrent index, backfill, partition 같은 작업은 J4 Admin 영역으로 분리합니다. citeturn19search0turn19search1turn17search3
여섯째, H2에서 된다를 호환성 증거로 쓰지 않습니다. PostgreSQL 16·17·18의 실제 locking, constraint, batch, migration, query plan, SQLSTATE를 통과하는 것을 이 플랫폼의 Stable 완료 조건으로 삼는 것이 적절합니다. citeturn0search3turn13search5