본문 바로가기

삽질

JPA, EntityManager와 persist 관련 알게 된 사실

JPA 학습을 하다가 의문이 든 내용과 알게 된 사실을 정리하고자 한다.

JPA의 EntityManager 객체에서 find 메서드를 호출하던 중 의문이 생겼다.🤔

의문이 생겼던 코드를 살펴보자. (학습 테스트기에 테스트 코드가 불편해도 이해해주세요..)

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

@DataJpaTest
public class JpaTest {

    @Autowired
    EntityManager em;

    @Test
    void persist() {
        ...
        Member member = new Member();
        member.setName("member");
        em.persist(member);
        em.flush();
        em.clear();
        Member findMember = em.find(Member.class, member.getId());
        ...
    }
}

의문이 생겼던 부분은 em.find(Member.class, member.getId()); 부분이다.

Member member = new Member();로 생성했던 member에는 id를 설정해준 적이 없는데 EntityManager 객체의 find 메서드를 호출할 때는 member 객체에서 getId 메서드를 호출해서 인자로 넣고있다.

'어떻게 가능하지?' 의문이 생겼고 이유를 고민해봤다.

flush 메서드로 DB에 넣어줬기 때문에?
영속성 컨텍스트에 저장되어 있기 때문에?
@DataJpaTest에서 commit을 미리 해줬나?

명쾌한 해답이 나오지 않았다. JPA를 학습한 내용을 토대로 답을 끼워 맞추고 있었다.

그리고 이런 테스트를 작성해봤다.

@DataJpaTest
public class JpaTest {

    @Autowired
    EntityManager em;

    @Test
    void persist2() {
        Member member = new Member();
        member.setName("member");
        em.persist(member);
        System.out.println(member.getId());
    }
}

이번 테스트는 더 간결하고 flush도 없다. 결과는 여전히 1이 출력됐다.
디버그를 돌려봤을 때 @DataJpaTest 때문에 commit이 된 것도 아니었다.

아무리 생각해도 persist 메서드는 db에 저장하는 것이 아니라 영속성 컨텍스트에 저장할 뿐인데 getId 메서드에서 1이 반환되는 게 이해되지 않았다.

결국 구글에 열심히 검색해서 이 글을 통해 궁금증을 명쾌하게 해결했다.
결론적으로 의문점에 출발이 잘못됐었다.

영속성 컨텍스트는 식별자를 기준으로 저장하는데 Memeber 객체의 식별자인 id가 null인 상태로 어떻게 영속성 컨텍스트에 저장될 수 있을까? 라는 의문이 선행됐어야 정상이다.

그렇다면 식별자가 null인데 어떻게 영속성 컨텍스트에 저장할 수 있었을까?

JPA는 persist()가 호출되고 엔티티가 영속성 컨텍스트에 저장될 때 식별자가 필요하다. 이때 엔티티 식별자가 null이라면 다음과 같은 과정을 겪는다.

  1. Entity의 식별자가 GeneratedValue인지 확인한다.
  2. GeneratedValue가 아니라면 식별자가 없으므로 영속성 컨텍스트에 저장 불가. javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: ... 처럼 Exception이 터진다.
  3. GeneratedValue 라면 하이버네이트의 경우는 JDBC3에서 제공하는 API의 Statement.getGeneratedKeys() 메서드를 사용하여 데이터를 저장함과 동시에 생성된 식별자 값을 얻어온다.

당연히 이 경우엔 쓰기 지연은 동작하지 않는다.

이런 사실을 알게 되니 모든 의문이 명쾌하게 해결되었다.

Member 객체의 id는 GeneratedValue기 때문에 persist() 메서드가 호출 됨과 동시에 insert 쿼리를 날려서 db에 저장했고, id 값을 가져와서 영속성 컨텍스트에 저장했기 때문에 member.getId() 메서드가 정상적인 값을 반환할 수 있었던 것이다.

JPA에 대한 지식이 부족하고 얕음을 많이 느꼈다. 더 열심히 학습해서 이번 프로젝트에서 JPA를 잘 알고 사용하자.