변경감지 UPDATE 쿼리 생성 순서

헙크·2023년 10월 8일
1

1. 개요

1.1. 개요

flush() 호출시 동작은 다음과 같다. 참조 무결성을 보장하기 위해 다음과 같은 순서로 쿼리를 호출한다고 한다.

  1. Inserts, in the order they were performed
  2. Updates
  3. Deletion of collection elements
  4. Insertion of collection elements
  5. Deletes, in the order they were performed

1.1. 의문점

프로젝트를 진행하면서 데이터베이스 구조를 링크드 리스트 구조로 만들었다. 이는 엔티티 간의 순서를 나타내고, 순서를 수정할 때에 수정 쿼리를 최소화하기 위함이었다.

순서를 수정할 때에는 전후의 엔티티들과의 연관관계를 적절하게 끊어주고 맺어주어야 하는데, UPDATE 쿼리가 내가 예상한 순서대로 나가지 않는 문제가 발생했다. 따라서 이번 글에서는 flush() 발생시 UPDATE 쿼리 실행 순서를 알아보고자 한다.

1.3. 예제 도메인

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String memberName;
}

아주 간단한 예제 엔티티를 구성했다. 이를 기반으로 아래의 예제 상황을 살펴보자.

2. 예제 상황

2.1. 테스트 코드1

실제로 운영 환경에서는 MemberINSERT가 이미 되어 있을 것으로 가정했기 때문에, 트랜잭션을 분리하여 경계를 짓고 싶었다. @Transactional 애너테이션을 사용하면서, 전파 옵션과 em.flush(), em.clear()를 사용할 수도 있었지만 예제를 단순화하기 위해 TransactionTemplate을 사용해봤다.

@SpringBootTest
class MemberSpringBootTest {
    @Autowired private MemberRepository memberRepository;
    @Autowired private TransactionTemplate template;
    @Autowired private EntityManager em;

    @Test
    void test() {
        //given, when
        template.executeWithoutResult(result -> {
            Member member1 = new Member("member1");
            Member member2 = new Member("member2");
            Member member3 = new Member("member3");

            memberRepository.saveAll(List.of(member1, member2, member3));
        });

        //then
        template.executeWithoutResult(result -> {
            Member member1 = memberRepository.findByMemberName("member1");
            Member member2 = memberRepository.findByMemberName("member2");
            Member member3 = memberRepository.findByMemberName("member3");

            System.out.println("=====");

            member1.changeName("memberA");
            member2.changeName("memberB");
            member3.changeName("memberC");
        });
    }
}

given, when절의 트랜잭션은 기본 데이터를 넣어 놓는 것이기 때문에 설명을 생략하겠다. then절의 트랜잭션에서 발생한 쿼리를 살펴보면..

# member1 조회
select
    m1_0.id,
    m1_0.member_name      
from
    member m1_0      
where
    m1_0.member_name='member1'

# member2 조회
select
    m1_0.id,
    m1_0.member_name      
from
    member m1_0      
where
    m1_0.member_name='member2'

# member3 조회
select
    m1_0.id,
    m1_0.member_name      
from
    member m1_0      
where
    m1_0.member_name='member3'

---

# member1 수정
update
    member      
set
    member_name='memberA'      
where
    id=1

# member2 수정
update
    member      
set
    member_name='memberB'      
where
    id=2

# member3 수정
update
    member      
set
    member_name='memberC'      
where
    id=3

짐작한대로, 세 개의 조회 쿼리 이후에 UPDATE쿼리가 발생했다. memberAmemberBmemberC 순으로 UPDATE 쿼리가 발생했다. (id보다는 변경된 이름이 더 알아보기 편할 것으로 예상되어, 바뀐 member_name으로 설명한 것이다)

2.2. 테스트 코드2

그렇다면 다음과 같이 변경을 가하는 메서드의 호출 순서를 바꾸면 어떻게 될까?

@Test
void test() {
    //given
		...

    //then
    template.executeWithoutResult(result -> {
        Member member1 = memberRepository.findByMemberName("member1");
        Member member2 = memberRepository.findByMemberName("member2");
        Member member3 = memberRepository.findByMemberName("member3");

        System.out.println("=====");

		// 바뀐 부분!!
        member3.changeName("memberC"); 
        member2.changeName("memberB");
        member1.changeName("memberA");
    });
}

위의 케이스에서 UPDATE 쿼리는 어떤 순서대로 날라갈까?!?! UPDATE 쿼리 부분만 살펴보자.

