엔티티 생명주기와 영속성 컨텍스트 ps.entityManager그게 뭔데 왜 너만 있으면 다 해결되는 건데

코린이서현이·2024년 7월 24일
9

스프링

목록 보기
2/4
post-thumbnail
👩‍💻 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으로 오류가 난다. 
}

엔티티 매니저의 각 메서드, 엔티티의 생명주기, 영속성컨텍스트에 대해서 이야기해봐야한다. 이 부분들은 긴밀하게 연결이 되어있어서 한번에 이해가 어려울 수 있다. 일단 잘 읽어보고 헷갈리는 부분은 다시 읽어보자!

짧게 EntityManeger가 어떤 역할을 하는지 알아보자.

EntityManager는 영속성 컨텍스트를 관리한다.

  • entityManager.flush(); : 영속성 컨텍스트의 변경 내용을 DB에 동기화 시킨다. 이를 위해 수정된 Entity를 감지하고 쓰기 지연 SQL 저장소에 옮긴 후 쓰기 지연 SQL 저장소에 Query 들을 DB에 전송한다.
  • entityManager.clear(); : 영속성 컨텍스트를 지우고 준영속 상태로 변경한다.

오… 다 공부하고 나니 위의 세 문장에 엄청난 개념이 축약되어있는 것이 보인다.

위에 등장한 개념을 살펴보면 크게 🔖영속성 컨텍스트, 🔖 준영속 상태 두가지로 확인할 수 있다.

영속성 컨텍스트부터 알아봅시다!

  • 엔티티의 생명주기와 긴밀한 관계를 가지며, 이 영속성 컨텍스트를 이용해 쓰기 지연, 더티체킹등을 할 수 있다.
  • 논리적인 개념으로 엔티티 매니저를 통해 접근할 수 있다. 여러 엔티티가 같은 영속성 컨텍스트에 접근할 수도 있다.
  • JPA는 현재 영속성 컨텍스트에 존재하는 목록을 알려주지는 않지만 EntityManager.contains(entity)를 사용하여 해당 엔티티가 영속성 객체에 존재하는지 확인할 수 있다.

1차 캐시

  • 영속성 컨텍스트에 존재하는 캐시로, 영속상태의 엔티티가 저장되는 공간이다. Map 형태로 저장되는데, 이때 엔티티의 키는 @Id 칼럼의 값이다.

쓰기 지연 SQL 저장소

  • 엔티티의 변경사항을 반영하는 여러 쿼리들을 저장하는 공간이다. delete, insert 등등이 있겠죠?

아직 영속성 컨텍스트를 어떻게 활용하는지 아직 다루지 않았다. 그전에 엔티티의 생명주기에 대해서 배워보자.

엔티티의 생명 주기

영속성은 무엇이고 준영속은 무엇인지 알아보자

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속(managed) : 영속성 컨텍스트에 관리되는 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(remove) : 삭제된 상태

엔티티 매니저의 엔티티 상태바꾸기!

비영속 상태의 자바 객체

  • 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의 실제 구현을 확인하자.

  • 해당 엔티티가 새로운 엔티티일 경우 ( = 식별자가 null) 일 경우,  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을 사용후 테스트를 통과했다는 의문이 들 것이다. (그런 의문이 생겼다면 정말 당신 대단해)

JPA가 영속성 컨텍스트를 활용하는 방법

조회 시

  • 해당 객체가 1차 캐시에 존재한다면 조회쿼리를 실행하지 않고, 1차 캐시에 있는 내용을 반환한다.
  • 해당 객체가 1차 캐시에 존재하지 않을 때 조회쿼리를 실행하고, 해당 객체를 1차 캐시에 저장해 영속상태로 만든다. 반환한다.

수정 시

  • 영속성 컨텍스트에 객체가 처음 올라갔을 때의 상태를 저장해두고(스냅샷), 커밋 지점까지 기다렸다가 엔티티 상태와 비교해 변경 수준을 감지하고, 이를 데이터 베이스에 반영한다. (쓰기 지연)

