| name | jpa-patterns |
| description | Spring Boot에서 엔터티 설계, 관계 설정, 쿼리 최적화, 트랜잭션, 감사, 인덱싱, 페이지네이션, 커넥션 풀링을 위한 JPA/Hibernate 패턴입니다. |
| origin | ECC |
JPA/Hibernate Patterns
Spring Boot에서 데이터 모델링, repository, 성능 튜닝에 사용합니다.
활성화 시점
- JPA 엔터티와 테이블 매핑을 설계할 때
- 관계(
@OneToMany, @ManyToOne, @ManyToMany)를 정의할 때
- 쿼리를 최적화할 때
- 트랜잭션, auditing, soft delete를 설정할 때
- pagination, sorting, custom repository 메서드를 만들 때
- HikariCP나 second-level cache를 튜닝할 때
엔터티 설계
@Entity
@Table(name = "markets", indexes = {
@Index(name = "idx_markets_slug", columnList = "slug", unique = true)
})
@EntityListeners(AuditingEntityListener.class)
public class MarketEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true, length = 120)
private String slug;
@Enumerated(EnumType.STRING)
private MarketStatus status = MarketStatus.ACTIVE;
@CreatedDate private Instant createdAt;
@LastModifiedDate private Instant updatedAt;
}
Auditing 활성화:
@Configuration
@EnableJpaAuditing
class JpaConfig {}
관계와 N+1 방지
@OneToMany(mappedBy = "market", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PositionEntity> positions = new ArrayList<>();
- 기본은 lazy loading
- 필요할 때만
JOIN FETCH
- 컬렉션에
EAGER는 피하고 읽기 경로는 DTO projection을 선호
@Query("select m from MarketEntity m left join fetch m.positions where m.id = :id")
Optional<MarketEntity> findWithPositions(@Param("id") Long id);
Repository 패턴
public interface MarketRepository extends JpaRepository<MarketEntity, Long> {
Optional<MarketEntity> findBySlug(String slug);
@Query("select m from MarketEntity m where m.status = :status")
Page<MarketEntity> findByStatus(@Param("status") MarketStatus status, Pageable pageable);
}
Projection 예시:
public interface MarketSummary {
Long getId();
String getName();
MarketStatus getStatus();
}
Page<MarketSummary> findAllBy(Pageable pageable);
트랜잭션
- 서비스 메서드에
@Transactional
- 읽기 경로는
@Transactional(readOnly = true)
- 긴 트랜잭션은 피합니다
@Transactional
public Market updateStatus(Long id, MarketStatus status) {
MarketEntity entity = repo.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Market"));
entity.setStatus(status);
return Market.from(entity);
}
페이지네이션
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
Page<MarketEntity> markets = repo.findByStatus(MarketStatus.ACTIVE, page);
cursor 유사 페이지네이션은 id > :lastId 형태로 구현합니다.
인덱싱과 성능
- 자주 필터링되는 컬럼에 인덱스 추가
- 쿼리 패턴에 맞는 복합 인덱스 사용
select * 대신 필요한 컬럼만 projection
- 대량 쓰기는
saveAll과 hibernate.jdbc.batch_size 활용
Connection Pooling
권장 속성:
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.validation-timeout=5000
PostgreSQL LOB 처리:
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
캐싱
- 1차 캐시는 EntityManager 단위
- 읽기 중심 엔터티에 한해 2차 캐시를 신중하게 검토하고 eviction 전략을 검증합니다
마이그레이션
- Flyway 또는 Liquibase 사용
- 프로덕션에서 Hibernate auto DDL에 의존하지 않습니다
- migration은 additive하고 idempotent하게 유지합니다
데이터 접근 테스트
@DataJpaTest + Testcontainers를 선호합니다
- SQL 효율은 로그로 검증합니다
엔터티는 가볍게, 쿼리는 의도적으로, 트랜잭션은 짧게 유지합니다. fetch 전략, projection, 인덱스로 N+1을 방지합니다.