영속성 컨텍스트의 주요 기능
JPA의 영속성 컨텍스트는 엔티티를 관리하는 논리적 저장소로서, 성능 최적화 및 데이터 일관성을 유지하는 중요한 역할을 한다. 이를 잘 활용하면 불필요한 쿼리를 줄이고 성능을 향상시킬 수 있다.
1차 캐시
엔티티 조회 시, 먼저 영속성 컨텍스트의 1차 캐시에서 데이터를 찾고, 없을 경우에만 DB에서 조회한다.
같은 트랜잭션 내에서는 동일한 엔티티를 DB 조회 없이 1차 캐시에서 재사용할 수 있어 성능 최적화가 가능하다.
쓰기 지연
`EntityManager.persist()`를 호출해도 즉시 DB에 반영되지 않고, 트랜잭션이 끝날 때 `flush()`가 발생하며 한 번에 실행된다.
INSERT, UPDATE, DELETE 같은 SQL이 쓰기 지연 저장소에 모였다가 트랜잭션이 끝나면 한꺼번에 실행됨으로써 성능을 개선할 수 있다.
변경 감지
트랜잭션이 끝날 때 변경된 값이 자동으로 감지되어 UPDATE 쿼리가 실행된다.
`flush()` 시점에서 스냅샷(초기 상태)과 현재 엔티티의 상태를 비교하여 변경된 데이터만 UPDATE하는 방식.
(스냅샷: JPA는 엔티티가 영속성 컨텍스트에 처음 들어올 때 그 상태를 복사해 저장)
지연 로딩 (Lazy Loading)
`@OneToMany(fetch = FetchType.LAZY)` 설정 시 연관된 엔티티는 실제로 접근할 때만 로드된다.
불필요한 데이터 로딩을 방지하여 성능을 최적화할 수 있다.
JPA에서 JOIN을 직접 사용할 수 없는 이유
JPA는 객체 지향 프로그래밍을 기반으로 하므로, SQL처럼 직접 JOIN을 사용하여 데이터를 가져오는 방식과 다르다.
대신, JPQL(Java Persistence Query Language)을 사용하여 JOIN을 작성해야 한다.
JPQL을 활용한 JOIN
JPQL은 SQL과 비슷하지만, 객체(Entity)를 기준으로 작성한다.
예제: `@Query` 어노테이션을 활용한 JPQL 기본
interface StudentRepository extends JpaRepository<Student, Long> {
@Query("SELECT s FROM Student s JOIN s.course c WHERE c.name = :courseName")
List<Student> findStudentsByCourse(@Param("courseName") String courseName);
}
N+1 문제와 해결 방법
N+1 문제란?
`@OneToMany(fetch = FetchType.LAZY)` 설정 시 1개의 SELECT 쿼리 실행 후, 관련 엔티티를 조회할 때 추가적인 N개의 쿼리가 발생하는 문제
예제:
public void getOrderList() {
List<Order> orderList = repository.findAll(); // (1) 1번 실행
//Order 테이블에서 모든 주문(Order) 데이터를 가져옴.
//쿼리 실행 횟수: 1번
//이 시점에서 OrderDetails 데이터는 가져오지 않음! (LAZY 로딩이기 때문)
for (Order order : orderList) {
System.out.println(order.getOrderDetails()); // (2) 100번 실행 (N번)
//getOrderDetails()를 호출하면 그때서야 OrderDetails 데이터를 조회하기 위해 추가 쿼리 실행됨.
//주문(Order)이 100개 있다고 가정하면, 100번 추가적인 쿼리가 실행됨!
}
}
N+1 문제 해결 방법
FetchType.EAGER로 변경 (N+1 문제가 생길만한 엔티티만)
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
JPQL을 이용한 패치 조인
@Query("SELECT o FROM Order o JOIN FETCH o.orderDetails")
List<Order> findAllOrderWithOrderDetails();
@EntityGraph 어노테이션 사용 (잘 사용하지 않음)
@EntityGraph(attributePaths = {"orderDetails"})
List<Order> findAllOrderWithOrderDetails();
QueryDSL
QueryDSL은 JPA의 JPQL을 타입 안전한 방식으로 작성할 수 있도록 도와주는 DSL(Domain Specific Language)이다.
QueryDSL 장점
- 타입 안전성: JPQL은 런타임 오류 가능성이 있지만, QueryDSL은 컴파일 타임에 오류를 잡을 수 있다.
- 동적 쿼리 작성이 용이: `BooleanExpression`을 활용하여 조건을 쉽게 조합 가능.
- 코드 가독성 향상: 네이티브 SQL보다 직관적인 문법 제공. JPQL을 문자열로 작성하지 않고, 코드 기반으로 쿼리를 작성
QueryDSL vs JPQL
JPQL (문자열 기반, 런타임 오류 가능성 있음)
@Query("SELECT s FROM Student s WHERE s.name = :name")
List<Student> findStudentByName(@Param("name") String name);
QueryDSL (타입 안전, 컴파일 타임 오류 검출 가능)
QStudent student = QStudent.student;
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
List<Student> students = queryFactory.selectFrom(student)
.where(student.name.eq("John Doe"))
.fetch();
결론
- JPA의 영속성 컨텍스트는 1차 캐시, 변경 감지, 지연 로딩을 통해 불필요한 쿼리를 줄이고 성능을 최적화할 수 있다.
- JPA는 객체(Entity) 중심으로 동작하며, 자동으로 SQL을 생성한다.
- JOIN을 사용해 객체 간 관계를 조회할 수 있다.
- JPQL을 사용해 JOIN을 작성하며, 페치 조인(FETCH JOIN)을 활용하면 N+1 문제를 해결할 수 있다.
- QueryDSL을 활용하면 JPQL보다 타입 안전하고 가독성이 높은 쿼리 작성이 가능하다.
'Java Study > Frameworks' 카테고리의 다른 글
[Spring] @Transactional을 어디에 붙여야 할까🤔❔ (0) | 2025.02.12 |
---|---|
[Spring] spring.jpa.hibernate.ddl-auto 설정과 각 옵션 (0) | 2025.02.12 |
[Spring] JPA에서 꼭 알아야 할 개념: 영속성 (0) | 2025.02.10 |
[Spring] Spring Data JPA에서 @Repository를 사용하지 않는 이유 (1) | 2025.02.09 |
[Spring] JPA Auditing 적용하기: @CreatedDate, @LastModifiedDate 활용법 (0) | 2025.02.07 |