"분명히 save()메서드를 호출했는데, 왜 select 쿼리문이 나가지?"
"save()랑 select 쿼리문은 어떤 관계가 있는걸까?"
예시 코드
유저의 팔로우 기능을 구현하는 간단한 예제 코드로 알아보겠습니다.
@Override
public void save(User user, User targetUser) {
UserRelationshipEntity entity = new UserRelationshipEntity(user.getId(), targetUser.getId());
jpaUserRelationRepository.save(entity);
//jpaUserRepository.saveAll(List.of(new UserEntity(user), new UserEntity(targetUser)));
}
이 메서드는 팔로워유저와 팔로잉 유저를 파라미터 값으로 전달받습니다. 쉽게 말해서 A유저가 B유저를 팔로우한다고 가정하면
[user : A], [targetUser : B]가 됩니다.
UserRelationshipEntity entity = new UserRelationshipEntity(user.getId(), targetUser.getId());
UserRelatoinshipEntity는 팔로워유저의 id와 팔로잉유저의 id로 만든 복합키를 사용하고 있습니다.
생성자에 유저 id를 넣어주어 엔티티 객체를 생성합니다.
jpaUserRelationRepository.save(entity);
생성해준 entity를 JpaRepository를 통해서 save() 해줍니다.
쿼리 분석
포스트맨을 통해서 API를 호출하게 되면, JPA에서 아래와 같은 쿼리를 flush()해줍니다.
Hibernate:
select
ure1_0.follower_user_id,
ure1_0.following_user_id,
ure1_0.mod_date,
ure1_0.reg_date
from
community_user_relation ure1_0
where
(
ure1_0.follower_user_id, ure1_0.following_user_id
) in ((?, ?))
Hibernate:
insert
into
community_user_relation
(mod_date, reg_date, follower_user_id, following_user_id)
values
(?, ?, ?, ?)
쿼리문을 살펴보니, 분명 save()만 호출해주었는데, select쿼리가 조회된 것을 확인할 수 있습니다.
"왜 이런 현상이 발생하는 걸까요?"
JpaRepository save()메서드를 분석해보겠습니다.
JpaRepository save()메서드 분석
save()메서드는 아래 코드와 같이 구현됩니다.
조건문을 살펴보면, entityInformation.isNew(entity)를 호출해서 true인경우엔 persist를 false인 경우엔 merge를 수행합니다.
entityInformation.isNew메서드는 무엇일까요?
한번 더 파고들겠습니다. 아래는 isNew()의 코드입니다.
코드에서 먼저 entity의 id를 추출합니다.
첫번째 조건문은 추출한 아이디가 원시타입이 아닌 경우 id가 null인 여부에따라 true나 false를 return합니다.
두번째 조건문은 id가 래퍼 클래스일 경우를 대비하여 id가 0L인 여부에따라 true나 false를 return합니다.
이 코드에서 한가지 알 수 있는 점은 엔티티가 새로운 엔티티인지 판단하는 여부는 엔티티의 id값을 기준으로 판단하는 것입니다.
그렇다면, 예시 코드는 어떤 엔티티일까요? (새로운 엔티티 or 그렇지 않은 엔티티)
예시 코드와 isNew(T entity)와의 관계
예시 코드는 새로운 엔티티가 아닙니다.
UserRelationshipEntity entity = new UserRelationshipEntity(user.getId(), targetUser.getId());
entity객체를 생성할때, 생성자에 넘겨준 값은 복합키로 사용되는 키값입니다.
즉, 엔티티 생성시 키값을 넣어주었습니다. 이는 곧 isNew()메서드에서 false를 return함을 의미합니다.
새로운 엔티티가 아니니, entityManager.merge(entity) 메서드를 호출하게 됩니다.
merge(entity)메서드는 어떤 역할을 해줄까요?
entityManger.merge(entity)의 역할
공식문서는 다음과 같이 이야기 합니다.
영속성 컨텍스트 안에 주어진 엔티티의 상태를 병합하고, 이 과정에서 새로 영속화된 Managed 상태의 엔티티 인스턴스를 반환
그럼, 도대체 merge()라는 메서드와 select 쿼리는 어떤 관계가 있는걸까요?
merge()의 주요 동작 흐름
merg()메서드는 다음과 같은 흐름을 가집니다.
- 영속성 컨텍스트를 확인해 해당 엔티티가 관리되는지 확인합니다.
- 만약, 관리중이지 않은 경우엔 DB에서 해당 엔티티를 SELECT로 조회해 영속 상태로 변환합니다.
- 조회한 데이터를 영속성 컨텍스트(1차 캐시)에 저장합니다. 조회가 되지 않는 경우 INSERT 쿼리를 실행해 새 엔티티를 삽입합니다.
select쿼리를 날리게 된 이유는 흐름 중 2번 문장으로 인해 발생하던 것이였습니다.
예시 코드에서 제가 생성한 엔티티는 아직 DB에 flush()하여 저장하지 않은 엔티티로 영속성 컨텍스트에는 저장이 되어 있지 않은 상태입니다. 즉, 관리중이지 않기 때문에 DB에서 해당 엔티티를 SELECT 쿼리로 조회하게 됩니다. DB를 조회해도 해당 엔티티는 존재하지 않기 때문에 INSERT쿼리가 최종적으로 실행된 것입니다.
"select쿼리 한번 나가는 걸로 왜이리 호들갑이지?"라고 생각하실 수 있습니다.
사실 규모가 작은 프로젝트에서는 상관이 없지만, 규모가 커질 수록 남용되는 하나의 쿼리가 데이터베이스 자원 낭비와 서비스 지연을 발생시킬 수 있습니다. 만약 100만명의 유저가 팔로우를 하게 되면, select문이 100만번이 추가로 실행되게 됩니다.
해결 방법
JpaRepository에서 자동으로 만들어주는 save()메서드를 사용하지 않고, JPQL이나 Native Query를 활용해 직접 쿼리를 작성함으로써 불필요한 SELECT 쿼리를 방지할 수 있습니다.
@Modifying
@Query(value = "INSERT INTO community_user_relation (follower_user_id, following_user_id, mod_date, reg_date) " +
"VALUES (:followerId, :followingId, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", nativeQuery = true)
void saveRelation(@Param("followerId") Long followerId, @Param("followingId") Long followingId);
정리
SELECT 쿼리가 발생한 이유는 엔티티 생성 시 ID 값을 함께 지정했기 때문입니다. JpaRepository가 제공하는 편리함은 박수 받을 만하지만, 때로는 SELECT와 같은 불필요한 쿼리가 추가로 실행될 수 있다는 점을 염두에 두어야 합니다. 따라서, 상황에 따라 JPA의 동작 방식을 잘 이해하고 신중히 사용하는 것이 중요하다고 생각합니다.
'JPA' 카테고리의 다른 글
동시성 문제 해결방법(Update Query편) (0) | 2025.01.29 |
---|---|
JPA와 DB는 어떻게 동기화가 되는걸까? (0) | 2025.01.26 |
🙆🏻♂️ 너, Fetch Join 만능이야? (0) | 2024.09.24 |
🙆🏻♂️ 너, Fetch Join 좀 알고 싶다? (0) | 2024.09.22 |
@DataJapTest를 사용하는 이유 (0) | 2024.07.17 |