[JPA] existById vs. getById vs. findById

나르·2022년 1월 19일
3

Spring

목록 보기
11/25
post-thumbnail

🤔 Intro.

프로젝트에서 이메일 중복체크 등 어떠한 데이터의 존재 여부를 확인할 때 findById를 자주 사용하고는 합니다.

하지만 Jpa에는 existById, findById, getById 등 다양한 메소드가 있습니다.
각 메서드의 차이는 무엇이고, 어떠한 경우에 어떤 것을 써야할까요???


ExistBy

회원가입 시 이메일 중복체크의 경우, 로직은 다음과 같습니다.

    @Transactional
    public void signUp(Users user) {
        final boolean isExistEmail = userRepository.existsByEmail(user.getEmail());
        if (isExistEmail) {
            throw new BadRequestException(ExceptionType.DUPLICATE_EMAIL);
        }
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        userRepository.save(user);
    }

단순히 존재 여부만 확인하면 되므로, existBy를 쓰는 것이 메서드 명으로 유추했을 때도, 리턴타입(boolean)을 봐도 맞다고 판단됩니다.
하지만 Jpa의 exist는 exist 쿼리를 날리는 것이 아니라 count 쿼리를 날리기 때문에, 사실상 select를 하는 findBy와 차이가 없는 것 아닐까? 라는 생각이 들었습니다.

// findById
Hibernate: 
    select
        post0_.id as id1_2_0_,
        post0_.created_at as created_2_2_0_,
        post0_.deleted_at as deleted_3_2_0_,
        post0_.updated_at as updated_4_2_0_,
        post0_.content as content5_2_0_,
        post0_.hashtag as hashtag6_2_0_,
        post0_.image as image7_2_0_,
        post0_.user_id as user_id8_2_0_ 
    from
        posts post0_ 
    where
        post0_.id=? 
        and (
            post0_.deleted_at is null
        )
        
// existById
Hibernate: 
    /* select
        count(*) 
    from
        Post x 
    WHERE
        x.id = :id */
        select
            count(*) as col_0_0_ 
        from
            posts post0_ 
        where
            (
                post0_.deleted_at is null
            ) 
            and post0_.id=?     

해당 부분은 이 글에서 해답을 찾을 수 있었습니다.
마지막 요약 부분만 가져오자면 다음과 같습니다.

  • count는 exists에 비해 성능상 이슈가 있다.
    • 그러나 @Query와 Querydsl에서는 select exists를 사용할 수가 없다.
  • 그래서 select exists를 limit 1 로 대체해서 사용한다.
  • 단, JpaRepository의 메소드 쿼리에선 내부적으로 limit 1를 사용하고 있어서 성능상 이슈가 없다.

FindById

다음으로 게시글의 권한을 확인하는 경우, 로직은 다음과 같습니다.
글이 존재하는지를 먼저 확인하고, 글의 작성자와 수정/삭제의 요청자가 같은 유저인지를 판단합니다.

그런데 존재 여부를 확인할 때, existBy를 사용하면 기능적으로는 맞지만, 이후 글 작성자의 정보를 가져오려면 entity가 필요하므로 결국 findById를 한번 더 날려야합니다.

❓ 그렇다면 똑같이 entity를 반환하는 getById는 어떨까요?

