👩💻 JPA… 너 진짜 양파같다 🧅 \(〇_o)/(⊙_⊙)?(⊙ˍ⊙)
entityManager.clear();
도입을 통해 해결했다. 왜 이 엔티티 매니저가 동작하면 해결되는지 공부를 하고자한다. (실제 오류 코드는 아니고 단순하게 설명하기 위해 만든 코드다. 만약 헷갈리는 부분이나 본인이 안되는 부분이 있다면 댓글로 알려주길 바란다.)
✌️문제 원인을 유추하면서 읽으면 더 재밌을 것 같다!✌️
문제 상황 1 : 조회가 안돼요!
조건 : ProductTest 엔티티와 OptionTest 엔티티는 일대다 양방향 관계를 가진다.
테스트 수행내용
- 새로운 Product를 저장한다.
- product 엔티티를 가지는 Option을 저장한다.
- 저장된 product의 getOption 메서드를 호출해 정상적으로 연관관계를 조회하는지 검증하자!
@Test void test() { var savedProduct = productRepository.save(new ProductTest()); optionRepository.save(new OptionTest(savedProduct)); var product = productRepository.findById(savedProduct.getId()).orElseThrow(); assertThat(product.getOptions()).hasSize(1); //실제값이 0으로 오류가 난다. }
이 코드가 성공하려면?!
entityManager.clear();
만 추가하면 성공한다.@Test void test() { var savedProduct = productRepository.save(new ProductTest()); optionRepository.save(new OptionTest(savedProduct)); entityManager.clear(); var product = productRepository.findById(savedProduct.getId()).orElseThrow(); assertThat(product.getOptions()).hasSize(1); //실제값이 0으로 오류가 난다. }
엔티티 매니저의 각 메서드, 엔티티의 생명주기, 영속성컨텍스트에 대해서 이야기해봐야한다. 이 부분들은 긴밀하게 연결이 되어있어서 한번에 이해가 어려울 수 있다. 일단 잘 읽어보고 헷갈리는 부분은 다시 읽어보자!
EntityManager
는 영속성 컨텍스트를 관리한다.
entityManager.flush();
: 영속성 컨텍스트의 변경 내용을 DB에 동기화 시킨다. 이를 위해 수정된 Entity를 감지하고 쓰기 지연 SQL 저장소에 옮긴 후 쓰기 지연 SQL 저장소에 Query 들을 DB에 전송한다.entityManager.clear();
: 영속성 컨텍스트를 지우고 준영속 상태로 변경한다.오… 다 공부하고 나니 위의 세 문장에 엄청난 개념이 축약되어있는 것이 보인다.
위에 등장한 개념을 살펴보면 크게 🔖영속성 컨텍스트, 🔖 준영속 상태 두가지로 확인할 수 있다.
EntityManager.contains(entity)
를 사용하여 해당 엔티티가 영속성 객체에 존재하는지 확인할 수 있다.1차 캐시
쓰기 지연 SQL 저장소
아직 영속성 컨텍스트를 어떻게 활용하는지 아직 다루지 않았다. 그전에 엔티티의 생명주기에 대해서 배워보자.
비영속 상태의 자바 객체
OptionTest optionTest= new OptionTest(savedProduct)
당연하게도 이 객체는 순수한 자바객체고, 아주아주 새로운 상태이다.
객체를 영속성컨텍스트로 올리는 방법
persist
메서드를 사용한다.JpaRepository
가 제공하는 save
, find
를 사용한다.영속상태의 객체를 준영속상태로 만드는 방법
엔티티매니저의 detach()
, close()
, clear()
메서드를 사용한다.
JpaRepository
는 따로 준영속상태로 만드는 메서드를 제공하지 않는다. (사실 엔티티메니저와 메서드 쿼리는 살짝 차이점이 있는데, 이 부분도 뒤에서 더 설명하겠다.
삭제 상태로 만드는 방법
엔티티매니저의 remove()
메서드를 사용한다.
JpaRepository
가 제공하는 delete
기능
결국
entityManager.clear()
는 영속상태에 있던 객체를 영속상태에서 벗어나게 하는 방법이었다. 영속상태에서 벗어난게 어떤 의미이길래 안되던 테스트 코드를 통과시켰을까?
준영속은 영속성 컨텍스트에서 지워진 상태이다.
즉, 영속성 컨텍스트의 1차 캐시, 쓰기 지연 SQL 저장소에서 사라지고, 엔티티 매니저가 관리할 수 없는 상태가 된다.
엔티티 메니저가 관리할 수 없는 상태란 무엇일까?
- 해당 엔티티를 더 이상 조회하거나, 병합하거나, 삭제할 수 없다.
- 준영속 상태에 있는 객체에 수정을 커밋을 하더라도, 엔티티 매니저가 관리하지 않은 상태로 빠지기 때문에, 수정된 데이터가 DB에 반영되지 않는다.
1) DB 관련 기능을 정상적으로 수행할 수 없다.
2) 식별자 값은 가지고 있다. (비영속 상태와 차이점)
준영속이 되었다는 것은 한번은!! 영속성 컨텍스트에 올라갔다는 것이다.
따라서 영속성 컨텍스트에 올라가면서 식별자를 가지게 된다.
3) 지연로딩을 할 수 없다.
당연히 영속성 컨텍스트가 관리하지 않기 때문에 지연 로딩시 문제가 발생한다.
🤔 find는 알겠어 DB에 접근하니까 그런데 save는 뭐야!
save의 실제 구현을 확인하자.
persist()
으로 영속성 컨텍스트에 저장하고, 식별자가 저장된 새로운 객체를 반환한다.merge()
가 동작하며 이때 전달 인자인 entity
가 아닌 새로운 인스턴스가 반환된다.이떄 왜
merge()
가 호출될까? 식별자가 존재하기 때문에 해당 객체는 영속성 컨텍스트에 저장되었던 준영속(detached) 상태라 볼 수 있기 때문이다.
@Transactional
@Override
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
여기까지 배운 것들을 정리하자.
엔티티의 생명주기 중 준영속상태의 객체들은 엔티티의 관리를 받을 수 없다.하지만 객체들을 준영속 상태에 만드는 clear을 사용후 테스트를 통과했다는 의문이 들 것이다. (그런 의문이 생겼다면 정말 당신 대단해)
조회 시
수정 시
이때 준 영속상태의 객체들은 비교할 수 없어 데이터 베이스에 변경되지 않는다.
DB 접근 비용을 줄일 수 있다.
더티 체킹을 할 수 있다. 는 장점이 있다.
DB에 접근하는 건 비용이 많이 든다. (진짜로) 컴퓨터 구조상 차이가 나는데 이건 컴구시간이나 운체시간에…
준영속상태, 즉 영속성 컨텍스트에 없기 때문에 조회쿼리를 날려, 조회를 한 후 가져올 것이다.
그렇다면 다시 내 문제의 코드를 살펴보자
var savedProduct = productRepository.save(new ProductTest());
optionRepository.save(new OptionTest(savedProduct));
데이터 베이스에는 Product와 Option객체가 하나씩 반영되어있는 상태다.
그러나 이때 그냥 Product.getOption()
을 실행하면 어떻게 될까? flush
는 데이터베이스에 변경사항을 반영하는 것이지 영속성 컨텍스트를 비우는 것이 아니다. 따라서 영속성컨텍스트에 Product값이 있어 실제 조회를 하지 않고 1차 캐시에 있는 값을 반환한다.
entityManager.clear();
var product = productRepository.findById(savedProduct.getId()).orElseThrow();
assertThat(product.getOptions()).hasSize(1);
만약 이렇게 clear
를 하고 난 후 조회를 하게 되면, 1차 캐시가 아니라 DB에 반영된 값을 가져오기 때문에 반영된 1을 가져온다.
결국, 영속성컨텍스트의 1차 캐시의 값을 활용해 일어나는 문제였다.
그러면 저장이후 명시적으로 flush를 하지 않았는데 어떻게 데이터 베이스에 반영되었을까..?
바로 3번, JQPL을 실행했기 때문이다. 메서드 쿼리는 JPQL 기반이다
JPQL이란, 테이블 대상이 아닌 객체를 대상으로 하는 객체 지향 쿼리 언어다. (추가로 찾아보자.)
JPQL의 동작은 위에서 본 것과 다른 점이 있다.
JQPL은 조회시 항상 DB에 접근한 후 영속성 컨텍스트와 비교해 값이 다르다면 영속성 컨텍스트의 값을 가져온다. 따라서 조회쿼리가 정상적으로 날라가도 예상한 값이 안나온다면 이 부분을 생각해봐야한다.
또한 JQPL은 쿼리가 실행될때마다 flush가 일어나 데이터베이스와의 반영이 일어난다.
결국 메서드 쿼리는 JPQL기반이기 때문에 따로 flush를 날리지 않아도 데이터베이스에 반영되었던 것이다.
블로그와 김영한님의 JPA 책을 참고했다. 처음 공부를 시작했을때는 가벼운마음이었는데 내용이 전혀가볍지 않았다. JPA를 사용할때의 가장 큰 장점, 객체지향 개발이 가능해진다는 내용이 조금씩 들어가 있었다.
글도 조금 횡설수설이 된 것같아 이게 맞나 싶지만…
헷갈릴만한 부분은 메서드 쿼리는 flush호출을 하지 않아도 자동 플러시가 된다는 점,
조회쿼리를 늘 날린다는 점이다.
또 위 양방향관계에서 지연로딩과 즉시로딩이야기를 하지 않았는데, 둘중 어떤 전략을 선택하던 테스트 실행결과에는 변함이 없다.
그런데 이 방법 너무 비합리적이라는 생각안드는가?
🥲🥲 아니~ 그러면 이렇게 연관관계에서 연관된 객체콜렉션을 가져올 때마다 이걸 신경써야하는 건 비용이 너무 많이 발생하잖아!그런 분들을 위해 읽을만한 자료 준비 되었습니다 ^_^