flush()
호출시 동작은 다음과 같다. 참조 무결성을 보장하기 위해 다음과 같은 순서로 쿼리를 호출한다고 한다.
프로젝트를 진행하면서 데이터베이스 구조를 링크드 리스트 구조로 만들었다. 이는 엔티티 간의 순서를 나타내고, 순서를 수정할 때에 수정 쿼리를 최소화하기 위함이었다.
순서를 수정할 때에는 전후의 엔티티들과의 연관관계를 적절하게 끊어주고 맺어주어야 하는데, UPDATE
쿼리가 내가 예상한 순서대로 나가지 않는 문제가 발생했다. 따라서 이번 글에서는 flush()
발생시 UPDATE
쿼리 실행 순서를 알아보고자 한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String memberName;
}
아주 간단한 예제 엔티티를 구성했다. 이를 기반으로 아래의 예제 상황을 살펴보자.
실제로 운영 환경에서는
Member
의INSERT
가 이미 되어 있을 것으로 가정했기 때문에, 트랜잭션을 분리하여 경계를 짓고 싶었다.@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
쿼리가 발생했다. memberA
→ memberB
→ memberC
순으로 UPDATE
쿼리가 발생했다. (id
보다는 변경된 이름이 더 알아보기 편할 것으로 예상되어, 바뀐 member_name
으로 설명한 것이다)
그렇다면 다음과 같이 변경을 가하는 메서드의 호출 순서를 바꾸면 어떻게 될까?
@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()
을 호출한 순서처럼 memberC
→ memberB
→ memberA
가 아니라 이전의 테스트 케이스와 동일한 순서대로 memberA
→ memberB
→ memberC
순으로 UPDATE
쿼리가 발생하는 것을 확인할 수 있다.
그렇다면 마지막으로 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");
});
}
이전처럼 memberA
→ memberB
→ memberC
순서대로 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
아니다. 이제는 또 memberC
→ memberB
→ memberA
순서대로 UPDATE
쿼리가 발생한다.
위의 일련의 과정을 통해 하나의 규칙을 찾을 수 있었다. 그것은 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차 캐시에 올라온 엔티티의 스냅샷을 순차적으로 비교하여, 그 순서대로 쿼리가 발생한다는 것을 알 수 있었다.
따라서 트랜잭션 내에서 변경감지를 사용하고, 수정 쿼리의 순서가 참조 무결성이나 데이터의 정합성에 영향을 미친다면 문제가 예상되는 부분에서 flush
를 날려주거나, 영속성 컨텍스트를 거치지 않고 바로 데이터베이스에 쿼리를 날리는 것이 좋을 것 같다.
https://www.javacodegeeks.com/2013/11/hibernate-facts-knowing-flush-operations-order-matters.html
오 헙크 전 완전 랜덤인줄 알았는데, 영속성 컨텍스트에 올라간 순서라는 특징이 있었군요.
재밌게 글 읽었습니다.