JPA의 변경감지
jpa에서 데이터를 수정하는 방식으로는 영속상태의 엔티티 필드를 수정하는 방식인 변경감지가 있다.
변경감지가 일어난 이후, 트랜잭션 종료 또는 JPQL 실행을 하면 영속성 컨텍스트를 flush하게 되어있다.
테스트를 하다 마주친 양방향 연관관계 매핑 과정에서 생긴 궁금증과 테스트를 통해 궁금증을 해결한 과정을 적어보고자 한다.
양방향 연관관계 매핑
우선 설명에 필요한 두 엔티티에 대해 간략하게 적어보자면 아래와 같다.
1. Member: Member 엔티티는 List<Solve> 타입의 필드를 가진다. (@OneToMany(mappedBy))
2. Solve: Solve 엔티티는 Member 타입의 필드를 가진다. (@ManyToOne)
이 두 관계는 양방향 연관관계로써, Member엔티티의 List<Solve> 타입의 필드가 mappedBy 형태를 취하고 있다.
위 상황을 간략히 설명하자면,
Member 엔티티를 새롭게 생성하고 1차 캐시에 저장한 후, Solve 엔티티를 새롭게 생성하고 1차 캐시에 저장한 상태이다.
그래서, 맨 아래 member.getSolvedList()의 크기를 1로 기댓값을 설정 후, 테스트를 돌린 결과 0이 반환되었다.
Solve 엔티티 내부에 있는 Member 타입의 필드에 현재 member객체를 넣었는데, 왜 이런결과가 나왔을까?
=> 당연하게도 1차캐시에서 데이터를 가져왔기 때문이다.
사실, member객체의 solvedList는 단지 자바객체일 뿐이고, 현재 Solve 엔티티는 1차 캐시에서 가져온 상태이다.
이게 의미하는 바는 solveId를 통해서 Solve 데이터를 가져왔지만, 쿼리를 통해 가져온 것이 아닌 1차 캐시에 저장해 놓은 것을 그대로 가져온 것이며, 이는 쿼리를 통해 가져온 것이 아니기 때문에 Member 엔티티를 join해서 가져온다거나 그런 상태가 전혀 아니라는 말이다.
좀 더 자세히 설명하면, 동일한 트랜잭션에서 테스트되는 동안 member객체는 1차 캐시에 저장되어 있고, 여기서의 solvedList 필드는 당연히 빈 List일 뿐이다. 그리고, (Member 엔티티 내부에 solvedList에 new ArrayList()를 할당해놓음.)
이렇게 테스트를 하다 보면 데이터가 불일치하는 경우가 발생한다. 때문에, mappedBy 쪽에 List 객체에 데이터를 넣어줘서 데이터를 동일한 트랜잭션 내에서 데이터를 일치시켜 줄 필요가 있다.
그렇기 때문에, Solve 데이터를 새롭게 생성할 때, member.getSolvedList().add(Solve엔티티)를 해줌으로써 한 트랜잭션 안에서도 데이터를 일치시켜 줄 수 있게 된다.
그래서, 다음으로 궁금했던 것은 위와 같이 member 객체에도 데이터를 맞춰준 상태에서 Solve엔티티를 수정하면 어떻게 될까?
"내가 생각했던 부분은 member.getSolvedList()를 통해 가져온 Solve엔티티도 올바르게 수정이 일어날까?"였다.
근데, 지금 이 글을 적으면서 생각해 보니깐 변경되는 게 당연하다.. 저 테스트하고, 궁금증을 해결하면서 좋아했었는데.. 생각을 정리하다 보니 너무 당연하게 느껴진다..
파란색 박스 부분이 수정 전에 member객체의 Solve 데이터를 가져온 모습이고,
빨간 박스 부분이 "content수정222"라고 수정 후 member객체의 Solve 데이터를 가져온 모습이다.
해당 테스트의 결과는 아래처럼 성공이었다.
왜 member 객체의 필드값인 List<Solve> solvedList 값의 Solve엔티티도 수정이 잘 되었을까?
처음에 이 의문을 가졌던 건 그거였다.
"member객체는 새로 가져온 것이 아닌 기존의 객체 그대로인데, 왜 필드값의 수정이 잘 이루어질까?"
영속성 컨텍스트의 전체적인 메커니즘과 함께 설명하자면, 아래와 같다.
- 빨간색 박스에 있는 updateSolveState()에는 Solve의 필드값을 수정하는 로직이 들어있는데,
그 안에는 member와 problem에 맞는 Solve 데이터를 찾기 위한 JPQL문이 수행된다. - 그렇기 때문에, 이전에 영속성 컨텍스트에 있던 쿼리문을 flush하고 나서, select문을 통해 Solve를 1차 캐시로 가져온다.
- 가지고 온 Solve에 필드 수정("content." -> "content수정222") 이 일어난다. 이 부분이 바로 dirty checking.
- 그리고, member객체 안에 있는 Solve 객체는 동일한 객체인데, 단지 그 Solve객체 안에서 content가 일부 수정된 것이다.
즉, 당연히 member.solvedList()에서는 똑같은 Solve객체를 가지고 있고, 그 똑같은 Solve 객체의 필드가 일부 수정된 것이기에 변경이 되는 것이다.
'backend > Spring' 카테고리의 다른 글
빈번한 북마크 update요청을 방지하기 위한 사용자별 스레드 관리 (0) | 2023.09.13 |
---|---|
무한 스크롤 구현 및 성능개선 (Pagination) (0) | 2023.09.13 |
Fetch join과 Paging (0) | 2023.08.14 |
[Spring] 스프링의 DI를 이용한 전략패턴 도입 (0) | 2023.06.08 |
[spring] @Configuration의 프록시 객체 (0) | 2023.03.10 |