[영상후기] [10분 테코톡] 잉, 페퍼의Spring Data JPA 삽질일지

박철현·2023년 4월 5일
0

영상후기

목록 보기
152/160

movie

Entity 생명 주기

  • 비영속 : 영속성 컨텍스트 들어가기 전
  • 영속 : 영속성 컨텍스트 내부
  • 준영속 : 영속성 컨텍스트에 있다가 제외된 것
  • 삭제 : 객체를 삭제한 상태(삭제) -> 영속성 컨텍스트에서 삭제하고 DB에서 삭제
    • flush가 되야 DB에서 삭제

save() 분기처리 : save 메소드

	@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); // 준영속 -> 영속
		}
	}
  • entity 객체와 merge 결과로 반환되는 entity 객체가 다르다
    • merge() 메서드의 결과 새로운 id값으로 새로운 entity 생성
    • 영속성 컨텍스트에서는 동일성을 id로 비교하기 때문에 두 엔티티는 다른 엔티티
  • save 메서드의 결과로 동일성이 보장된 엔티티로 동일성 비교가 권장됨
@Test
void saveAndCompare() {
	Crew pepper = new Crew("페퍼");
    Crew crew = crewRepository.save(pepper);
    
    assertThat(crew).isEqualTo(pepper);
}

@Test
void find() {
	Crew crew = crewRepository.save(new Crew("페퍼"));
    Optional<Crew> findCrew = crewRepository.findById(crew.getId());
    
    assertThat(findCrew).hasValue(crew);
}

삭제 엔티티

  • ID값이 있고 영속성 컨텍스트와 연결되어 있으며 DB에서 제거되도록 예약
    • 영속성 컨텍스트에 연결은 되어있으나 현재 값을 빼올 순 없음
@Test
void removed() {
	Crew pepper = crewRepository.save(new Crew("페퍼"));
    Long pepperId = pepper.getId();
    
    crewRepository.deletedById(pepperId);
    
    assertThat(crewRepository.findById(pepperId));
}

  • findById 부분의 select query가 안나가서 아직 영속성 컨텍스트 안에 살아있나?
@Test
void removed() {
	Crew pepper = crewRepository.save(new Crew("페퍼"));
    Long pepperId = pepper.getId();
    
    crewRepository.deletedById(pepperId);
    
    // 예외 발생 : 값은 빼올 수 없다
    assertThat(crewRepository.findById(pepperId))
    .isPresent();
}
  • 값이 비워있다는 에러 발생
    • 조회는 성공 하지만 값은 빼올 수 없는 상태임을 확인함

문제상황 2

  • 1차 캐시 : 엔티티를 DB에 저장(save) 명령 들어왔을때, 1차 캐시 먼저 저장
    • 단, ID값이 없는 엔티티의 경우 DB에 먼저 저장하고 1차 캐시로 감
    • ID 값이 있는 경우 : 1차 캐시에 넣어둔 다음 쓰기 지연 SQL저장소 저장
      • flush 명령을 통해 DB 반영
  • deleteAll : 내부적으로 findAll 먼저 수행

    • findAll : DB에서 1차 캐시로 모든 엔티티 넣음
    • 쓰기 지연 SQL 저장소 저장(delete 쿼리들)
    • flush가 되기 전에 삭제되는 데이터와 중복된 데이터를 save를 할 경우 유니크 오류가 뜸
      • save할때 id가 없기 때문에 DB에 먼저 저장을 하려고 시도
      • 하지만 아직 delete쿼리가 실제 DB에 반영되지 않아 DB에 적재된 상태에서, 유니크 속성으로 지정한 중복 데이터를 삽입하려고 하니 유니크 오류가 발생
  • 해결책 1 : deleteAll 후 바로 flush 직접 호출

    • 테스트나 특정 환경에서만 주로 사용
  • 헤결책 2 : JPQL 쿼리 사용

    • 쿼리문마다 flush
  • 해결책 3 : 트랜잭션 커밋

    • delete 후 커밋, save 후 커밋
  • flush는 언제 발생할까?

    • 강제로 flush 명령을 할 때
    • JPQL 쿼리가 실행 될 때
    • 트랜잭션 커밋 시

문제상황 3 : 정합성 보장 x

@Service
@RequiredArgsConstructor
public class CrewService {
	private final CrewRepository crewRepository;
    
    public void updateAllAge() {
    	List<Crew> crews = crewRepository.findAll();
        for (Crew crew : crews) {
        	crew.updateAge();
        }
    }
}
  • 하나씩 update 쿼리 생성하는 것이 비효율 적이라 생각하여 하나의 JPQL문 생성
