Transaction 과 영속성 컨텍스트, flush

이광훈·2024년 5월 26일

@Transaction 어노테이션에 따른 결과 차이

@Test
@Transactional
@DisplayName("[createVoteResult] 중복 투표가 아닌데 중복 투표하면 실패")
public void transactionSaveTestSuccess(){

    Restaurant restaurant1 = Restaurant.builder().name("abc").restaurantHash("13").build();
    Restaurant restaurant2 = Restaurant.builder().name("def").restaurantHash("12").build();
		
    this.restaurantRepository.save(restaurant1);
    this.restaurantRepository.save(restaurant2);

		// 레스토랑 엔티티를 만든 후 저장
		
    Vote vote =  Vote.builder()
            .title("Test")
            .email(null)
            .allowDuplicateVote(false)
            .expireAt(LocalDateTime.now().plusHours(2))
            .voteHash("abcdef")
            .build();

    Voter voter = Voter.builder().nickname("abcd").profileImage(1).vote(vote).build();
    
    this.voterRepository.save(voter);
    this.voteRepository.save(vote);

    List<Voter> voters = vote.getVoters();
    System.out.println("voters = " + voters);

}

@Test
// @Transactional
@DisplayName("[createVoteResult] 중복 투표가 아닌데 중복 투표하면 실패")
public void transactionSaveTestFail(){

    Restaurant restaurant1 = Restaurant.builder().name("abc").restaurantHash("13").build();
    Restaurant restaurant2 = Restaurant.builder().name("def").restaurantHash("12").build();
		
    this.restaurantRepository.save(restaurant1);
    this.restaurantRepository.save(restaurant2);

		// 레스토랑 엔티티를 만든 후 저장
		
    Vote vote =  Vote.builder()
            .title("Test")
            .email(null)
            .allowDuplicateVote(false)
            .expireAt(LocalDateTime.now().plusHours(2))
            .voteHash("abcdef")
            .build();

    Voter voter = Voter.builder().nickname("abcd").profileImage(1).vote(vote).build();
    
    this.voterRepository.save(voter);
    this.voteRepository.save(vote);

    List<Voter> voters = vote.getVoters();
    System.out.println("voters = " + voters);

}

org.hibernate.TransientPropertyValueException: 
object references an unsaved transient instance - save the transient instance before flushing : capstone.restaurant.entity.Voter.vote -> capstone.restaurant.entity.Vote
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:368)
  • 위에 두개의 코드가 있다. 첫번째 코드와 두번째 코드의 다른점은 @transactional 어노테이션의 유무 차이 밖에 없다. 하지만 첫번째 코드는 수행에 성공하지만 두번째 코드는 오류를 발생시키면서 실패한다. 오류를 보면,
object references an unsaved transient instance - save the transient instance before flushing 
  • 연관된 인스턴스가 현재 transient (비영속) 상태이므로 현재 인스턴스를 flush 하기전에 먼저 연관된 인스턴스를 save 해야 한다는 뜻이다. 그런데 왜 첫번째 코드에서만 잘 돌아가고 두번째 코드만 오류가 발생할까? 결국 둘 다 저장하는 순서가 voter 를 먼저 저장하고 vote 를 저장하는데?
  • 그러면 첫번째 코드에서는 voter 엔티티가 영속상태라는 뜻일까?

영속성 컨텍스트의 범위

  • 영속성 컨텍스트는 기본적으로 하나의 트랜잭션 범위이다. 여기서 두 코드가 다른점은,
    • 첫번째 코드는 @Transactional 어노테이션에 의해 트랜잭션의 범위가 테스트 코드 전체 실행범위로 늘어난다.
    • 하지만 두번째는 각 쿼리가 트랜잭션 범위이다.
    • 결국 첫번째 코드의 영속성 컨텍스트 범위는 테스트 코드 전체 범위, 두번째 코드의 영속성 컨텍스트는 매 쿼리마다 생성되고 소멸된다.

