JPA 쿼리 가이드

JPA/Hibernate 사용 시 주의사항 — 인덱스 설계, 배치 저장, N+1 방지

마지막 수정: 2026-05

JPA 쿼리 가이드

JPA 기본 설정

spring:
  jpa:
    open-in-view: false   # 필수 — View 렌더링까지 DB 커넥션 유지하는 OSIV 패턴 방지
    hibernate:
      ddl-auto: validate  # 운영 환경 고정값
    properties:
      hibernate:
        # dialect 명시 불필요 — Hibernate 6+에서 DB 드라이버 기반 자동 감지
        # 명시할 경우 deprecated 경고 발생

인덱스 설계

@Index columnList 주의사항

@IndexcolumnList에는 Java 필드명이 아닌 실제 DB 컬럼명(snake_case) 을 사용해야 한다.

// ❌ 잘못된 예 — Java 필드명 사용 시 ddl-auto: validate에서 기동 실패
@Index(name = "idx_item_collectedAt", columnList = "collectedAt")

// ✅ 올바른 예 — DB 컬럼명 사용
@Index(name = "idx_collected_item_collected_at", columnList = "collected_at")

N+1 문제 방지

배치 저장 패턴 (쓰기 N+1 방지)

반복 저장 시 save() per-item 방식은 N번의 SELECT + N번의 INSERT가 발생한다. IN 쿼리로 중복을 일괄 확인한 후 saveAll()로 한 번에 저장한다.

// ❌ 잘못된 예 — N번의 SELECT + N번의 INSERT
for (Item item : items) {
    if (repository.findByFingerprint(item.getFingerprint()).isEmpty()) {
        repository.save(item);
    }
}

// ✅ 올바른 예 — SELECT 1회 + INSERT 1회
public void saveAllIfNotExists(List<Item> items) {
    if (items.isEmpty()) return;
    List<String> fingerprints = items.stream()
            .map(Item::getFingerprint).toList();
    Set<String> existing = repository.findExistingFingerprints(fingerprints);
    List<Item> newItems = items.stream()
            .filter(i -> !existing.contains(i.getFingerprint()))
            .toList();
    if (!newItems.isEmpty()) repository.saveAll(newItems);
}

Repository에 IN 쿼리 메서드 추가:

@Query("SELECT i.fingerprint FROM Item i WHERE i.fingerprint IN :fingerprints")
Set<String> findExistingFingerprints(
        @Param("fingerprints") Collection<String> fingerprints);