JPA Clear

3 분 소요


지난 JPA Flush 포스트의 테스트 케이스를 만들면서 마주친 현상과 이를 해결할 수 있는 방법에 대해서 정리해보았습니다. 제가 지난 글을 포스팅하는 당시에 잘못 이해한 부분이 있었고, 테스트 케이스에서 원치않는 결과를 얻게 되었습니다.

잘못 이해한 내용 - JPQL은 영속성 컨텍스트에 의해 관리되고 있는 데이터를 고려하지 않고 동작합니다.

JPQL도 영속성 컨텍스트에 의해 관리되는 데이터를 고려하고 동작하는 부분이 있었습니다. JPQL은 데이터를 조회하였을 때 영속성 컨텍스트에 동일 @Id로 관리되는 엔티티가 있다면 조회한 데이터를 버리고 캐싱된 엔티티를 반환합니다.

예상 시나리오

예상한 시나리오는 다음과 같습니다.

  1. FlushModeType.AUTO 옵션 사용
  2. @Id로 조회한 엔티티의 필드 값을 변환 (flush를 통한 업데이트 예상)
  3. JPQL로 동일 ROW 업데이트 (flush 된 데이터를 다시 엎어칠 것으로 예상)
  4. JPQL SELECT 쿼리를 이용한 객체 조회
  5. 이전에 조회한 엔티티와 JQPL로 조회한 객체가 서로 다른 객체로 두 객체가 지닌 값이 다를 것으로 예상 (잘못 이해한 부분)
예상 시나리오 이미지

실제 동작

실제로 동작한 것은 다음과 같습니다.

  1. FlushModeType.AUTO 옵션 사용
  2. @Id로 조회한 엔티티의 필드 값을 변환 (flush를 통한 업데이트 예상)
  3. JPQL로 동일 ROW 업데이트 (flush 된 데이터를 다시 엎어칠 것으로 예상)
  4. JPQL SELECT 쿼리를 이용한 객체 조회
  5. 이전에 조회한 엔티티와 JQPL로 조회한 객체가 서로 같은 객체 (DB에 업데이트한 내용이 조회된 엔티티에 반영되어 있지 않는 현상 발생)
실제 동작 이미지

JPQL 업데이트 후 캐싱된 데이터 조회 테스트

아래와 같은 결과를 확인할 수 있습니다.

  • 테스트 결과를 확인해보면 find 메소드를 통해 조회한 엔티티와 JPQL로 조회한 객체가 동일한 주소를 가짐을 확인할 수 있습니다.
  • JPQL 업데이트 결과가 반영되지 않은 객체가 조회되었음을 확인할 수 있습니다.
package blog.in.action.clear;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;
import javax.persistence.TypedQuery;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import blog.in.action.entity.Member;
import lombok.extern.log4j.Log4j2;

@Log4j2
@SpringBootTest
public class WrongScenarioTest {

    @PersistenceUnit
    private EntityManagerFactory factory;

    @BeforeEach
    private void beforeEach() {
        EntityManager em = factory.createEntityManager();
        try {
            em.getTransaction().begin();
            Member member = em.find(Member.class, "01012341234");
            if (member == null) {
                member = new Member();
                member.setId("01012341234");
                member.setPassword("1234");
                member.setMemberName("Junhyunny");
                member.setMemberEmail("kang3966@naver.com");
                em.persist(member);
            } else {
                em.remove(member);
                member = new Member();
                member.setId("01012341234");
                member.setPassword("1234");
                member.setMemberName("Junhyunny");
                member.setMemberEmail("kang3966@naver.com");
                em.persist(member);
            }
            em.getTransaction().commit();
        } catch (Exception ex) {
            em.getTransaction().rollback();
            log.error("exception occurs", ex);
        } finally {
            em.close();
        }
    }