이때 준 영속상태의 객체들은 비교할 수 없어 데이터 베이스에 변경되지 않는다.

JPA가 영속성 컨텍스트를 두는 이유

DB 접근 비용을 줄일 수 있다.

더티 체킹을 할 수 있다. 는 장점이 있다.

DB에 접근하는 건 비용이 많이 든다. (진짜로) 컴퓨터 구조상 차이가 나는데 이건 컴구시간이나 운체시간에…

만약 준영속상태에 있는 객체를 조회한다면 JPA는 어떻게 동작할까?

준영속상태, 즉 영속성 컨텍스트에 없기 때문에 조회쿼리를 날려, 조회를 한 후 가져올 것이다.

내 문제 분석

그렇다면 다시 내 문제의 코드를 살펴보자

    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 동작에 1차 캐시와 쓰기 지연 SQL 저장소를 어떻게 활용하나

  1. 스냅샷을 활용해 변경감지(Dirty Checking)를 통해 수정된 엔티티를 찾는다.
  2. 수정된 엔티티에 대한 쿼리를 만들어 쓰기 지연 SQL에 등록한다.
  3. 쓰기 지연 SQL 저장소의 쿼리를 데이터 베이스에 전송한다.

그러면 저장이후 명시적으로 flush를 하지 않았는데 어떻게 데이터 베이스에 반영되었을까..?

flush가 일어나는 세 가지 상황

  1. EntityManager의 flush 호출
  2. 트랜잭션 커밋시
  3. JPQL 쿼리 실행시

바로 3번, JQPL을 실행했기 때문이다. 메서드 쿼리는 JPQL 기반이다

JPQL의 동작

JPQL이란, 테이블 대상이 아닌 객체를 대상으로 하는 객체 지향 쿼리 언어다. (추가로 찾아보자.)

JPQL의 동작은 위에서 본 것과 다른 점이 있다.

JQPL은 조회시 항상 DB에 접근한 후 영속성 컨텍스트와 비교해 값이 다르다면 영속성 컨텍스트의 값을 가져온다. 따라서 조회쿼리가 정상적으로 날라가도 예상한 값이 안나온다면 이 부분을 생각해봐야한다.

또한 JQPL은 쿼리가 실행될때마다 flush가 일어나 데이터베이스와의 반영이 일어난다.

결국 메서드 쿼리는 JPQL기반이기 때문에 따로 flush를 날리지 않아도 데이터베이스에 반영되었던 것이다.

진짜 마무리

블로그와 김영한님의 JPA 책을 참고했다. 처음 공부를 시작했을때는 가벼운마음이었는데 내용이 전혀가볍지 않았다. JPA를 사용할때의 가장 큰 장점, 객체지향 개발이 가능해진다는 내용이 조금씩 들어가 있었다.

글도 조금 횡설수설이 된 것같아 이게 맞나 싶지만…

헷갈릴만한 부분은 메서드 쿼리는 flush호출을 하지 않아도 자동 플러시가 된다는 점,
조회쿼리를 늘 날린다는 점이다.

또 위 양방향관계에서 지연로딩과 즉시로딩이야기를 하지 않았는데, 둘중 어떤 전략을 선택하던 테스트 실행결과에는 변함이 없다.

그런데 이 방법 너무 비합리적이라는 생각안드는가?
🥲🥲 아니~ 그러면 이렇게 연관관계에서 연관된 객체콜렉션을 가져올 때마다 이걸 신경써야하는 건 비용이 너무 많이 발생하잖아!

그런 분들을 위해 읽을만한 자료 준비 되었습니다 ^_^

🔗 이전 글_ 양방향 관계에서 편의메서드를 써야하는 이유


참고자료

[JPA] 플러시(Flush)란 - Heee's Development Blog

[JPA] commit, flush, Entity Manager의 clear()와 close()에서 궁금한 부분들 탐구 + 데이터 삭제 및 수정 시 1차 캐시에서 발생하는 현상 + 준영속과 비영속의 차이점

[JPA] 준영속

[JPA] 영속성 컨텍스트(Persistence Context)란? - 개넘 정리 및 사용법

profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글