영속성 컨텍스트는 이슈는 JPA의 "동일성" 때문에 발생하는 문제이다.
왜냐하면 영속성 컨테스트에서 관리되는 객체들은 "==" true를 보장하기 때문이다.
따라서, 예상치 못한 문제가 발생할 수 있으니 영속성 컨텍스트를 주의해서 관리해야 한다.
*주의*
1. 값을 조회해서 "수정"하는지, 새롭게 "생성"하는지 주의해서 확인한다.
2. @Transactional이 선언된 메서드는 모든 메서드가 "끝나는 시점"에 영속성 컨텍스트가 DB에 반영된다.
3. JPA에서 "조회"를 하면 DB에서 쿼리를 조회해오기 전에 영속성 컨텍스트에서 먼저 조회한다.
4. JPA에서 "save()"는 DB에 저장하는 것이 아니라 영속성 컨텍스트에 넣는 것이다.
1. 임베디드 타입
entityManger.clear() 유무에 따라서 조회하는 객체의 형태가 달라질 수 있음을 확인해본다.
@Test
void embedTest() {
User user1 = new User();
user1.setName("bert");
user1.setHomeAddress(null);
user1.setCompanyAddress(null);
userRepository.save(user1);
User user2 = new User();
user2.setName("john");
user2.setHomeAddress(new Address());
user2.setCompanyAddress(new Address());
userRepository.save(user2);
// entityManager.clear();
userRepository.findAll().forEach(System.out::println);
userHistoryRepository.findAll().forEach(System.out::println);
assertAll(
() ->assertThat(userRepository.findById(5L).get().getHomeAddress()).isNull(),
() ->assertThat(userRepository.findById(6L).get().getHomeAddress()).isInstanceOf(Address.class)
);
}
user1는 address를 null로, user2는 address를 new Address() 비어있는 객체를 넣는 차이점에 주목한다. save()할 때 영속성 컨텍스트에 그대로 값이 남아있으므로 findById()를 할 때 영속성 컨텍스트에 저장되어있는 값을 그대로 조회한다. 따라서 assertAll은 모두 성공한다. user1과 user2는 address에 각각 null과 비어있는 address 객체를 가지고 있는 상태다.
하지만, eneityMnager.clear()와 함께 실행하면 테스트는 실패한다. 왜냐하면 clear()로 영속성 컨텍스트가 초기화되면서 1차 캐시에 아무 것도 없기 때문에 findById()로 User를 호출할 때, 영속성 컨텍스트에 user1, user2의 address는 모두 null로 조회되기 때문이다. 따라서 isInstanceOf(Address.class)가 아니라 isNull()을 사용해야 테스트를 통과한다.
2. 칼럼 값의 수정 값 영속성 유지 여부
@Test
@Transactional
void commentTest() {
Comment comment = commentRepository.findById(3L).get();
comment.setCommentedAt(LocalDateTime.now());
commentRepository.save(comment);
// entityManager.clear();
System.out.println(commentRepository.findById(3L).get());
}
commentedAt을 setter로 추가한 comment를 save()한다. save()는 DB에 반영하는 것이 아니라 영속성 컨텍스트에만 넣어둔다. 메서드가 "끝나는 시점"에서 DB에 반영이 된다.
마지막 조회에서 commentedAt이 null이 아닌 이유는, 동일성 때문에 현재 1차 캐시에 setCommentedAt()이 적용된 comment 가 조회되기 때문이다.
@Test
@Transactional
void commentTest() {
Comment comment = commentRepository.findById(3L).get();
comment.setCommentedAt(LocalDateTime.now());
commentRepository.save(comment);
entityManager.clear();
System.out.println(commentRepository.findById(3L).get());
}
save()는 영속성 컨텍스트에 넣는 것일 뿐, DB 저장소에 넣는 것이 아니다. 따라서, entityManager.clear()는 영속성 컨텍스트를 비워버리기 때문에 comment에는 setCommentedAt()이 초기화가 된다. 마지막 조회에서 commentedAt이 null이 나온다.
@Test
@Transactional
void commentTest() {
Comment comment = commentRepository.findById(3L).get();
comment.setCommentedAt(LocalDateTime.now());
commentRepository.saveAndFlush(comment);
entityManager.clear();
System.out.println(commentRepository.findById(3L).get());
}
save()는 영속성 컨텍스트에만 넣지만 saveAndFlush()는 영속성 컨텍스트에 있는 엔티티를 실제 DB 저장소에 넣는다. 따라서 comment가 DB에 저장되기 때문에, entityManger.clear()를 해서 영속성 컨텍스트를 비워줘도 상관없다. 마지막 조회쿼리에서 DB 저장소에 있는 comment는 commentedAt을 가지고 있으므로 조회된다.
* commentedAt 자동생성
@Column(columnDefinition = "datetime(6) default now(6)")
private LocalDateTime commentedAt;
datetime을 datetime(6) default now(6)로 변경한다.
...
@DynamicInsert
public class Comment {
....
}
Comment 클래스에 @DynamicInsert를 추가한다.
@Test
@Transactional
void commentTest2() {
Comment comment = new Comment();
comment.setComment("좀 별로였습니다.");
commentRepository.saveAndFlush(comment);
entityManager.clear();
commentRepository.findAll().forEach(System.out::println);
}
comment는 이미 saveAndFlush()로 DB에 저장되었다. entityManger.clear()로 초기화해주면, 영속성 컨텍스트 1차 캐시에 있던 comment가 사라지면서 초기화된다.
따라서 findAll()로 조회하면 DB에 저장되있던 값을 영속성 컨텍스트로 조회해오기 떄문에 저장된 commentedAt이 조회된다.
@Test
@Transactional
void commentTest2() {
Comment comment = new Comment();
comment.setComment("좀 별로였습니다.");
commentRepository.saveAndFlush(comment);
//entityManager.clear();
commentRepository.findAll().forEach(System.out::println);
}
만약에 entityMnager.clear()를 제거하면, findAll() 조회에서 commentedAt은 null이 나온다. 영속성 컨텍스트에 처음부터 끝까지 처음 comment로 유지가 된다. Comment 객체는 commentedAt을 @DynamicInsert로 설정했기 떄문에 캐시에는 commentedAt 내용이 저장되어있지 않다. 정리하면, @DynamicInsert로 추가되는 commentedAt은 영속성 컨텍스트에 반영되지 않는다.
* 업데이트는 반영하지 않는 @DynamicInsert
@Test
@Transactional
void commentDynamicInsertTest() {
Comment comment = commentRepository.findById(1L).get();
comment.setComment("좀 별로였습니다.");
commentRepository.saveAndFlush(comment);
entityManager.clear();
commentRepository.findAll().forEach(System.out::println);
}
Comment를 새로 생성하지 않고, findById()로 조회하여 수정한다면, commentedAt이 적용되지 않는다. 왜냐하면, @DynamicInsert는 새로 생성하는 Insert에 해당하기 때문에 update는 반영되지 않는다.
*정리
- 엔티티가 새로 생성되거나 수정한 이후에, 영속성 컨텍스트를 clear()로 초기화해 주어야 한다.
clear() 이후 조회 시, 최신화 된 객체를 얻을 수 있다.
- @DynamicInsert는 1번이 아니라 3번에서 적용된다.
'학습 > DB' 카테고리의 다른 글
경로 표현식 (0) | 2021.10.05 |
---|---|
JPA에서 Dirty Check 방지하기 (0) | 2021.10.02 |
Converter 사용하기 (0) | 2021.09.30 |
@Embedded, @Embedabble (0) | 2021.09.30 |
Native Query (0) | 2021.09.30 |