public interface CrewRepository extends JpaRepository<Crew, Long> {
	@Modifying
    @Query("update Crew c set c.age = c.age + 1")
    int increaseAllAge();
}
@Test
void increaseAgeOfEveryone() {
	Crew 마루 = crewRepository.save(new Crew("마루", 24));
    Crew= crewRepository.save(new Crew("잉", 20));
    Crew 페퍼 = crewRepository.save(new Crew("페퍼", 26));
    Crew 록바 = crewRepository.save(new Crew("록바", 26));
    
    crewRopository.increaseAllAge();
    System.out.println("count = " + count);
    
    assertAll(
    	() -> assertThat(마루.getAge()).isEqualTo(25),
        () -> assertThat(.getAge()).isEqualTo(21),
        () -> assertThat(페퍼.getAge()).isEqualTo(27),
        () -> assertThat(록바.getAge()).isEqualTo(27)
   );
}

변경감지(Dirty Checking)

  • 1차 캐시에 저장할때 DB에 없으면 id를 받아오고 스냅샷 형태로 1차 캐시에 저장
  • 쓰기 지연 : 변경 쿼리문 저장, findAll() 같이 데이터베이스 직접 찌르는 메서드 호출될 때 flush 발생하고 쿼리가 날아가고, findAll이 호출됨
    • JPQL 쿼리를 바로 쓸 경우, flush가 발생하기 때문에 쓰기 지연 SQL 저장소에 있는 쿼리가 flush 발생
    • 항상 발생하는 것은 아니고, 날라온 JPQL 쿼리와 관련된 SQL 저장소에 있는 쿼리만 flush 발생
      • 쓰기지연 저장소에 crew와 team이 있고, JPQL 쿼리가 crew만 사용한다 했을때
      • 쓰기지연 저장소에 저장된 crew에 대한 쿼리만 날라감

엔티티의 상태?

  • JPQL은 바로 DB에 flush를 하기 때문에 DB 데이터만 변경
  • Entity의 데이터는 변경되지 않기에 기존 테스트가 실패함

해결1

  • JPQL 사용하지 않고 마음 편하게 쓰기 지연 사용하면 됨
@Service
@RequiredArgsConstructor
public class CrewService {
	private final CrewRepository crewRepository;
    
    public void updateAllAge() {
    	List<Crew> crews = crewRepository.findAll();
        for(Crew crew : crews) {
        	crew.updateAge();
        }
}

해결2

  • entityManager를 비워주고 findById로 가져오면 성공
@Test
void increaseAgeOfEveryone() {
	Crew 마루 = crewRepository.save(new Crew("마루", 24));
    Crew= crewRepository.save(new Crew("잉", 20));
    Crew 페퍼 = crewRepository.save(new Crew("페퍼", 26));
    Crew 록바 = crewRepository.save(new Crew("록바", 26));
    
    crewRopository.increaseAllAge();
    System.out.println("count = " + count);
    
    entityManager.flush();
    entityManager.clear();
    
    assertAll(
			() -> assertThat(crewRepository.findById(마루.getId()).get().getAge()).isEqualTo(25),
			() -> assertThat(crewRepository.findById(.getId()).get().getAge()).isEqualTo(21),
			() -> assertThat(crewRepository.findById(페퍼.getId()).get().getAge()).isEqualTo(27),
			() -> assertThat(crewRepository.findById(록바.getId()).get().getAge()).isEqualTo(27)
		);
}

해결3

  • clearAutomatically 옵션으로 메서드 실행 후 영속성 컨텍스트 clear 가능
public interface CrewRepository extends JpaRepository<Crew, Long> {
	@Modifying(clearAutomatically = true)
    @Query("update Crew c set c.age = c.age + 1")
    int increaseAllAge();
}
	@Test
	@DisplayName("Test")
	void t001() throws Exception {
		Crew 마루 = crewRepository.save(new Crew("마루", 24));
		Crew= crewRepository.save(new Crew("잉", 20));
		Crew 페퍼 = crewRepository.save(new Crew("페퍼", 26));
		Crew 록바 = crewRepository.save(new Crew("록바", 26));

		int count = crewRepository.increaseAllAge();
		System.out.println("count = " + count);
		assertAll(
			() -> assertThat(crewRepository.findById(마루.getId()).get().getAge()).isEqualTo(25),
			() -> assertThat(crewRepository.findById(.getId()).get().getAge()).isEqualTo(21),
			() -> assertThat(crewRepository.findById(페퍼.getId()).get().getAge()).isEqualTo(27),
			() -> assertThat(crewRepository.findById(록바.getId()).get().getAge()).isEqualTo(27)
		);
	}

프록시 주의할 점

  • 프록시, 실제 엔티티는 독립성 보장이 안되기 때문에 getClass() 부분을 없애 class가 같다는 부분을 없애서 override 하는 것이 좋음
profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글

관련 채용 정보