JPA 쿼리 가이드
JPA 기본 설정
spring:
jpa:
open-in-view: false # 필수 — View 렌더링까지 DB 커넥션 유지하는 OSIV 패턴 방지
hibernate:
ddl-auto: validate # 운영 환경 고정값
properties:
hibernate:
# dialect 명시 불필요 — Hibernate 6+에서 DB 드라이버 기반 자동 감지
# 명시할 경우 deprecated 경고 발생
인덱스 설계
@Index columnList 주의사항
@Index의 columnList에는 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);