영속성 컨텍스트의 동작

  • 기본적으로 영속성 컨텍스트는 commit 혹은 flush 가 될때 , 쓰기 지연 sql 저장소의 쿼리들을 DB 에 반영한다. 즉 매번 persist , save 등의 동작을 하더라도 즉시 DB 에 반영되는 것이 아니라 쓰기 지연 SQL 저장소 에 쌓여 있다가 flush 나 commit 이 될때, DB 에 쿼리를 날린다.

  • 첫번째 코드는 영속성 컨텍스트가 테스트 코드 전체 범위이다.

this.voterRepository.save(voter);
this.voteRepository.save(vote);
  • 그러면 먼저 영속성 컨텍스트에 voter 를 저장하면서 voter 가 영속성 컨텍스트에 저장된다. (이때 , 바로 DB 에 반영되지 않는다)
  • 이후 vote 가 영속성 컨텍스트에 저장된다. ( 이때도 vote 가 아직 DB 에 반영되지 않는다)

성공한 코드 쿼리 분석

  • 아래는 성공한 첫번째 코드의 sql 쿼리 실행이다. 쓰기 지연 저장소에 쌓여있던 sql 들이 commit 되는 시점에 DB 에 반영되는 쿼리들을 보자.

Untitled

  • 맨 처음 2개는 레스토랑 entity 를 저장하는 부분이다.
  • 이후 voter 를 저장하는 insert into voter 쿼리가 실행되고, 이어서 vote 를 저장하는 insert into vote 가 실행된다.
  • 그 이후에 다시 update voter set ~ 쿼리가 나간다.
  • 위 경우에는 영속성 컨텍스트가 각 엔티티의 상태를 관리해준다. 따라서 voter 를 먼저 save 하고 vote 를 save 해도 영속성 컨텍스트가 엔티티들을 관리해준다. 하지만 두번째 코드의 경우, 각 저장작업이 독립적으로 이뤄지고 영속성 컨텍스트가 유지되지 않는다. update voter 는 영속성 컨텍스트가 엔티티들의 일관성을 유지하기 위해 작성하는 쿼리이다.

영속성 컨텍스트 clear

@Test
@Transactional
@DisplayName("[createVoteResult] 중복 투표가 아닌데 중복 투표하면 실패")
public void duplicateVoteFailTest1(){

    Restaurant restaurant1 = Restaurant.builder().name("abc").restaurantHash("13").build();
    Restaurant restaurant2 = Restaurant.builder().name("def").restaurantHash("12").build();

    this.restaurantRepository.save(restaurant1);
    this.restaurantRepository.save(restaurant2);

    Vote vote =  Vote.builder()
            .title("Test")
            .email(null)
            .allowDuplicateVote(false)
            .expireAt(LocalDateTime.now().plusHours(2))
            .voteHash("abcdef")
            .build();

    Voter voter = Voter.builder().nickname("abcd").profileImage(1).vote(vote).build();

    this.voteRepository.save(vote);
    this.voterRepository.save(voter);
    
    entityManager.flush();
    entityManager.clear();
    
    Vote vote1 = this.voteRepository.findByVoteHash(vote.getVoteHash());

    for (Voter vote1Voter : vote1.getVoters()) {
        System.out.println("vote1Voter.getNickname() = " + vote1Voter.getNickname());
    }
}
  • 위 코드에서 entityManager 의 flush , clear 작업의 유무에 따라 결과의 차이가 난다.
    • flush 와 clear 를 하지 않은 경우, voter.getNickname 에 아무것도 출력되지 않는다
    • 하지만 flush 와 clear 를 하는 경우, voter.getNickname 에 voter 의 이름이 출력된다.
  • 결과를 보고 생각했을때, jpa 를 사용하는 경우, 연관관계가 직접적으로 맺어지는 시기는 영속성 컨텍스트 내에서가 아니라 해당 결과들이 DB 에 flush 될 때 인듯하다.
    • entityManager 의 flush 와 clear 가 없는 경우, vote1 을 가져오는 과정은 DB 에서 직접 가져오는 것이 아니라 영속성 컨텍스트에서 가져온다. 현재 영속성 컨텍스트에 해당 엔티티들이 존재하기 때문이다.
    • 하지만 만약 flush 와 clear 를 하는 경우, vote1 은 DB 에서 가져오게 된다. 이때는 vote1 이 정상적으로 출력된다.
profile
허허,,,

0개의 댓글