Post

엔티티 생명주기와 관리

🎬Intro

엔티티 생명주기와 관리에 대해 알아보기

✅ 영속

엔티티가 영속성 컨텍스트에 저장된 상태

영속이 되는 시점

  • 비영속 엔티티를 EntityManager로 persist
  • 준영속 OR 비영속 엔티티를 EntityManage로 merge
  • EntityManager로 find/JPQL 조회

✅ 준영속

엔티티가 영속성 컨텍스트에서 관리되다가 detached된 상태

준영속이 되는 시점

  • 영속 엔티티를 EntityManager로 detach
  • 트랜잭션이 끝났을 때

✅ 비영속

엔티티가 영속성 컨텍스트에서 한번도 관리가 되지 않은 상태

비영속이 되는 시점

  • 생성자로 엔티티를 만들때

✅ 삭제 예정

엔티티가 영속성 컨텍스트에서 삭제 예정인 상태(DB에서는 아직 삭제되지 않음)

삭제 예정이 되는 시점

  • 영속 엔티티를 EntityManager로 remove

✅ 엔티티의 상태를 관리하는 메서드

persist(), merge(), remove() 3가지가 있다. 이 중 remove()는 간단하니 제외하고 알아본다.

1️⃣ persist()

  • 엔티티를 비영속 -> 영속으로 변환
  • 엔티티에 id가 존재하면, jpa는 이미 영속된 엔티티로 판단하고 EntityExistsException 던짐
테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@DataJpaTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class PersistTest {

  @Autowired
  private EntityManager entityManager;

  @Test
  @DisplayName("persist 성공 테스트")
  void persistTest1() {
    final MyEntity myEntity = new MyEntity("윌슨");
    entityManager.persist(myEntity);
  }

  @Test
  @DisplayName("persist 실패 테스트")
  void persistTest2() {
    final MyEntity myEntity = new MyEntity(1L, "윌슨");
    assertThatThrownBy(() -> entityManager.persist(myEntity)).isInstanceOf(EntityExistsException.class);
  }
}

2️⃣ merge()

merge 메서드의 역할
  • 비영속 -> 영속으로 변환
  • 준영속 -> 영속으로 변환
merge 메서드의 흐름

