개요
데이터베이스에 연관 관계를 가진 엔티티를 저장할 때, 우리는 종종 연관된 엔티티의 모든 정보를 조회(SELECT)하게 된다. 하지만 이러한 방식은 의문점을 들게한다.
'실제로 DB에 저장되는건 연관 객체의 ID뿐인데, 왜 모든 정보를 조회해야 할까?'
@Service
@Transactional
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
public void saveOrder(Long customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> throw new BusinessLogicException(ExceptionCode.CUSTOMER_NOT_FOUND);
Order order = new Order();
order.setCustomer(customer);
orderRepository.save(order);
}
}
-- Customer 조회
SELECT c.*
FROM customer c
WHERE c.id = ?
-- Order 저장
INSERT INTO orders (customer_id, ...) VALUES (?, ...)
10만 개의 엔티티를 저장한다면, 연관 관계를 위해 10만 번의 추가 SELECT 쿼리가 발생한다. 1:N, N:1 관계나 여러 연관 관계가 있다면 이 문제는 더욱 심각해진다. 불필요한 쿼리를 줄이는 것은 애플리케이션의 성능을 크게 최적화할 수 있다.
분명 이러한 고민을 하고 해결한 분들이 있을 것이다. 연관 관계를 위한 불필요한 select문을 줄이는 방법에 대해 알아보자
getReferenceById()
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
...
List<T> findAllById(Iterable<ID> ids);
T getReferenceById(ID id);
...
}
이럴 때 getReferenceById() 메서드를 사용할 수 있다. 이해하기 쉽도록 우리에게 익숙한 findById() 메서드와 비교해보자.
findById() vs getReferenceById()
- DB 조회 시점
- findById: 메서드 실행 즉시 DB 조회
- getReferenceById: 프록시 객체를 반환하고, id 외의 필드에 접근 시 DB 조회. 영속성 컨텍스트에 해당 엔티티가 있다면 프록시 객체가 아닌 엔티티를 반환
- 반환 타입
- findById: Optional<Entity> 반환
- getReferenceById: Entity 타입 반환
- 존재하지 않는 엔티티 조회할 경우
- findById: Optional.empty() 반환
- getReferenceById: 존재하지 않는 엔티티더라도 프록시 객체를 반환. id를 제외한 필드 접근 시 EntityNotFoundException 예외 발생
위와 같은 특징덕분에 연관 관계 매핑에서 외래 키만 필요한 경우 유용하게 사용할 수 있다.
getReferenceById() 메서드를 통해 불필요한 DB 조회를 줄여 성능을 향상 시킬 수 있다.
@Service
@Transactional
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
public void saveOrder(Long customerId) {
Customer customer = customerRepository.getReferenceById(customerId);
Order order = new Order();
order.setCustomer(customer);
orderRepository.save(order);
}
}
-- Order 저장 쿼리만 나간다
INSERT INTO orders (customer_id, ...) VALUES (?, ...)
💡프록시 객체 getId() 메서드는 왜 DB 조회하지 않을까?
Hibernate는 프록시 객체를 생성할 때 Primary Key 속성을 설정한다. 그래서 getId() 메서드를 호출할 때 DB를 조회할 필요가 없다. 하지만 id가 아닌 필드에 접근할 때는 DB를 조회해야 한다. 이러한 이유때문에 존재하지 않는 id로 생성된 프록시 객체에서 id가 아닌 필드에 접근할 때까지는 존재하지 않는 Entity라는 것을 인지하지 못한다. id 외의 필드에 접근하면 EntityNotFoundException 예외를 발생시킨다.
Long nonExistentId = 999L; // DB에 존재하지 않는 ID
Member member = memberRepoistory.getReferenceById(nonExistentId); // 프록시 객체 반환
Long id = member.getId(); // DB조회 안함
String name = customer.getName(); // DB 조회
id를 조회하는 메서드를 호출하면 최종적으로 AbstractLazyInitilizer 클래스의 getIdentifier 메서드를 호출하게 된다.
이 때 if 문 바깥을 보면 id를 반환하는 것을 알 수 있다. AbstractLazyInitilizer 클래스는 id값을 가지고 있기 때문에 가능하다.
public abstract class AbstractLazyInitializer implements LazyInitializer {
...
public final Serializable getIdentifier() {
if (this.isUninitialized() && this.isInitializeProxyWhenAccessingIdentifier()) {
this.initialize();
}
return this.id;
}
...
}
if문의 조건을 true로 처리하기 위해서는 application.yml에서 hibernate.jpa.compilance.proxy 값을 true로 설정해주면 id 접근 시에도 프록시 객체를 초기화할 수 있다.
프록시에 대한 자세한 내용은 추후에 다루도록 하겠다.
참조
https://docs.oracle.com/javaee/7/api/javax/persistence/EntityNotFoundException.html
https://thorben-janssen.com/jpa-getreference/
https://tecoble.techcourse.co.kr/post/2022-10-17-jpa-hibernate-proxy/
'JPA' 카테고리의 다른 글
JPA - JPA에서 일괄 삭제하는 방법과 주의점 (deleteAllById, deleteAllByIdIn, deleteAllByIdInBatch, Querydsl) (3) | 2024.12.04 |
---|---|
Querydsl - Expressions클래스로 select에서 상수 사용하는 법 (1) | 2024.11.14 |
Spring Data JPA - 외래키(Foreign Key)를 복합 기본키(Composite Primary Key)로 사용하기 (1) | 2024.11.08 |
JPA - 하나의 컬럼에 여러 개의 데이터를 저장하기 (0) | 2023.05.08 |
JPA - Querydsl를 사용해 DTO 받는 방법 (0) | 2023.03.02 |