ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA로 Update를 해보자!
    프로젝트/게시판 프로젝트 2022. 5. 14. 00:01

    회원가입 로직을 작성하던 중 회원이 정보를 수정할 수 있기 때문에 JPA를 통해 Update를 해보고자 합니다.

     

    하지만 Spirng Data JPA에서는  update 메서드가 없습니다.

     

    그러면 JPA에서는 어떻게 Update를 할 수 있을까요?

     

    이를 하기 위해서는 JPA의 Dirty Checking이란 것을 먼저 알아야 합니다.

     

    Dirty Checking이란?

    여기서 Dirty란 상태의 변화가 생긴 정도로 이해하면 좋습니다.

    즉, 상태 변화를 확인하는 것이 바로 Dirty Checking입니다.

     

    JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해 줍니다.

     

    변화가 있다의 기준은 최초 조회 상태입니다.

     

    JPA는 엔티티를 조회하게 되면 해당 엔티티의 조회 상태를 그대로 스냅샷으로 만들어놓습니다.

    그리고 트랜잭션이 끝나는 시점에 이 스냅샷과 비교하여 다른 점이 있다면 Update Query를 DB로 전달합니다.

     

     

    그래서 Update를 위해서 Entity에 필드 값을 변경할 수 있는 Setter와 유사한 메서드를 제공합니다.

     

    코드 예시(출처)

    package com.jojoldu.blogcode.jpatheory.domain;
    
    import lombok.AccessLevel;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import org.hibernate.annotations.DynamicUpdate;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    import javax.persistence.OneToMany;
    import java.util.ArrayList;
    import java.util.List;
    
    @Getter
    @NoArgsConstructor
    @Entity
    @DynamicUpdate // 변경한 필드만 대응
    public class Pay {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String tradeNo;
        private long amount;
    
        @OneToMany(mappedBy = "pay", cascade = CascadeType.ALL)
        private List<PayDetail> details = new ArrayList<>();
    
        public Pay(String tradeNo, long amount) {
            this.tradeNo = tradeNo;
            this.amount = amount;
        }
    
        public void addDetail(PayDetail detail) {
            this.details.add(detail);
            detail.setPay(this);
        }
    
        public void changeTradeNo(String tradeNo) {
            this.tradeNo = tradeNo;
        }
    }

    위의 Pay Entity를 보게 되면 Setter는 존재하지 않지만 changeTradeNo라는 메서드가 존재합니다.

     

    즉, Update를 위해서는 다음과 같은 과정이 일어나게 됩니다.

    1. id를 기준으로 해당 id값을 가진 Pay Entity 조회하기(스냅샷이 만들어짐)

     

    2. changeTradeNo메서드를 호출하여 tradeNo를 변경

     

    3. 해당 Entity를 다시 저장하기(스냅샷과 비교하여 이전과 다른 점이 있기 때문에 Update Query가 나감)

     

    결과 예시

    https://jojoldu.tistory.com/415

     

    만약 Entity에 @DynamicUpdate 어노테이션이 존재하지 않는다면 다음과 같은 쿼리가 나가게 됩니다.

     

    우리는 tradeNo만 수정했지만 amount도 같이 업데이트되는 것이죠

     

    하지만 Entity의 필드가 20~30개 이상인 경우에는 전체 필드 Update 쿼리가 부담스러울 수 있습니다.

     

    따라서 이런 경우에 @DynamicUpdate 어노테이션을 사용하여 변경된 필드만 반영되도록 할 수 있습니다.

     

     

    @DynamicUpdate 어노테이션 적용 결과

    https://jojoldu.tistory.com/415

     

     

     

    내 프로젝트에 반영해보기

     

    기존 테스트 코드

      @DisplayName("UPDATE 테스트")
        @Test
        public void updateSuccessTest() {
            //given
            Member member = Member.builder().userId("Test").name("Test").nickName("Test").password("Test").phoneNumber("Test").address(new Address("a1","a2","a3")).build();
            Member fail = Member.builder().userId("fail").name("fail").nickName("fail").password("fail").phoneNumber("fail").address(new Address("a1","a2","a3")).build();
            memberRepository.save(member);
    
            //when
            Member updateMember = Member.builder().id(member.getId()).userId("updated").build();
            memberRepository.save(updateMember);
    
            //then
            Optional<Member> result = memberRepository.findById(member.getId());
            Assertions.assertThat(result.orElse(fail).getUserId()).isEqualTo("updated");
        }

    member 객체를 하나 생성하여 저장합니다.

    이후에 member와 동일한 ID를 가지는 updateMember 객체를 생성한 후 저장합니다.

    이후에 findByID를 통해서 해당 ID를 가지는 Entity를 조회하여 result 객체에 담고 그 값이 "updated"인지 검증합니다.

     

    여기서는 Setter 메서드가 없어서 동일한 ID를 가지는 새로운 updateMember 객체를 만들어서 Dirty Checking을 활용했습니다.

     

    Entity에 nickName을 변경하는 메서드를 만들고 그것을 테스트해보겠습니다.

     

    바뀐 테스트 코드

    우선 Member Entity에는 다음과 같은 코드를 추가했습니다.

    public void changeNickName(String nickName){
            this.nickName = nickName;
        }

     

    테스트 코드

     @DisplayName("UPDATE 테스트")
        @Test
        public void updateSuccessTest() {
            //given
            Member member = Member.builder().userId("Test").name("Test").nickName("Test").password("Test").phoneNumber("Test").address(new Address("a1","a2","a3")).build();
            Member fail = Member.builder().userId("fail").name("fail").nickName("fail").password("fail").phoneNumber("fail").address(new Address("a1","a2","a3")).build();
            memberRepository.save(member);
    
            //when
            member.changeNickName("updated");
            memberRepository.save(member);
    
            //then
            Optional<Member> result = memberRepository.findById(member.getId());
            Assertions.assertThat(result.orElse(fail).getNickName()).isEqualTo("updated");
        }

    정상적으로 테스트가 수행됩니다!

    하지만 쿼리를 찍어보면 insert 쿼리만 나갑니다.

    Insert Query

    왜 그럴까요?

    https://okky.kr/article/308772

    다음은 조금 더 명확한 답변인 것 같습니다.

    https://code-examples.net/ko/q/1438ba3

    위에서는 save를 2번 수행하기 때문에 한번의 insert Query만 나가게 된 것 같습니다.

     

    따라서 이를 saveAndFlush로 수정하였습니다.

    @DisplayName("UPDATE 테스트")
        @Test
        public void updateSuccessTest() {
            //given
            Member member = Member.builder().userId("Test").name("Test").nickName("Test").password("Test").phoneNumber("Test").address(new Address("a1","a2","a3")).build();
            Member fail = Member.builder().userId("fail").name("fail").nickName("fail").password("fail").phoneNumber("fail").address(new Address("a1","a2","a3")).build();
            memberRepository.saveAndFlush(member);
    
            //when
            member.changeNickName("updated");
            memberRepository.saveAndFlush(member);
    
            //then
            Optional<Member> result = memberRepository.findById(member.getId());
            Assertions.assertThat(result.orElse(fail).getNickName()).isEqualTo("updated");
        }

     

    이제 정상적으로 Update 쿼리가 나갑니다.

    Update Query

     

    여기서 근데 문제가 있습니다.

    Dirty Checking을 이용하려고 했으나 saveAndFlush를 2번 호출했습니다.

    사실 여기서 save할 필요가 없다고 생각합니다.

     

    Dirty Checking을 이용한 수정을 다음과 같습니다.

    @DisplayName("UPDATE 테스트")
        @Test
        public void updateSuccessTest() {
            //given
            Member member = Member.builder().userId("Test").name("Test").nickName("Test").password("Test").phoneNumber("Test").address(new Address("a1", "a2", "a3")).build();
            Member fail = Member.builder().userId("fail").name("fail").nickName("fail").password("fail").phoneNumber("fail").address(new Address("a1", "a2", "a3")).build();
            memberRepository.save(member);
    
            //when
            String updated = "updated";
            member.changeNickName(updated);
    
            //then
            Optional<Member> result = memberRepository.findById(member.getId());
            Assertions.assertThat(result.orElse(fail).getNickName()).isEqualTo(updated);
        }

    위의 코드와 다르게 save 없이 NickName만 변경함으로써 잘 수행되는 모습을 볼 수 있습니다.

     

    결론

    테스트만 통과하면 패스하는것보다는 쿼리까지 확인해보면서 공부해야 할 것 같습니다.

    save와 saveAndFlush, JPA의 Update와 Dirty Cheking에 대해서 공부가 잘된 것 같습니다.

     

     

    출처

    https://jojoldu.tistory.com/415

     

    더티 체킹 (Dirty Checking)이란?

    Spring Data Jpa와 같은 ORM 구현체를 사용하다보면 더티 체킹이란 단어를 종종 듣게 됩니다. 더티 체킹이란 단어를 처음 듣는분들을 몇번 만나게 되어 이번 시간엔 더티 체킹이 무엇인지 알아보겠습

    jojoldu.tistory.com

    https://okky.kr/article/308772

     

    OKKY | JPA 에서 save(insert) 순서 때문에 오류가 발생해요

    JPA (하이버네이트) 이용중인데, 연관관계가 있는 테이블에 값을 집어 넣는데 오류가 발생해요. 모든 테이블은 Entity 와 Repository 가 지정되어 있고, Transactional 로 선언되어 있는 Service 에서 실행되

    okky.kr

    https://code-examples.net/ko/q/1438ba3

     

    java - 순서 - Spring 데이터 jpa에서 save와 saveAndFlush의 차이점

     

    code-examples.net

     

    댓글

Designed by Tistory.