merge 메서드의 핵심 로직은 Hibernate MergeEventListener의 구현체인 DefaultMergeEventListener에 구현되어 있다. 하지만 해당 로직은 너무 복잡하니, 아래 코드를 통해 merge의 흐름을 분석해본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object merge(Object newEntity) {
  // 엔티티 상태 판단
  EntityState state = getEntityState(newEntity);
  switch (state) {
    case PERSISTENT:
      // 이미 영속 상태 → 그대로 반환
      return newEntity;
    case TRANSIENT: // ID가 null인 모든 엔티티 (비영속 + ID가 null인 준영속)
      // 비영속 OR ID가 null인 준영속 → 영속 (INSERT)
      return insert(newEntity);
    case DETACHED:
      // 비영속 OR 준영속 → 영속 처리 (SELECT + UPDATE)
      return mergeDetached(newEntity);
    // REMOVED 상태면 예외 발생
    case REMOVED:
      throw new IllegalArgumentException();
    default:
      throw new IllegalStateException();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private EntityState getEntityState(Object entity) {
  // 영속성 컨텍스트에 있으면 PERSISTENT (영속)
  if (persistenceContext.contains(entity)) {
    EntityEntry entry = persistenceContext.getEntry(entity);
    if (entry != null && entry.getStatus() == Status.DELETED) {
      return EntityState.REMOVED;  // 삭제 예정
    }
    return EntityState.PERSISTENT;
  }

  Object id = getId(entity);

  // ID가 null이면 TRANSIENT
  if (id == null) {
    return EntityState.TRANSIENT;
  }

  // 나머지는 모두 DETACHED (준영속)
  return EntityState.DETACHED;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Object mergeDetached(Object newEntity) {
  Object id = getId(newEntity);

  // DB에서 id 조회 (SELECT)
  Object oldEntity = findById(id);
  if (oldEntity == null) {
    // DB에 없으면 예외
    throw new OptimisticLockException();
  }

  // DB에 있음 → oldEntity에 newEntity를 덮어씌움 (UPDATE)
  // 준영속 -> 영속
  update(newEntity, oldEntity);
  return oldEntity;
}

결과적으로 merge 메서드는 엔티티의 ID 존재 유무에 따라 다음과 같이 동작한다.

  • 엔티티에 ID가 없음
    • 새로운 엔티티로 판단하여 INSERT
  • 엔티티에 ID가 존재
    • DB에 해당 ID가 존재, 엔티티의 정보가 수정 x SELECT
    • DB에 해당 ID가 존재, 엔티티의 정보가 수정 o SELECT + UPDATE
    • DB에 해당 ID가 없으면, OptimisticLockException 예외
테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
@DataJpaTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class MergeTest {

  @Autowired
  private EntityManager entityManager;

  @Nested
  class NotExistsIdCase {
    @Test
    @DisplayName("새로운 엔티티로 판단하여 INSERT")
    void mergeTest1() {
      // 비영속 -> 영속
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.merge(myEntity);
      entityManager.flush();
    }

    @Test
    @DisplayName("새로운 엔티티로 판단하여 INSERT")
    void mergeTest2() {
      // 비영속 -> 영속
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.persist(myEntity);
      entityManager.flush();

      // 영속 -> 준영속
      entityManager.detach(myEntity);

      // id 값을 null로
      myEntity.setId(null);

      System.out.println("------ INSERT ------");
      // 준영속 -> 영속
      entityManager.merge(myEntity);
      entityManager.flush();
    }
  }

  @Nested
  class ExistsIdCase {

    @Test
    @DisplayName("비영속_DB에 해당 ID가 존재, 엔티티의 정보가 수정 x SELECT")
    void mergeTest1() {
      // id가 1L인 데이터 db에 준비
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.persist(myEntity);
      entityManager.flush();
      entityManager.clear();

      // 비영속인데 id 값이 db에 존재
      System.out.println("------ SELECT ------");
      final MyEntity newMyEntity = new MyEntity(1L, "윌슨");
      entityManager.merge(newMyEntity);
      entityManager.flush();
    }

    @Test
    @DisplayName("준영속_DB에 해당 ID가 존재, 엔티티의 정보가 수정 x SELECT")
    void mergeTest2() {
      // id가 1L인 데이터 db에 준비
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.persist(myEntity);
      entityManager.flush();
      entityManager.clear();

      System.out.println("------ SELECT ------");
      final MyEntity savedEntity = entityManager.find(MyEntity.class, myEntity.getId());
      entityManager.detach(savedEntity);
      savedEntity.setId(1L);

      // 준영속 인데 id 값이 db에 존재
      System.out.println("------ SELECT ------");
      entityManager.merge(savedEntity);
      entityManager.flush();
    }

    @Test
    @DisplayName("비영속_DB에 해당 ID가 존재, 엔티티의 정보가 수정 o SELECT + UPDATE")
    void mergeTest3() {
      // id가 1L인 데이터 db에 준비
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.persist(myEntity);
      entityManager.flush();
      entityManager.clear();

      // 비영속인데 id 값이 db에 존재
      System.out.println("------ SELECT + UPDATE ------");
      final MyEntity newMyEntity = new MyEntity(1L, "wilson");
      entityManager.merge(newMyEntity);
      entityManager.flush();
    }

    @Test
    @DisplayName("준영속_DB에 해당 ID가 존재, 엔티티의 정보가 수정 o SELECT + UPDATE")
    void mergeTest4() {
      // id가 1L인 데이터 db에 준비
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.persist(myEntity);
      entityManager.flush();
      entityManager.clear();

      System.out.println("------ SELECT ------");
      final MyEntity savedEntity = entityManager.find(MyEntity.class, myEntity.getId());
      entityManager.detach(savedEntity);
      savedEntity.setId(1L);
      savedEntity.setName("wilson");

      // 준영속 인데 id 값이 db에 존재
      System.out.println("------ SELECT + UPDATE ------");
      entityManager.merge(savedEntity);
      entityManager.flush();
    }

    @Test
    @DisplayName("비영속_DB에 해당 ID가 없으면, OptimisticLockException 예외")
    void mergeTest5() {
      // 비영속 -> 영속
      System.out.println("------ SELECT * 2------");
      final MyEntity myEntity = new MyEntity(100L, "윌슨");
      assertThatThrownBy(() -> entityManager.merge(myEntity)).isInstanceOf(OptimisticLockException.class);
    }

    @Test
    @DisplayName("준영속_DB에 해당 ID가 없으면, OptimisticLockException 예외")
    void mergeTest6() {
      // id가 1L인 데이터 db에 준비
      System.out.println("------ INSERT ------");
      final MyEntity myEntity = new MyEntity("윌슨");
      entityManager.persist(myEntity);
      entityManager.flush();
      entityManager.clear();

      System.out.println("------ SELECT ------");
      final MyEntity savedEntity = entityManager.find(MyEntity.class, myEntity.getId());
      entityManager.detach(savedEntity);
      savedEntity.setId(999L);

      // 준영속 인데 id 값이 db에 존재 X
      System.out.println("------ SELECT * 2------");
      assertThatThrownBy(() -> entityManager.merge(savedEntity)).isInstanceOf(OptimisticLockException.class);
    }
  }
}
  • 여기서 특이한 점은 DB에 id값이 없으면, select 쿼리가 2번 발생한다.
  • 그 이유는 동시성 문제를 위해 Hibernate가 1번 더 조회를 하는 것이라고 한다.

✨ Summary

  • 엔티티 저장은 되도록 persist()를 사용하는 것이 좋다.
  • merge()에서 ID가 있으면 반드시 DB 조회를 수반하므로 새 엔티티 저장 시 불필요한 SELECT가 발생할 수 있다.
  • merge()로 존재하지 않는 ID를 처리하면 동시성 체크를 위해 SELECT가 2번 실행된다.
This post is licensed under CC BY 4.0 by the author.