@RestController
@RequiredArgsConstructor
@RequestMapping("/post")
public class PostController {
private final PostService postService;
@PatchMapping("/{postId}")
public Response<Long> updatePost(@PathVariable(name = "postId") Long postId,
@RequestBody UpdatePostRequestDto dto) {
Post post = postService.updatePost(postId, dto);
return Response.ok(post.getPostId());
}
}
//service
public Post getPost(Long id) {
return postRepository.findById(id);
}
public Post updatePost(Long id, UpdatePostRequestDto dto){
Post post = getPost(id);
User user = userService.getUser(dto.userId());
post.updatePost(user, dto.content(), dto.state());
return postRepository.save(post);
}
@Repository
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepository {
private final JpaPostRepository jpaPostRepository;
@Override
@Transactional
public Post save(Post post) {
PostEntity postEntity = new PostEntity(post);
postEntity = jpaPostRepository.save(postEntity);
return postEntity.toPost();
}
@Override
public Post findById(Long id) {
PostEntity postEntity = jpaPostRepository.findById(id).orElseThrow();
return postEntity.toPost();
}
}
이렇게 구성된 Post update 과정을 호출하면 어떤 쿼리가 호출될까?
예상대로라면
로 예상될 것이다.
하지만 그렇지않다!!! 실제로는?
Hibernate:
select
pe1_0.id,
pe1_0.author_id,
a1_0.id,
a1_0.follower_count,
a1_0.following_count,
a1_0.name,
a1_0.profile_image,
a1_0.reg_dt,
a1_0.upd_dt,
pe1_0.content,
pe1_0.like_count,
pe1_0.reg_dt,
pe1_0.state,
pe1_0.upd_dt
from
community_post pe1_0
left join
community_feed a1_0
on a1_0.id=pe1_0.author_id
where
pe1_0.id=?
Hibernate:
select
ue1_0.id,
ue1_0.follower_count,
ue1_0.following_count,
ue1_0.name,
ue1_0.profile_image,
ue1_0.reg_dt,
ue1_0.upd_dt
from
community_feed ue1_0
where
ue1_0.id=?
Hibernate:
select
pe1_0.id,
pe1_0.author_id,
a1_0.id,
a1_0.follower_count,
a1_0.following_count,
a1_0.name,
a1_0.profile_image,
a1_0.reg_dt,
a1_0.upd_dt,
pe1_0.content,
pe1_0.like_count,
pe1_0.reg_dt,
pe1_0.state,
pe1_0.upd_dt
from
community_post pe1_0
left join
community_feed a1_0
on a1_0.id=pe1_0.author_id
where
pe1_0.id=?
Hibernate:
update
community_post
set
author_id=?,
content=?,
like_count=?,
state=?,
upd_dt=?
where
id=?
이런 쿼리가 발생한다
보면 post를 2번 조회하는 모습을 볼 수 있다.
이유를 알려면 jpa 에서 사용하는 save() 의 동작 방식을 알아야한다.
현재 내가 짠 코드에서는 도메인 Post와 PostEntity를 분리하여 사용하고 있다.
PostEntity가 DB와 연결이되는 클래스이다.
findById 메서드를 살펴보면 jpa 레포지토리를 통해서 PostEntity 정보를 가져오고 있다.
하지만 우리는 비즈니스 로직 내부에서 Post 객체를 사용하고 있기에
findById 를 통해서 조회해온 영속성 컨텍스트에 담긴 PostEntity 객체를 사용하지 않는다.
그 후 Post 객체를 update 하고 (도메인 로직) 다시 save 하는 과정을 거치고 있다.
save 메서드 내부에서는 매개변수로 넣은 Post 객체로 PostEntity 객체를 새로 만들고
해당 PostEntity 객체를 새로 저장한다.
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (this.entityInformation.isNew(entity)) {
this.entityManager.persist(entity);
return entity;
} else {
return this.entityManager.merge(entity);
}
}
save 메서드의 동작 방식을 보면 해당 엔티티의 id가 null 이거나 해서 isNew 에 걸리는
새로운 엔티티인 경우, 따로 조회과정 없이 바로 persist 하여 insert 문을 수행한다.
하지만 우리가 save 메서드를 통해 넣는 PostEntity는 새로운 엔티티가 아니기 때문에
else 문의 merge 가 실행된다.
merge 는 persist 와 다르게 DB 에 해당 엔티티가 존재하는지 조회하고 찾아내어
변경해주는 과정을 거친다.
하지만 만약 영속성 컨텍스트에 해당 객체가 존재한다면 DB 조회를 하지 않지만
우리는 findById를 통해 영속성 컨텍스트에 저장된 PostEntity 객체를 사용하지않고
Post 변환 -> 그 Post로 다시 PostEntity를 생성했기에 이것은 비영속 객체
따라서 한번 더 post 를 조회하는 쿼리문이 생기는 것이었다.
다시 한번 정리하자면
findById
로 조회한 PostEntity
는 영속성 컨텍스트에 저장된다.Post
도메인 객체로 변환하여 비즈니스 로직에서 사용한다.Post
객체를 업데이트한 후, 이를 다시 PostEntity
로 변환하여 save
메서드에 전달한다.save
메서드는 merge
를 실행한다. merge
는 비영속 상태 객체를 영속화하는 과정에서 DB를 조회한다. PostEntity
)가 영속성 컨텍스트에 관리되지 않는 비영속 상태이기 때문이다.Post
객체를 다시 PostEntity
로 변환하는 설계로 인해 동일한 데이터를 다시 조회하는 불필요한 쿼리가 발생한다.findById
의 결과를 사용하지 않음
findById
로 조회한 PostEntity
객체는 영속성 컨텍스트에 등록되지만, 이를 사용하지 않고, Post
로 변환하여 비영속 상태로 만든다.
save
에서의 merge
동작
PostEntity
는 영속성 컨텍스트와 무관한 비영속 객체이다. merge
를 통해 해당 객체를 영속화하려고 하며, 이 과정에서 동일한 엔티티를 DB에서 조회하여 영속성 컨텍스트에 병합한다.영속성 컨텍스트와 분리된 설계
Post
와 PostEntity
를 별도로 사용하는 구조가 문제의 근본 원인이다. public interface JpaPostRepository extends JpaRepository<PostEntity, Long> {
@Modifying
@Query(value = "UPDATE PostEntity p "
+ "SET p.content = :#{#postEntity.getContent()}, "
+ "p.state = :#{#postEntity.getState()},"
+ "p.updDt = now()"
+ "WHERE p.id = :#{#postEntity.id}")
void updatePostEntity(PostEntity postEntity);
}
@Repository
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepository {
private final JpaPostRepository jpaPostRepository;
@Override
@Transactional
public Post save(Post post) {
PostEntity postEntity = new PostEntity(post);
if (post.getPostId() != null) {
jpaPostRepository.updatePostEntity(postEntity);
return postEntity.toPost();
}
postEntity = jpaPostRepository.save(postEntity);
return postEntity.toPost();
}
@Override
public Post findById(Long id) {
PostEntity postEntity = jpaPostRepository.findById(id).orElseThrow();
return postEntity.toPost();
}
\
Impl 에서 사용하는 jpa 레포지토리에 update 쿼리문을 jpql 로 구현
save 메서드를 진행할 때 postId 가 null 이 아니라면 즉 새로운 PostEntity 가 아니라면
바로 update 쿼리문을 수행하도록 하여 중복 조회를 하지 않도록 처리했다.
그냥 merge 만 이해하려했다면 금방 했겠지만
Post, PostEntity 를 변환하는 과정이 섞여있어 이해하는데 꽤나 오랜 시간을 소비했다....
이것도 성장이겠지..!!!!!