2025-05-28
JPA와 Hibernate
JPAHibernateSpringN+1성능 튜닝
JPA와 Hibernate
JPA와 Hibernate를 한 단계 더 깊이 이해하기 위해 중요한 네 가지 주제를 다뤄보겠습니다. 실제 예제를 통해 영속성 컨텍스트의 동작 원리부터, 효과적인 로딩 전략, N+1 문제 해결 기법, 그리고 쿼리 성능 튜닝까지 살펴보겠습니다.
1. 영속성 컨텍스트, flush, dirty checking
JPA의 핵심은 '영속성 컨텍스트(Persistence Context)'입니다. 영속성 컨텍스트는 EntityManager가 관리하는 1차 캐시로, 애플리케이션에서 조회한 엔티티를 보관합니다. 이 컨텍스트 안에서는 엔티티의 변경을 자동으로 감지하고, 필요 시점에 DB로 동기화해 주죠.
@Transactional
void dirtyCheckingTest(EntityManager em) {
Member member = em.find(Member.class, 1L); // 1) 영속성 컨텍스트에 엔티티 등록
member.setName("홍길동"); // 2) 필드 변경 (dirty)
em.flush(); // 3) 변경 사항을 즉시 DB 반영
// → UPDATE MEMBER SET name = '홍길동' WHERE id = 1
}- dirty checking: 변경 내역을 스캔해 자동으로 UPDATE 쿼리를 생성
- flush(): 영속성 컨텍스트와 DB를 동기화
- 주의: flush만 호출해도 SQL은 즉시 실행되지만, 트랜잭션이 커밋되어야 실제로 반영됩니다.
2. Fetch 전략 (즉시 vs 지연 로딩)
연관관계를 맺은 엔티티를 조회할 때, 언제 SQL이 실행될지 결정하는 것이 Fetch 전략입니다.
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private Member member; // 기본값이 LAZY
}- 즉시(EAGER): 부모 조회 시점에 연관 엔티티도 함께 즉시 조회
- 지연(LAZY): 실제 접근 시점에 프록시가 초기화되며 조회
- 팁: LAZY 로딩 후 트랜잭션을 벗어나면
LazyInitializationException이 발생하니, 꼭 필요한 곳에서만 사용하고 조회 전략을 조심스럽게 선택하세요.
3. N+1 문제와 해결
지연 로딩을 사용하다 보면 흔히 마주치는 N+1 문제. 예를 들어, 모든 주문을 조회하고 각 주문마다 회원을 가져오는 경우입니다.
List<Order> orders = em.createQuery("select o from Order o", Order.class)
.getResultList(); // 1번 쿼리
orders.forEach(o -> System.out.println(o.getMember().getName()));
// 주문 개수(N)만큼 추가 쿼리 발생 → N+1해결 방안
Fetch Join
List<Order> orders = em.createQuery(
"select o from Order o join fetch o.member", Order.class)
.getResultList(); // 단 1개의 쿼리로 해결EntityGraph
@NamedEntityGraph(name = "Order.withMember",
attributeNodes = @NamedAttributeNode("member"))
public class Order { ... }
// 조회 시 힌트 지정
em.createQuery("select o from Order o", Order.class)
.setHint("javax.persistence.fetchgraph", em.getEntityGraph("Order.withMember"))
.getResultList();이 두 기법으로 연관 엔티티를 한 번에 가져와 불필요한 추가 쿼리를 방지할 수 있습니다.
4. 성능 개선을 위한 쿼리 튜닝
1) 인덱스 적용
컬럼에 인덱스를 걸어 검색 비용을 크게 줄일 수 있습니다.
@Table(name = "MEMBER",
indexes = @Index(name = "idx_member_email", columnList = "email"))
public class Member { ... }2) Projection
불필요한 모든 컬럼을 조회하지 않고, 필요한 데이터만 DTO나 인터페이스로 가져오면 조회 속도가 빨라집니다.
DTO 생성자
public class MemberDto {
private Long id;
private String name;
public MemberDto(Long id, String name) {
this.id = id;
this.name = name;
}
// getters
}
// JPQL
List<MemberDto> dtos = em.createQuery(
"select new com.example.dto.MemberDto(m.id, m.name) " +
"from Member m " +
"where m.status = :status", MemberDto.class)
.setParameter("status", "ACTIVE")
.getResultList();인터페이스 프로젝션 (Spring Data JPA)
// 프로젝션 인터페이스
public interface MemberSummary {
Long getId();
String getName();
}
// Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
List<MemberSummary> findByStatus(String status);
}
// 사용
List<MemberSummary> summaries = memberRepository.findByStatus("ACTIVE");마무리
- 인덱스는 검색 속도의 핵심 열쇠이지만, 너무 많이 걸면 쓰기 성능이 저하됩니다.
- 프로젝션은 불필요한 데이터를 깔끔히 제거해 전송·처리 비용을 크게 절감합니다.