[Spring/스프링] JPA에서 엔티티를 수정할 때 - 쿼리와 변경 감지

kindof

·

2022. 1. 13. 00:31

0. 들어가면서

얼마 전, 새로운 프로젝트를 시작하게 되면서 처음 엔티티 설계부터 컨트롤러, 서비스 등을 어떻게 하면 좋을 지 고민하고 있습니다.

 

그런데 문득 엔티티의 수정에 대한 컨트롤러를 작성하면서 무의식적으로 업데이트 DTO를 만들고 있는 제 모습을 보게 됐습니다.

 

사실 DTO를 통해 엔티티를 수정하는 것은 아래에서 소개할 '변경 감지'를 통한 엔티티의 수정 방법인데요. 과연 이 방법이 베스트인가에 대해 다른 블로그들의 글도 보고 JPA 책도 보면서 나름대로 이해한 내용을 글로 옮겨보겠습니다.

 

 

 

1. Update 쿼리를 이용한 엔티티 수정

업데이트 쿼리를 이용해서 엔티티를 수정하는 방법은 간단해보입니다.

 

우선 Member 엔티티와 JPA 레포지토리를 생성합니다.

@Entity
@Getter
@Setter
public class Member {

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

    private String username;

    private String nickname;
    
    ...
    
 }
public interface MemberRepository extends JpaRepository<Member, Long> {

    @Modifying
    @Query("UPDATE Member m SET m.nickname = :nickname where m.id = :id")
    int updateUserNickname(@Param(value="nickname") String nickname, @Param(value="id") Long id);
    
    ...
    
}

* 참고로 엔티티에서 @Setter는 테스트 코드 작성의 편의를 위해 작성했습니다.

 

어쨌든 위와 같이 회원(Member)의 닉네임을 바꾸는 쿼리를 JPQL을 이용해 작성했습니다.

 

그리고 아래와 같이 테스트 코드를 작성하고 실행시키면 어떤 결과가 나올까요?

updateMember 테스트코드


테스트코드 - 실패

결과를 보면, 테스트코드는 실패하는 것을 볼 수 있는데요.

 

그런데 아이러니하게 아래 사진과 같이 update 쿼리는 정상적으로 나가는 것을 볼 수 있습니다. 테스트코드가 실패한 이유는 무엇일까요?

update 쿼리는 정상적으로 나간다

 

정답은 영속성 컨텍스트에 있습니다.

 

update 쿼리는 영속성 컨텍스트에 저장된 캐시값을 변경하지 않은 채로 DB에 직접 쿼리를 날리게 되는데요.

 

그래서 실제 DB에 저장된 회원의 정보와 현재 영속성 컨텍스트에 남아있는 회원의 정보가 동기화되지 못합니다.

 

이 때, 테스트코드처럼 memberRepository.findById()를 통해 회원을 조회해오면 이전에 save()를 통해 저장했던 회원의 정보가 캐시로 남아있기 때문에 실제 DB의 값과는 달리 변경되지 않은 회원의 정보가 조회되는 것이죠.

 


이 문제를 해결하기 위해서는 아래와 같이 @Modifying(clearAutomatically = true) 옵션을 넣어줄 수 있습니다.

Modifying(clearAutomatically = true)

위처럼 clearAutomatically = true 옵션을 주게 되면 업데이트 쿼리를 날린 후에 영속성 컨텍스트를 자동으로 clear() 해주는데요.

 

따라서, 다시 테스트코드를 실행하게 되면 DB에서 회원 정보를 읽어온 후 영속성 컨텍스트에 저장하여 테스트가 성공하는 것을 볼 수 있습니다.

테스트코드 - 성공

 

하지만 위 쿼리를 보시면 알 수 있겠지만 회원의 닉네임을 수정하기 위해서는 하나의 쿼리문이 필요하고, 이에 따라 엔티티의 각 속성을 변경할 때마다 새로운 쿼리문을 작성해야 한다는 것을 알 수 있습니다.

 

그래서 여러 개의 필드를 수정하는 한 방 쿼리를 만들어서 필요한 부분만 변경되도록 파라미터로 값을 전달할 수 있는데요. 하지만 이 때에도 실수로 변경되지 않을 값을 빼먹게되면 쿼리가 제대로 동작하지 않을 수 있는 문제가 생길 수 있습니다.

 

그럼 이제 JPA에서는 어떻게 엔티티를 업데이트하면 좋을까에 대해 '변경 감지'를 통한 방법을 살펴보겠습니다.

 

 

2. 변경 감지를 통한 엔티티 수정

위에서 회원의 닉네임을 변경하는 과정을 JPA 변경 감지를 통해 시도해보겠습니다.

변경 감지를 통한 엔티티 수정

 

먼저 두 개의 EntityManager와 각각의 트랜잭션을 따로 만들었습니다. 그리고 "Ronaldo"라는 이름의 회원을 만들어 DB에 저장하고 트랜잭션을 커밋했습니다.

 

이후에 엔티티의 이름을 "Messi"로 업데이트했는데요. 보시다시피, 두번째 단락의 코드에서는 tr2.commit()만 있을 뿐, em2.update(m)같은 코드가 존재하지 않습니다.

 

이처럼 JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 됩니다.

그리고 이것을 가능하게 하는 것을 바로 '변경 감지'라고 하는데요.

JPA 변경감지

 

JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장하는데 이것을 스냅샷이라고 합니다. 그리고 flush 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾죠.

 

그래서 위 테스트코드에서는 회원 엔티티와 이전의 스냅샷을 비교하여 Update SQL을 생성하고, 이를 commit하는 것입니다.

 

뿐만 아니라, JPA는 기본적으로 업데이트 시점에서 엔티티의 모든 필드를 업데이트하기 때문에 모든 필드를 사용하면 수정 쿼리가 항상 같게 되는 장점이 있습니다.

 

 

3. 정리

이번 포스팅에서는 JPA에서 엔티티를 업데이트를 할 때 JPQL 쿼리와 변경 감지를 통한 방법을 각각 공부해봤습니다.

 

결국 맨 처음에 했던 고민처럼, 업데이트 Dto를 하나 생성해서 서비스 로직에 던져주고 변경 감지를 통해 업데이트하는 방식이 좋을 것 같다는 생각을 하게 됐습니다.

 

감사합니다.