BackEnd/JPA

JPA의 Dirty Checking이란?

hyunki.Dev 2022. 11. 5. 22:07

Jpa를 활용하여 백엔드 개발을 하다보면 'JPA 영속성 컨텍스트' 혹은 '더티 체킹'이란 말을 종종 들을 수 있습니다. 실제로 얼마전에 회사에서 코드리뷰를 진행하는데 어떤 팀원 분께서  다음과 같이 말씀하셨다.

 

제가 알기로 JPA는 더티 체킹이라는 걸 해서 변경사항을 감지해 자동으로 update를 해주는 걸로 알고 있는데 왜 한번 더 save를 해줬나요? 이유가 있어요?



그 때 나는 물론 명확하게 답변을 드리지 못했다... 아..? 네..! 그러면 한번 더 save 하지 않고 트랜잭션을 마무리 하도록 하겠습니다. 라고 대충 얼버무렸던 기억이.... 그래서 오늘은 이 Dirty Checking 이라는 것의 개념을 한번 짚고 넘어가려고 합니다. 다음에는 명확히 답변드릴 수 있도록...! 

 

예를 들어 다음과 같은 코드가 있습니다.

@Slf4j
@RequiredArgsConstructor
@Service
public class CouponService {

    public void updateCouponStatus(Long id, String tradeNo) {
        EntityManager em = entityManagerFactory.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin(); //트랜잭션 시작
        Coupon coupon = em.find(Coupon.class, id);
        coupon.changeCouponStatus(); // 엔티티만 변경
        tx.commit(); //트랜잭션 커밋
    }
}

 

위의 코드를 보시면 별도로 데이터베이스에 save 하는 부분이 존재하지 않습니다.

  1. 트랜잭션이 시작되고
  2. 엔티티를 조회하고
  3. 엔티티의 값을 변경하고
  4. 트랜잭션을 커밋합니다.

여기서 데이터베이스에 update 쿼리에 관한 코드는 어디에도 없습니다. 그런데 위의 코드에 대한 테스트 코드를 작성하고 실행해 보면 update 쿼리가 수행되는 것을 확인할 수 있습니다.

 

@RunWith(SpringRunner.class)
@SpringBootTest
public class CouponServiceTest {

    @Autowired
    CouponRepository couponRepository;

    @Autowired
    CouponService couponService;

    @After
    public void tearDown() throws Exception {
        couponRepository.deleteAll();
    }

    @Test
    public void 엔티티매니저로_확인() {
        //given
        Coupon coupon = couponRepository.save(new Coupon("test1",  100));

        //when
        String updateCouponStatus = "USED";
        couponService.updateCoupon(coupon.getId(), updateCouponStatus);

        //then
        Coupon saved = couponRepository.findAll().get(0);
        assertThat(saved.getCouponStatus()).isEqualTo(updateCouponStatus);
    }
}

 

이렇게 수행된 이유는 Dirty Checking 덕분입니다. 여기에서 Dirty 란 '상태변화' 정도로 이해하시면 됩니다. 즉 상태 변화에 대한 체킹을 의미합니다.

 

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

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

JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어놓습니다.
그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른점이 있다면 Update Query를 데이터베이스로 전달합니다.

당연히 이런 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만적용 됩니다.

  • detach된 엔티티 (준영속)
  • DB에 반영되기 전 처음 생성된 엔티티 (비영속)

등 준영속/비영속 상태의 엔티티는 Dirty Checking 대상에 포함되지 않습니다.

즉, 값을 변경해도 데이터베이스에 반영되지 않는다는 것이죠.

그럼에도 불구하고 내가 안심이 되지 않거나 동적 쿼리를 사용한 update 문을 반드시 날려야 한다. 라고 하신다면 Querydsl 에서도 update 문을 날리는 방법이 있긴 하지만 그렇게 추천하는 방법은 아니라고 합니다. 웬만하면 Querydsl의 사용을 줄이고 자바에서 해결 할 수 있으면 해결 하는 것이 좋다고 합니다.

 


변경 부분만 update하고 싶다면?

Dirty Checking으로 생성되는 update 쿼리는 기본적으로 모든 컬럼을 업데이트합니다.

JPA에서는 전체 컬럼을 업데이트하는 방식을 기본값으로 사용합니다.
전체 컬럼을 업데이트하는 방식의 장점은 다음과 같습니다.

  • 생성되는 쿼리가 같아 부트 실행시점에 미리 만들어서 재사용가능합니다.
  • 데이터베이스 입장에서 쿼리 재사용이 가능하다
    • 동일한 쿼리를 받으면 이전에 파싱된 쿼리를 재사용한다.

출처: 김영한님의 자바 ORM 표준 JPA 프로그래밍

다만, 필드가 20~30개 이상인 경우엔 이런 전체 필드 Update 쿼리가 부담스러울 수 있습니다.

사실 이런 경우 정규화가 잘못된 경우일 확률이 높습니다.
한 테이블에 필드 30개는 확실히 많습니다.
현재 운영중인 정산 서비스에는 데이터양이나 복잡도가 국내에서 손꼽히지만 15개 넘는 필드를 가진 테이블은 없습니다.

 

이런 경우엔 @DynamicUpdate로 변경 필드만 반영되도록 할 수 있습니다.

엔티티 최상단에 아래와 같이 @DynamicUpdate 를 선언해주시면 됩니다.

@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate // 변경한 필드만 대응하기 위한 어노테이션
public class Coupon {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long couponId;

    private String status;
    private String amount;

 

조회한 Entity에서 필요한 부분만 변경 후 해당 Entity를 다시 Save 해도 물론 같은 프로세스를 수행하게 됩니다. 회사의 시니어 개발자 분들도 그저 취향 차이일 뿐이라고 하시긴 했습니다만... 좀 더 더티 체킹 같은 부분들을 활용할 수 있다면 활용하는 것을 좋아하시는 것 같기도 합니다.

 

 

참고자료 : https://jojoldu.tistory.com/415

 

더티 체킹 (Dirty Checking)이란?

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

jojoldu.tistory.com

https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80

 

JPA 영속성 컨텍스트란?

영속성 컨텐스트란 엔티티를 영구 저장하는 환경이라는 뜻이다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.em.persist

velog.io