[JPA] 엔티티의 생명주기와 Spring Data JPA- (1)

2022. 10. 8. 21:00· ORM/JPA
목차
  1. 0. 들어가기 전에
  2. 1. 영속성 컨텍스트(PersistenceContext)
  3. 2. 엔티티의 생명주기
  4. 3. 테스트
  5. 3.1 save() 메소드 관련
  6. 3.2 deleteAll() 메소드 관련
  7. ※ 참조

[JPA] 엔티티의 생명주기와 Spring Data JPA- (1)

0. 들어가기 전에

  • [10분 테코톡] 잉, 페퍼의Spring Data JPA 삽질일지 을 보다가 흥미로운 점이 생겨서 JPA의 영속성 관리 및 기본 메서드에 대해서 알아보려고 한다.

1. 영속성 컨텍스트(PersistenceContext)

  • 엔티티를 영구 저장하는 환경으로 논리적 개념
  • 엔티티 매니저(EntityManger)를 통해 영속성 컨텍스트에 접근
  • 장점
    • 1차 캐시
      • 한 트랜잭션 내에서 공유
      • JPA가 객체를 찾을 때 1차 캐시를 먼저 찾은 후에 없으면 DB에서 확인
      • DB에서 조회를 하고 1차 캐시에 저장 후 반환
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지(Dirty Checking)
      • commit이 발생하면 1차 캐시와 스냅샷을 비교한다
        • 변경 사항이 있으면 쓰기 지연 sql 저장소에 update 쿼리를 생성하고, flush를 통해 db에 반영한다.
        • flush : 쓰기 지연 sql 저장소에 있는 sql을 실행하여 db에 쿼리를 날리는 행위, 1차캐시 삭제x

2. 엔티티의 생명주기

  • 비영속(new/transient)
    • 객체를 생성만 한 상태
  • 영속(managed)
    • 객체를 생성한 상태 + 객체를 저장(persist)한 상태
    • DB에 쿼리를 날리지 않음 -> 트랜잭션이 commit을 해야 쿼리가 나감
  • 준영속(detached)
    • 영속성 컨텍스트가 관리하지 않음
    • 변경감지x
    • detach()
      • 1차 캐시에 해당 객체 지우기 -> 쓰기지연 sql 저장소 지우기
  • 삭제(removed)

3. 테스트

3.1 save() 메소드 관련

@SpringBootTest
class MemberRepositoryTest {

  //...

    private static final Member 홍길동 = new Member("홍길동");

    @DisplayName("저장_후_비교_한다")
    @Test
    void 저장_후_비교_한다() {
        log.info("홍길동이 비영속 상태 인가요? {}", entityInformation.isNew(홍길동));
        log.info("홍길동 ID = {}", 홍길동.getId());

        Member 결과 = memberRepository.save(홍길동);

        assertThat(결과).isEqualTo(홍길동);
    }

    @DisplayName("회원_찾기")
    @Test
    void 찾기() {
        log.info("홍길동이 비영속 상태 인가요? {}", entityInformation.isNew(홍길동));
        log.info("홍길동 ID = {}", 홍길동.getId());

        Member 저장됨 = memberRepository.save(홍길동);
        Optional<Member> 결과 = memberRepository.findById(저장됨.getId());

        assertThat(결과).hasValue(홍길동);
    }

  //...

}
  • 테스트 두 개를 같이 돌리면 실패하게 된다.
  • 테스트 격리가 안되서 발생한 문제도 있지만, 추가로 영속성과 관련된 문제도 존재한다.
    • static으로 선언된 홍길동이 먼저 영속 상태가 되고, 그 객체를 다른 테스트에서도 사용해서 문제가 발생한다.
    • 이 문제를 해결할 방법을 찾으려면 save() 메서드에 대한 이해가 필요하다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

  //...

  @Transactional
    @Override
    public <S extends T> S save(S entity) {

        Assert.notNull(entity, "Entity must not be null.");

        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }

  //...

}
  • save() 메서드를 보면 isNew()메서드로 영속상태인지 확인하고, 비영속상태이면 persist()를 실행하고, 아니면 merge()를 실행한다.
    • persist()는 null인 Id를 새로 발급하는 과정이고, merge()는 기존 아이디를 가지고 있어도 복사본을 만들어 새로운 Id로 발급하는 과정이다. 이후에는 모두 영속상태가 된다.
    • 주의할 점은 merge()는 객체를 교환하는 형식이기 때문에 일부분만 변경을 시도하면, 변경이 안된 부분이 null 값으로 들어갈 위험이 있다.
  • 따라서 테스트를 실행하면 persist와 merge가 둘 다 발생해서 동일성이 보장이 안된다.
  • 그러므로 동일성을 보장하기 위해서, 객체를 일일이 선언해서 쓰는게 더 낫다.

3.2 deleteAll() 메소드 관련

  • name을 unique로 설정하고 테스트를 진행한다.
  • deleteAndSave() 메서드는 먼저 deleteAll()을 실행하고, saveAll(List members)을 실행한다.
