-
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가 나감)
결과 예시
만약 Entity에 @DynamicUpdate 어노테이션이 존재하지 않는다면 다음과 같은 쿼리가 나가게 됩니다.
우리는 tradeNo만 수정했지만 amount도 같이 업데이트되는 것이죠
하지만 Entity의 필드가 20~30개 이상인 경우에는 전체 필드 Update 쿼리가 부담스러울 수 있습니다.
따라서 이런 경우에 @DynamicUpdate 어노테이션을 사용하여 변경된 필드만 반영되도록 할 수 있습니다.
@DynamicUpdate 어노테이션 적용 결과
내 프로젝트에 반영해보기
기존 테스트 코드
@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 쿼리만 나갑니다.
왜 그럴까요?
다음은 조금 더 명확한 답변인 것 같습니다.
위에서는 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 쿼리가 나갑니다.
여기서 근데 문제가 있습니다.
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
https://okky.kr/article/308772
https://code-examples.net/ko/q/1438ba3
'프로젝트 > 게시판 프로젝트' 카테고리의 다른 글
intellij 자동 포맷팅 시 줄 바꿈 처리 적용해주기 (0) 2022.05.19 로그인 기능을 만들어보자 (0) 2022.05.18 컨트롤러를 테스트해보자! (0) 2022.05.13 [에러 해결 완료] Type definition error , InvalidDefinitionException (0) 2022.05.12 테스트코드에서는 H2 DB를 사용하자! (0) 2022.05.11