구현 클래스를 쓰지 않았는데 동작하는 이유는 Spring이 런타임에 구현체(프록시)를 만들어 빈으로 등록하기 때문이다.
어떻게 만들어지나
① 인터페이스를 스캔한다 (JpaRepository 상속)
② 각 메서드를 분류한다
· @Query 가 붙었나 → 그 JPQL/SQL 을 쓴다
· 이름이 규칙에 맞나 → PartTree 파서가 쿼리를 만든다
· 기본 제공 메서드인가 → SimpleJpaRepository 구현으로 위임
③ 이 셋을 라우팅하는 프록시를 만들어 빈으로 등록한다
메서드 이름이 곧 쿼리 — PartTree 파서
List<Order> findByMemberIdAndStatusOrderByCreatedAtDesc(Long memberId, String status);
// └주제┘└──────── 조건 ─────────┘└──── 정렬 ────┘
// → SELECT o FROM Order o WHERE o.member.id = ?1 AND o.status = ?2 ORDER BY o.createdAt DESC
지원하는 키워드
- And · Or · Between · LessThan · GreaterThanEqual · Like · StartingWith
- In · IsNull · True · Not · Containing · IgnoreCase ...
이름이 규칙에 안 맞으면 기동 시점에 실패한다 — 오타를 런타임이 아니라 부팅에서 잡아 준다는 게 장점이다.
PropertyReferenceException: No property 'memberld' found for type 'Order'
이름이 길어지면 @Query로
@Query("select o from Order o join fetch o.member where o.status = :status")
List<Order> findWithMember(@Param("status") String status);
@Query(value = "SELECT * FROM orders WHERE ...", nativeQuery = true)
List<Order> findNative();
조건이 4개를 넘어가면 이름이 읽기 어려워진다 — 그 지점이 @Query나 QueryDSL로 넘어갈 신호다.
save()의 숨은 분기
// SimpleJpaRepository
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) { em.persist(entity); return entity; }
else { return em.merge(entity); } // ← 여기가 함정
}
merge 는 '준영속 엔티티를 다시 붙이는' 연산이다
- DB 를 한 번 SELECT 해서 현재 상태를 읽고
- 넘긴 엔티티의 모든 필드로 덮어쓴다
결과: 일부 필드만 채워 넘기면 나머지가 null 로 덮인다
영속 상태의 엔티티는 save()를 부를 필요가 없다 — 변경 감지가 알아서 한다.
면접 함정
- ❌ "Spring Data가 리플렉션으로 매번 쿼리를 만든다" → 기동 시점에 만들어 둔다.
- ❌ "save()는 항상 INSERT/UPDATE" → 새 엔티티면
persist, 아니면merge다.