// PostService.java

    private Post checkAuthorization(Long postId) {
        logger.info("게시글 존재 여부");
        Post post = postRepository.findById(postId)
        	.orElseThrow(() -> new BadRequestException(ExceptionType.NOT_EXIST);
        // Post post = postRepository.getById(postId)
        
        logger.info("작성자 일치 여부");
        if (!post.getUserId().equals(SecurityUtil.getCurrentMemberId())) {
            throw new BadRequestException(ExceptionType.NOT_AUTHOR);
        }
        
        logger.info("메서드 종료");
        return post;
    }

두 케이스를 모두 테스트 해봤습니다.

// findById
2022-02-04 14:19:13.007  INFO 54630 --- 게시글 존재 여부
Hibernate: 
    select
        post0_.id as id1_2_0_,
        post0_.created_at as created_2_2_0_,
        post0_.deleted_at as deleted_3_2_0_,
        post0_.updated_at as updated_4_2_0_,
        post0_.content as content5_2_0_,
        post0_.hashtag as hashtag6_2_0_,
        post0_.image as image7_2_0_,
        post0_.user_id as user_id8_2_0_ 
    from
        posts post0_ 
    where
        post0_.id=? 
        and (
            post0_.deleted_at is null
        )
2022-02-04 14:19:13.031  INFO 54630 --- 작성자 일치 여부
2022-02-04 14:19:13.031  INFO 54630 --- 메서드 종료

findBy는 메서드가 실행되는 시점에서 실제 DB를 조회하여 객체를 가져오는 것을 확인할 수 있습니다.

// getById
2022-02-04 14:16:32.165  INFO 54343 --- 게시글 존재 여부
2022-02-04 14:16:32.177  INFO 54343 --- 작성자 일치 여부
Hibernate: 
    select
        post0_.id as id1_2_0_,
        post0_.created_at as created_2_2_0_,
        post0_.deleted_at as deleted_3_2_0_,
        post0_.updated_at as updated_4_2_0_,
        post0_.content as content5_2_0_,
        post0_.hashtag as hashtag6_2_0_,
        post0_.image as image7_2_0_,
        post0_.user_id as user_id8_2_0_ 
    from
        posts post0_ 
    where
        post0_.id=? 
        and (
            post0_.deleted_at is null
        )
2022-02-04 14:16:32.195  INFO 54343 --- 메서드 종료

getBy는 getter를 호출해 사용하는 시점에서 DB에 접근하는 것을 확인할 수 있습니다.

이처럼 getBy는 내부적으로 EntityManager.getReference() 메소드를 호출하기 때문에 엔티티를 직접 반환하는게 아니라 레퍼런스만 반환합니다.

때문에 EntityNotFoundException 또한 프록시에서 DB에 접근하려고 할 때 데이터가 없으면 발생하므로, 존재 여부를 확인해야하는 로직에 쓰기는 적합하지 않습니다.


GetBy

❓ 그렇다면 getBy는 언제 사용하는게 좋을까요?

프록시 객체는 식별자 값만을 가지고있는 가짜 객체입니다.
아래와 같이 유저의 Id값을 제외한 데이터는 필요 없는 경우라면, 실제 테이블을 조회하지 않고도 객체를 생성할 수 있는 getById를 사용하는게 유리합니다.

// User.java
@Table(name = "users")
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;
}

// Post.java
@Table(name = "posts")
@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

// PostService.java
@RequiredArgsConstructor
@Service
public class PostService {
    private final PostRepository postRepository;

    @Transactional
    public void createPost(Long userId) {
        User user = userRepository.getById(userId);
        Post post = new Post();
        post.setUser(user);
        postRepository.save(post);
    }
}

❗️ LazyInitializationException: could not initialize proxy – no Session

위 에러는 지연 조회 시점까지 세션이 유지되지 않았기 때문에 발생하는 에러입니다.
getById 로 가져온 프록시 객체를 실제 엔티티로 변환해 사용한다면 @Transactional 어노테이션을 올려줘야 합니다!

🚩 Conclusion

  • 존재 여부를 확인할 때는 existBy
  • Id 값만 사용할 엔티티가 필요한 경우에는 getBy
  • Id 이외의 데이터도 사용하는 엔티티의 경우에는 findBy
    를 사용하면 되겠습니다!

Ref.

Difference between getOne and findById in Spring Data JPA?
[JPA Lazy Evaluation] LazyInitializationException: could not initialize proxy – no Session
JPA / 프록시 알아보기
proxy

profile
💻 + ☕ = </>

2개의 댓글

comment-user-thumbnail
2023년 5월 26일

잘 읽었습니다!

답글 달기
comment-user-thumbnail
2023년 5월 31일

깔끔한 정리, 잘 읽었습니다 !

답글 달기