프레임워크/자바 스프링

findById vs getReferenceById 차이

hyeseong-dev 2024. 4. 22. 14:41

JPA findById vs getReferenceById

JPA(Java Persistence API)를 사용하여 데이터베이스에서 엔티티를 조회할 때 일반적으로 사용되는 두 가지 메서드가 있습니다. findByIdgetReferenceById는 일부 유사점이 있지만, 중요한 차이점이 있습니다.

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 객체의 속성에 액세스할 때입니다.

 

이벤트의 순서는 다음과 같습니다.

  1. getReferenceById가 호출되어 Article 엔티티를 나타내는 프록시 객체를 반환합니다.
  2. 프록시 객체가 article 변수에 할당됩니다.
  3. article 객체의 속성에 액세스할 때(예: setTitle, setContent, 또는 setHashtag), JPA 공급자는 엔티티를 데이터베이스에서 조회하는 SELECT 문을 실행합니다.
  4. 조회된 엔티티가 해당 속성을 업데이트하는 데 사용됩니다.

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

getReferenceByIdfindById는 다음과 같은 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를 사용하여 사용자 엔티티를 실제로 로드하고, 엔티티의 속성을 조작합니다.