# member1 수정
update
    member      
set
    member_name='memberA'      
where
    id=1

# member2 수정
update
    member      
set
    member_name='memberB'      
where
    id=2

# member3 수정
update
    member      
set
    member_name='memberC'      
where
    id=3

이상하게도 changeName()을 호출한 순서처럼 memberCmemberBmemberA가 아니라 이전의 테스트 케이스와 동일한 순서대로 memberAmemberBmemberC 순으로 UPDATE 쿼리가 발생하는 것을 확인할 수 있다.

2.3. 테스트 코드3

그렇다면 마지막으로 member조회하는 순서를 변경해보자. 아래와 같은 상황에서는 UPDATE 쿼리의 순서가 어떻게 될까?

@Test
void test() {
    //given
		...

    //then
    template.executeWithoutResult(result -> {
    	// 바뀐 부분!!
        Member member3 = memberRepository.findByMemberName("member3");
        Member member2 = memberRepository.findByMemberName("member2");
        Member member1 = memberRepository.findByMemberName("member1");

        System.out.println("=====");

        member1.changeName("memberA");
        member2.changeName("memberB");
        member3.changeName("memberC");
    });
}

이전처럼 memberAmemberBmemberC 순서대로 UPDATE 쿼리가 발생할까?

# member3 수정
update
    member      
set
    member_name='memberC'      
where
    id=3

# member2 수정
update
    member      
set
    member_name='memberB'      
where
    id=2

# member1 수정
update
    member      
set
    member_name='memberA'      
where
    id=1

아니다. 이제는 또 memberCmemberBmemberA 순서대로 UPDATE 쿼리가 발생한다.

3. 정리

위의 일련의 과정을 통해 하나의 규칙을 찾을 수 있었다. 그것은 UPDATE 쿼리는 변경을 가한 메서드를 호출한 순서대로 발생하는 것이 아니라, 영속성 컨텍스트에 엔티티가 올라온 순서대로 발생한다는 것이다.

테스트 코드2에서 설명했던 코드를 토대로 정리해보면

@Test
void test() {
    //given
		...

    //then
    template.executeWithoutResult(result -> {
        Member member1 = memberRepository.findByMemberName("member1");
        Member member2 = memberRepository.findByMemberName("member2");
        Member member3 = memberRepository.findByMemberName("member3");

        System.out.println("=====");

        member3.changeName("memberC"); 
        member2.changeName("memberB");
        member1.changeName("memberA");
    });
}

🔼 조회할 때까지의 영속성 컨텍스트의 모습이다.

🔼 member3.changeName("memberC"); 호출

🔼 member2.changeName("memberB"); 호출

🔼 member1.changeName("memberA"); 호출

commit()이 호출되면 flush()가 호출되고, 변경 감지(dirty-checking)가 발생할 것이다. 이때 1차 캐시에 올라온 엔티티의 순서대로 변경 사항을 감지할 것이다.

🔼 flush()호출 후, 변경 감지

🔼1차 캐시에 저장된 엔티티의 순서대로 변경 감지를 진행하고 쓰기 지연 SQL 저장소UPDATE 쿼리 저장. 이후 DB에 반영.

따라서 UPDATE쿼리는 변경을 가한 순서대로 쿼리가 발생하는 것이 아니라, flush() 가 일어나는 시점에 1차 캐시에 올라온 엔티티의 스냅샷을 순차적으로 비교하여, 그 순서대로 쿼리가 발생한다는 것을 알 수 있었다.

4. 정리

따라서 트랜잭션 내에서 변경감지를 사용하고, 수정 쿼리의 순서가 참조 무결성이나 데이터의 정합성에 영향을 미친다면 문제가 예상되는 부분에서 flush를 날려주거나, 영속성 컨텍스트를 거치지 않고 바로 데이터베이스에 쿼리를 날리는 것이 좋을 것 같다.

5. 참고

https://www.javacodegeeks.com/2013/11/hibernate-facts-knowing-flush-operations-order-matters.html

4개의 댓글

comment-user-thumbnail
2023년 10월 9일

오 헙크 전 완전 랜덤인줄 알았는데, 영속성 컨텍스트에 올라간 순서라는 특징이 있었군요.
재밌게 글 읽었습니다.

1개의 답글
comment-user-thumbnail
2023년 11월 8일

와 처음 안 사실이에요 ㅋㅋㅋㅋ bb

1개의 답글