@DisplayName("삭제 후 저장 테스트")
    @Test
    void 삭제_후_저장_테스트() {
        //given
        memberRepository.save(new Member("홍길동"));
        memberRepository.save(new Member("김철수"));

        //when
        //then
        assertThatThrownBy(() -> memberService.deleteAndSave(List.of(
                new Member("홍길동"),
                new Member("김철수"),
                new Member("이영희")
        ))).doesNotThrowAnyException();
    }
}
  • 위 테스트를 실행하면 log는 다음과 같이 나오며, UniqueViolation이 발생하며 실패한다.
    • 왜냐하면 JPA에서는, 우리의 생각처럼 DB에 있는 내용을 먼저 지우고, 바로 저장하지 않는다.
      • deleteAll() 먼저 db에서 값들을 모두 찾아 1차 캐시에 저장하고, 쓰기지연 sql 저장소에 delete 쿼리를 저장하고 flush()가 발생하면 그 때 실행된다.
        • 여기에서 위 문제가 발생하는데, 쓰기지연 저장소에 쿼리들이 존재하지만 아직 실행이 되지 않은 채로 save 메서드를 호출했기 때문에 문제가 발생했다.
  • 그렇다면 이 문제는 어떻게 해결 할 수 있을까?
    • 유튜브 영상에서는 아래와 같은 방법을 사용한다.
    • save()를 하기전에 flush()를 사용한다.
      1. JPQL을 사용할 때 flush가 사용된다.
      2. 직접 flush()를 사용한다.
      3. commit을 활용한다.
  • 하지만 위 방법보다 좋은 방법이 존재한다.
  • deleteAllInBatch() 메서드를 사용하는 것이다.

< deleteAllInBatch() >

/*
     * (non-Javadoc)
     * @see org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java.lang.Iterable)
     */
    @Override
    @Transactional
    public void deleteAllInBatch(Iterable<T> entities) {

        Assert.notNull(entities, "Entities must not be null!");

        if (!entities.iterator().hasNext()) {
            return;
        }

        applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
                .executeUpdate();
    }
  • deleteAll()은 쓰기지연 때문에 문제가 발생하며, 또한 iterator로 delete() 메서드를 호출하기 때문에 삭제할 객체 수대로 쿼리가 만들어서 보내진다.
  • 하지만 deleteAllInBatch() 위의 내용과 같이 applyAndBind를 통해 쿼리를 만들고 executeUpdate() 형식으로 쿼리를 직접 실행하므로, 쓰기 지연 문제도 해결되며, 다수의 객체를 삭제해도 쿼리를 한 개만 삭제하므로 언급했던 문제는 물론, 성능 문제도 해결된다.

※ 참조

  • [10분 테코톡] 잉, 페퍼의Spring Data JPA 삽질일지
  • jpa gitbooks
  • 자바 ORM 표준 JPA 프로그래밍 - 기본편
  • JPA deleteAll()후 save()를 하면 EntityExistsException 에러가 발생하는 이유
저작자표시 (새창열림)

'ORM > JPA' 카테고리의 다른 글

[JPA] 영속성 전이와 고아 객체  (0) 2022.07.18
자바 ORM 표준 JPA 프로그래밍 - 객체지향 쿼리 언어  (0) 2021.08.22
자바 ORM 표준 JPA 프로그래밍 - 값 타입  (0) 2021.08.21
자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계 관리  (0) 2021.08.19
자바 ORM 표준 JPA 프로그래밍 - 고급 매핑  (0) 2021.08.16
  1. 0. 들어가기 전에
  2. 1. 영속성 컨텍스트(PersistenceContext)
  3. 2. 엔티티의 생명주기
  4. 3. 테스트
  5. 3.1 save() 메소드 관련
  6. 3.2 deleteAll() 메소드 관련
  7. ※ 참조
'ORM/JPA' 카테고리의 다른 글
  • [JPA] 영속성 전이와 고아 객체
  • 자바 ORM 표준 JPA 프로그래밍 - 객체지향 쿼리 언어
  • 자바 ORM 표준 JPA 프로그래밍 - 값 타입
  • 자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계 관리
쿠엔크
쿠엔크
우아한테크코스 5기 BE 에단 Github : https://github.com/cookienc
쿠엔크
기러기는 기록기록
쿠엔크
전체
오늘
어제
  • 분류 전체보기 (132)
    • CS (46)
      • [OS] (12)
      • [NETWORK] (10)
      • [DATABASE] (11)
      • [BASIC CONCEPT] (1)
      • [DATA STRUCTURE] (7)
      • [ALGORITHM] (5)
    • LANGUAGE (17)
      • [JAVA] (17)
    • DESIGN_PATTERN (2)
    • FRAMEWORK (18)
      • [SPRING] (18)
    • ORM (11)
      • JPA (11)
    • AWS (7)
    • BOOK (10)
      • [자바 웹 개발 워크북] (3)
      • [이펙티브 자바] (7)
    • 개발 (19)
      • [오류] (7)
      • [고민] (1)
      • [우테코] (10)
      • [iTracker] (1)
    • Tip (1)
      • [Plugins] (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • ArgumentResolver
  • 알고리즘
  • 오류
  • Spring
  • 네트워크
  • HTTP
  • 운영체제
  • java
  • aws
  • 개념
  • 가비아
  • JPA
  • CORS
  • Effective Java
  • 데이터베이스
  • 스프링
  • 디자인 패턴
  • 자바 웹 개발 워크북
  • 자료구조
  • JVM

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
쿠엔크
[JPA] 엔티티의 생명주기와 Spring Data JPA- (1)

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.