    @Test
    public void test() {
        EntityManager em = factory.createEntityManager();
        try {

            em.getTransaction().begin();

            // 영속성 컨텍스트에 저장된 엔티티
            Member member = em.find(Member.class, "01012341234");
            List<String> authorities = new ArrayList<>();
            authorities.add("MEMBER");
            member.setAuthorities(authorities);

            String jpql = "update Member m set m.authorities = 'JQPL_MEMBER' where m.id = '01012341234'";
            em.createQuery(jpql).executeUpdate();

            jpql = "select m from Member m where m.id = '01012341234'";
            TypedQuery<Member> query = em.createQuery(jpql, Member.class);
            Member jpqlMember = query.getSingleResult();

            assertTrue(System.identityHashCode(member) == System.identityHashCode(jpqlMember));
            log.info("member 객체의 권한: " + member.getAuthorities());
            log.info("jpqlMember 객체의 권한: " + jpqlMember.getAuthorities());

            em.getTransaction().commit();
        } catch (Exception ex) {
            em.getTransaction().rollback();
            log.error("exception occurs", ex);
        } finally {
            em.close();
        }
    }
}
테스트 결과 로그

JPQL 업데이트 후 반영 데이터 조회 테스트

entityManager.clear() 메소드를 호출하여 영속성 컨텍스트에 캐싱된 객체들을 비워줍니다. 아래와 같은 결과를 확인할 수 있습니다.

  • find 메소드를 통해 조회한 엔티티 주소와 JPQL로 조회한 객체의 주소가 다름을 확인할 수 있습니다.
  • JPQL 업데이트 결과가 반영된 객체가 조회되었음을 확인할 수 있습니다.
package blog.in.action.clear;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;
import javax.persistence.TypedQuery;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import blog.in.action.entity.Member;
import lombok.extern.log4j.Log4j2;

@Log4j2
@SpringBootTest
public class JpaClearTest {

    @PersistenceUnit
    private EntityManagerFactory factory;

    @BeforeEach
    private void beforeEach() {
        EntityManager em = factory.createEntityManager();
        try {
            em.getTransaction().begin();
            Member member = em.find(Member.class, "01012341234");
            if (member == null) {
                member = new Member();
                member.setId("01012341234");
                member.setPassword("1234");
                member.setMemberName("Junhyunny");
                member.setMemberEmail("kang3966@naver.com");
                em.persist(member);
            } else {
                em.remove(member);
                member = new Member();
                member.setId("01012341234");
                member.setPassword("1234");
                member.setMemberName("Junhyunny");
                member.setMemberEmail("kang3966@naver.com");
                em.persist(member);
            }
            em.getTransaction().commit();
        } catch (Exception ex) {
            em.getTransaction().rollback();
            log.error("exception occurs", ex);
        } finally {
            em.close();
        }
    }

    @Test
    public void test() {
        EntityManager em = factory.createEntityManager();
        try {

            em.getTransaction().begin();

            // 영속성 컨텍스트에 저장된 엔티티
            Member member = em.find(Member.class, "01012341234");
            List<String> authorities = new ArrayList<>();
            authorities.add("MEMBER");
            member.setAuthorities(authorities);

            em.clear();

            String jpql = "update Member m set m.authorities = 'JQPL_MEMBER' where m.id = '01012341234'";
            em.createQuery(jpql).executeUpdate();

            jpql = "select m from Member m where m.id = '01012341234'";
            TypedQuery<Member> query = em.createQuery(jpql, Member.class);
            Member jpqlMember = query.getSingleResult();

            assertTrue(System.identityHashCode(member) != System.identityHashCode(jpqlMember));
            log.info("member 객체의 권한: " + member.getAuthorities());
            log.info("jpqlMember 객체의 권한: " + jpqlMember.getAuthorities());

            em.getTransaction().commit();
        } catch (Exception ex) {
            em.getTransaction().rollback();
            log.error("exception occurs", ex);
        } finally {
            em.close();
        }
    }
}
테스트 결과 로그

OPINION

개발자가 기술에 대해 이해도가 낮은 경우 의도치 않은 버그를 유발할 수 있습니다.

네, 그렇습니다. 저의 이야기였습니다. 앞으로도 계속 기술 관련 내용과 발생한 이슈를 해결한 방법들을 포스팅하면서 부족한 내공을 쌓아가도록 하겠습니다. 테스트 코드는 blog-in-action 저장소에서 확인 가능합니다.

REFERENCE

댓글남기기