JPA의 UpdateQuery와 Dirty Checking의 혼용에 대해서

Mo-Greene·2024년 6월 17일
post-thumbnail

가끔 이런 코드가 있다.

Scenario #

1. 테이블의 특정옵션 초기화
2. 특정 row의 특정옵션 설정

나의 경우 프로젝트에서 기본배송지 설정과 관련한 로직을 구현하다가 이런 시나리오가 나왔다.

Usecase #

1. "기본배송지 설정" 클릭 시
2. 클릭한 회원과 관련된 모든 배송지 false (true가 기본배송지)
3. 기본 배송지로 선택한 배송지 수정된 내용과 배송지 옵션 true로 변환

코드의 구현은 아래와 같이 진행했다.


//Usecase 1.
@Transactional
	public void updateMemberDelivery(Long memberDeliveryId, MemberDeliveryRequest request) {

		var memberDelivery = memberDeliveryRepository.findById(memberDeliveryId)
			.orElseThrow(() -> new CustomException("배송지가 없습니다."));

		if (request.getIsDefault()) {
			Member member = getMemberEntity();
			//Usecase 2.
			memberDeliveryRepository.updateMemberDeliveryDefault(member.getId(), memberDelivery.getId());
		}

		//Usecase 3.
		memberDelivery.updateMemberDelivery(request);
	}

해당 코드의 updateMemberDeliveryDefault는

@Modifying(clearAutomatically = true, flushAutomatically = true)
	@Query(
		"UPDATE 회원배송지 md " +
		"SET md.기본배송지 = "
			+ "CASE "
				+ "WHEN md.배송지 pk = :memberDeliveryId "
				+ "THEN true "
				+ "ELSE false END " +
		"WHERE md.회원 id = :memberId"
	)
	void updateMemberDeliveryDefault(@Param("memberId") Long memberId, @Param("memberDeliveryId") Long memberDeliveryId);

그리고 updateMemberDelivery는 jpa의 더티체킹을 이용한 영속화 메서드이다.

	/**
	 * 회원 배송지 수정
	 * @param request MemberDeliveryRequest
	 */
	public void updateMemberDelivery(MemberDeliveryRequest request) {
		this.title = request.getTitle();
		this.name = request.getName();
		this.phone = request.getPhone();
		this.address = request.getAddress();
		this.addressDetail = request.getAddressDetail();
		this.location = request.getLocation();
		this.content = request.getContent();
		this.isDefault = request.getIsDefault();
	}

내 생각대로 흘러간다면 query log는

query log

1. 회원배송지 select
2. 회원 select
3. 회원배송지 전부 false update
4. 선택한 회원배송지 true update

위와 같은 흐름으로 나와야 한다고 봤다. 하지만 쿼리는

[Hibernate] 
    select
        회원 배송지 조회
    from
        회원 배송지 md1_0 
    where
        md1_0.id=?
[Hibernate] 
    select
        회원 조회
    from
        회원 m1_0 
    where
        m1_0.id=?
[Hibernate] 
    update
        회원 배송지 
    set
        기본 배송지(true/false)=case 
            when id=? 
                then true 
            else false 
        end 
    where
        회원 pk=?

결과는 이런 식으로 나왔다.
Usecase의 1,2번까지는 정상적으로 동작 후 3번째 회원의 배송지 정보를 수정하는 query가 나가지 않는것이다.
사실 코드를 만들때 부터 긴가민가한 의문점이 있었다.


유추

var memberDelivery = memberDeliveryRepository.findById(memberDeliveryId)
"UPDATE 회원배송지 md " +
"SET md.기본배송지 = "
	+ "CASE "
		+ "WHEN md.배송지 pk = :memberDeliveryId "
		+ "THEN true "
		+ "ELSE false END " +
"WHERE md.회원 id = :memberId"

이 2개의 코드 즉, findById로 조회한 MemberDelivery는 영속성 컨텍스트에 저장하게 되고
@Query 어노테이션으로 update를 진행하는 쿼리는 영속성 컨텍스트 접근없이 직접 데이터베이스에 해당 쿼리를 동작시켜버린다.

그렇기때문에 이게 만약 동작하더라도 영속성 변화를 jpa가 눈치챌 수 있을까? 하는 생각이 있었다.

즉, jpa는 조회한 MemberDelivery 객체를 영속성 컨텍스트에 넣고 바라보고 있는데 내가 직접 jpa를 거치지 않고 @Query 메서드를 통해 DB의 값들을 바꿔버렸다면 jpa에서 보는 영속성 컨텍스트의 값들은 틀린 값들이 되기 때문에 정상적으로 동작하지 않을 것 이다. 라고 유추하였다.


해결방안

2가지의 해결방안이 있다고 생각한다.

  1. DirtyChecking을 먼저 동작시켜 update쿼리를 동작시킨다.
  2. 한방 update쿼리를 만들어 동작시킨다.

한방 update쿼리의 경우 조금 복잡해질 가능성이 있다고 생각해 1번의 방법을 사용해봤다.

//기존 코드
@Transactional
	public void updateMemberDelivery(Long memberDeliveryId, MemberDeliveryRequest request) {

		var memberDelivery = memberDeliveryRepository.findById(memberDeliveryId)
			.orElseThrow(() -> new CustomException("배송지가 없습니다."));

		if (request.getIsDefault()) {
			Member member = getMemberEntity();
			memberDeliveryRepository.updateMemberDeliveryDefault(member.getId(), memberDelivery.getId());
		}
		memberDelivery.updateMemberDelivery(request);
	}
    
//변경한 코드
@Transactional
	public void updateMemberDelivery(Long memberDeliveryId, MemberDeliveryRequest request) {

		var memberDelivery = memberDeliveryRepository.findById(memberDeliveryId)
			.orElseThrow(() -> new CustomException("배송지가 없습니다."));
	
    	//순서만 바꿨다.
		memberDelivery.updateMemberDelivery(request);

		if (request.getIsDefault()) {
			Member member = getMemberEntity();
			memberDeliveryRepository.updateMemberDeliveryDefault(member.getId(), memberDeliveryId);
		}
	}

실행 query log

[Hibernate] 
    select
        회원 배송지 조회
    from
        회원 배송지 md1_0 
    where
        md1_0.id=?
[Hibernate] 
    select
        회원 조회
    from
        회원 m1_0 
    where
        m1_0.id=?
[Hibernate] 
    update
        회원 배송지
    set
        배송지 제목=?,
        수정날짜=? 
    where
        id=?
[Hibernate] 
    update
        회원 배송지 
    set
        기본 배송지(true/false)=case 
            when id=? 
                then true 
            else false 
        end 
    where
        회원 pk=?

내 생각대로 더티체킹으로 인한 배송지update 쿼리 이후 나머지 기존 배송지의 배송지 상태값을 전부 변경하는 update 쿼리가 동작하게 되었다.


마치며

내가 해결한 이 상황이 jpa를 완벽하게 이해하고 해결한 것이 아니다.

이건 엄연히 나의 추측을 기반으로 하여 해결한 상황이다. (올바른 해결방안이 아닐 수도 있다.)

profile
아둥바둥 버텨라

0개의 댓글