JPA findById
vs getReferenceById
JPA(Java Persistence API)를 사용하여 데이터베이스에서 엔티티를 조회할 때 일반적으로 사용되는 두 가지 메서드가 있습니다. findById
와 getReferenceById
는 일부 유사점이 있지만, 중요한 차이점이 있습니다.
findById
- 지정된 ID의 엔티티를 조회하고, 해당 엔티티가 없으면
null
을 반환합니다. - 데이터베이스에서 엔티티를 직접 조회합니다.
- 엔티티 객체 또는
null
을 반환합니다. - 데이터베이스에서 엔티티를 로드하는 데 필요한 쿼리를 생성하고 실행합니다.
- 데이터베이스 조회를 즉시 수행하고 엔티티를 직접 반환합니다.
getReferenceById
- 지정된 ID의 엔티티에 대한 프록시 참조를 반환합니다.
- 데이터베이스에서 엔티티를 직접 조회하지 않습니다. 대신, 엔티티에 대한 프록시 참조를 반환합니다.
- 엔티티에 대한 프록시 참조를 반환합니다.
- 데이터베이스에 쿼리를 실행하지 않습니다. 대신, 엔티티를 지연 로딩(lazy loading)하는 데 사용됩니다.
- 엔티티에 실제로 접근하거나, 엔티티의 속성에 접근하거나, 엔티티의 메서드를 호출할 때 데이터베이스 조회가 발생합니다.
- 엔티티에 대한 프록시 참조를 반환하여 지연 로딩을 구현합니다.
각 메서드의 사용 시점
findById
사용 시점
- 데이터베이스에서 엔티티를 즉시 로드하고 싶은 경우
- 엔티티를 직접 조작하거나, 여러 엔티티 속성에 접근하거나, 엔티티의 메서드를 호출해야 하는 경우
getReferenceById
사용 시점
- 데이터베이스 조회를 지연시키고 싶은 경우
- 엔티티에 대한 참조가 필요하지만, 모든 속성이나 메서드에 접근할 필요가 없는 경우
- 성능 최적화가 필요한 경우, 불필요한 데이터베이스 접근을 줄일 수 있습니다.
코드 예시
package com.fastcampus.projectboard.service;
import com.fastcampus.projectboard.domain.Article;
import com.fastcampus.projectboard.domain.type.SearchType;
import com.fastcampus.projectboard.dto.ArticleDto;
import com.fastcampus.projectboard.dto.ArticleWithCommentsDto;
import com.fastcampus.projectboard.repository.ArticleRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class ArticleService {
private final ArticleRepository articleRepository;
@Transactional(readOnly = true)
public ArticleWithCommentsDto getArticle(long articleId) {
return articleRepository.findById(articleId)
.map(ArticleWithCommentsDto::from)
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다 - articleId: " + articleId));
}
public void updateArticle(ArticleDto articleDto) {
try{
Article article = articleRepository.getReferenceById(articleDto.id());
if (articleDto.title() != null) { article.setTitle(articleDto.title()); }
if (articleDto.content() != null) { article.setContent(articleDto.content()); }
article.setHashtag(articleDto.hashtag());
} catch (EntityNotFoundException e){
log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다 - dto: {}", articleDto);
}
}
}
코드 설명
위 코드에서 updateArticle
메서드는 getReferenceById
를 사용하여 Article 엔티티를 ID로 조회합니다. 그러나 getReferenceById
는 즉시 엔티티를 데이터베이스에서 로드하지 않습니다.
대신, 엔티티를 나타내는 프록시 객체를 반환합니다.
실제 엔티티 로딩은 엔티티의 속성이나 메서드에 액세스할 때 발생합니다. 이를 지연 로딩이라고 합니다.
이 경우에는 article
객체의 속성에 액세스할 때, 즉 setTitle
, setContent
, 또는 setHashtag
메서드를 호출할 때 실제 엔티티 로딩이 발생합니다. 이때 JPA provider(Hibernate)는 엔티티를 데이터베이스에서 조회하는 SELECT 문을 실행합니다.
따라서, 실제 SELECT 문이 실행되는 시점은 getReferenceById
를 호출할 때가 아니라 article
객체의 속성에 액세스할 때입니다.
이벤트의 순서는 다음과 같습니다.
getReferenceById
가 호출되어 Article 엔티티를 나타내는 프록시 객체를 반환합니다.- 프록시 객체가
article
변수에 할당됩니다. article
객체의 속성에 액세스할 때(예:setTitle
,setContent
, 또는setHashtag
), JPA 공급자는 엔티티를 데이터베이스에서 조회하는 SELECT 문을 실행합니다.- 조회된 엔티티가 해당 속성을 업데이트하는 데 사용됩니다.
getReferenceById
와 지연 로딩을 사용하면 엔티티 로딩을 실제로 필요할 때까지 지연할 수 있습니다. 이렇게 하면 성능을 개선할 수 있습니다.
TIP
으로 한 가지 더 설명하자면, updateArticle
메소드 내에서 set메소드를 통해서 article의 속성이 변경된 이후 articleRepository.save(article)
메소드를 호출하지 않은 것을 알수 있습니다.
즉, updateArticle 메소드는 class level transactional에 의해서 메소드 단위로 트랜잭션이 묶여 있습니다. 그래서 updateArticle메소드 호출이 끝나면 article이 변경 사항을 감지하게 됩니다. 그럼 변경된 것에 대해 query를 날리게 됩니다.
하지만 만약 명시적으로 save를 하고 싶거나 flush 메서드를 사용하고 싶다면 사용 할 수 있습니다.
요약하자면, findById
는 데이터베이스에서 엔티티를 직접 조회하고, 해당 엔티티가 없으면 null
을 반환합니다. 반면, getReferenceById
는 데이터베이스 조회를 지연시키고, 엔티티에 실제로 접근하거나 속성에 접근할 때 데이터베이스 조회를 수행하는 프록시 참조를 반환합니다. 사용 시점은 성능, 지연 로딩, 직접 조작의 필요성에 따라 달라집니다.
QnA
프록시
프록시는 클라이언트와 대상 객체 사이의 중개자 역할을 하는 객체입니다. 이는 대상 객체에 대한 액세스를 제어하거나, 추가 기능을 추가하거나, 실제로 필요할 때까지 대상 객체의 생성을 지연하는 디자인 패턴입니다.
JPA의 경우, 프록시는 엔티티를 나타내는 객체지만, 실제로 데이터베이스에서 엔티티를 로드하지는 않습니다. 이렇게 하면 지연 로딩이 가능해집니다.
프록시 참조
프록시 참조는 프록시 객체에 대한 참조입니다. 이 참조를 통해 실제 엔티티에 액세스할 수 있습니다. 프록시 참조는 엔티티가 실제로 로드되지 않은 상태에서 엔티티에 대한 정보를 제공할 수 있습니다.
Lazy Loading
Lazy Loading은 실제로 필요할 때까지 데이터베이스에서 엔티티를 로드하지 않는 기법입니다. 이렇게 하면 불필요한 데이터베이스 액세스를 줄일 수 있습니다.
예를 들어, 다음과 같은 코드가 있습니다.
@Entity
public class User {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Order> orders;
// getters and setters
}
User user = entityManager.getReferenceById(User.class, 1L);
System.out.println(user.getName()); // 실제로 데이터베이스에서 엔티티를 로드하지 않음
List<Order> orders = user.getOrders(); // 실제로 데이터베이스에서 엔티티를 로드
위 코드에서는 getReferenceById
메서드를 사용하여 사용자 엔티티를 로드합니다. 그러나 실제로 데이터베이스에서 엔티티를 로드하지는 않습니다. 대신, 프록시 참조를 반환합니다. 그리고 getOrders
메서드를 호출할 때 실제로 데이터베이스에서 엔티티를 로드합니다.
Eager Loading
Eager Loading은 Lazy Loading의 반대 개념입니다. 즉, 실제로 필요할 때까지 기다리지 않고, 즉시 데이터베이스에서 엔티티를 로드하는 기법입니다.
예를 들어, 다음과 같은 코드가 있습니다.
@Entity
public class User {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Order> orders;
// getters and setters
}
User user = entityManager.find(User.class, 1L);
System.out.println(user.getName()); // 실제로 데이터베이스에서 엔티티를 로드
List<Order> orders = user.getOrders(); // 이미 로드된 엔티티를 반환
위 코드에서는 find
메서드를 사용하여 사용자 엔티티를 로드합니다. 그리고 fetch = FetchType.EAGER
를 사용하여 즉시 데이터베이스에서 엔티티를 로드합니다.
getReferenceById와 findById의 Use Case
getReferenceById
와 findById
는 다음과 같은 Use Case에서 사용됩니다.
getReferenceById
: 엔티티에 대한 참조가 필요하지만, 실제로 엔티티를 로드하지는 않아도 되는 경우에 사용됩니다. 예를 들어, 엔티티의 존재 여부를 확인하거나, 엔티티의 일부 속성에만 액세스하는 경우에 사용됩니다.findById
: 엔티티를 실제로 로드해야 하는 경우에 사용됩니다. 예를 들어, 엔티티의 모든 속성에 액세스하거나, 엔티티를 조작하는 경우에 사용됩니다.
예를 들어, 다음과 같은 코드가 있습니다.
// getReferenceById 사용
User user = entityManager.getReferenceById(User.class, 1L);
if (user!= null) {
System.out.println("User exists");
} else {
System.out.println("User does not exist");
}
// findById 사용
User user = entityManager.find(User.class, 1L);
if (user!= null) {
System.out.println("User name: " + user.getName());
user.setName("New Name");
entityManager.persist(user);
}
위 코드에서는 getReferenceById
를 사용하여 사용자 엔티티의 존재 여부를 확인합니다. 그리고 findById
를 사용하여 사용자 엔티티를 실제로 로드하고, 엔티티의 속성을 조작합니다.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
콘서트 예매 서비스에서 발생할 수 있는 동시성 이슈와 처리 (0) | 2024.07.20 |
---|---|
JPA 테스트 코드 작성시 UPDATE Query 생성이 안되네? (0) | 2024.05.28 |
JPA 연결 테스트 코드 (0) | 2024.05.28 |
[그냥 보는] application.yaml (0) | 2024.05.28 |
로깅 출력 어느 것으로?(feat.Interpolation vs Concatenation) (0) | 2024.04.22 |