|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Spring Data JPA - 새로운 Entity 판별" |
| 4 | +description: "How Does Spring Data JPA Determine if an Entity is New?" |
| 5 | +excerpt: "Spring Data JPA 에서 Entity 가 새로운 것인지 판단하는 방법에 대해서 알아보자." |
| 6 | +category: Study |
| 7 | +comments: true |
| 8 | +--- |
| 9 | + |
| 10 | +<div id ="notice--info"> |
| 11 | + |
| 12 | + <p style='margin-top:1em;'> |
| 13 | + <b>🐱 Meow, meow </b> |
| 14 | + </p> |
| 15 | + Spring Data JPA 를 사용하다보면 save() 메서드 호출 시 내부적으로 persist() 를 호출할지, merge() 를 호출할지 결정하게 된다. <br> |
| 16 | + 이 결정은 해당 Entity 가 새로운 Entity 인지 여부에 따라 이루어지는데, Spring Data JPA 는 Entity 가 새로운지 어떻게 판단하는지 알아보자. |
| 17 | + <p style='margin-top:1em;'/> |
| 18 | + |
| 19 | +</div> |
| 20 | + |
| 21 | + |
| 22 | +## 신규 Entity 판단 방식 |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +Spring Data JPA 는 내부적으로 `JpaEntityInformation` 의 `isNew(T entity)` 메서드를 호출해서 판단한다. |
| 27 | + |
| 28 | +<pre class="prettyprint lang-java"> |
| 29 | +@Override |
| 30 | +public boolean isNew(T entity) { |
| 31 | + if (versionAttribute.isEmpty() |
| 32 | + || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) { |
| 33 | + return super.isNew(entity); |
| 34 | + } |
| 35 | + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity); |
| 36 | + return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true); |
| 37 | +} |
| 38 | +</pre> |
| 39 | + |
| 40 | +1) @Version 필드가 있다면 → null 여부로 판단 |
| 41 | +2) @Version 필드가 없다면 → @Id 필드가 null 이거나, primitive 타입인 경우 0인지 확인 |
| 42 | + |
| 43 | +<div id="notice--warning"> |
| 44 | + |
| 45 | + 🏷️ 즉, ID 가 null 이면 <b> 신규 Entity </b> 로 간주하여 <b> persist() </b> 가 호출된다. |
| 46 | + |
| 47 | +</div> |
| 48 | + |
| 49 | +<br> |
| 50 | + |
| 51 | +### 직접 ID 를 지정한 경우의 동작 |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +ID 를 직접 지정하면 JPA 는 해당 Entity 가 이미 존재하는 것으로 판단하여 `merge()` 를 호출한다. |
| 56 | +하지만, Database 에는 존재하지 않는 경우 다음과 같은 문제가 발생하는데, |
| 57 | +* SELECT 쿼리로 존재 여부 확인 |
| 58 | +* 실제 INSERT 가 아닌 **UPDATE** 시도 |
| 59 | +* <span style="color:red"> → 실패하거나 잘못된 데이터 상태 유발!! </span> |
| 60 | + |
| 61 | +이를 해결하기 위한 방법으로는 `Persistable<T>` 인터페이스를 구현하면 된다. |
| 62 | + |
| 63 | +<pre class="prettyprint lang-java"> |
| 64 | +// User.class |
| 65 | + |
| 66 | +@Entity |
| 67 | +@Table(name = "users") |
| 68 | +@EntityListeners(UserEntityListener.class) |
| 69 | +public class User implements Persistable<String> { |
| 70 | + |
| 71 | + @Id |
| 72 | + private String id; |
| 73 | + |
| 74 | + private String name; |
| 75 | + |
| 76 | + private boolean isNew = true; |
| 77 | + |
| 78 | + protected User() { |
| 79 | + |
| 80 | + } |
| 81 | + |
| 82 | + public User(String id, String name) { |
| 83 | + this.id = id; |
| 84 | + this.name = name; |
| 85 | + } |
| 86 | + |
| 87 | + @Override |
| 88 | + public String getId() { |
| 89 | + return this.id; |
| 90 | + } |
| 91 | + |
| 92 | + public void setId(String id) { |
| 93 | + this.id = id; |
| 94 | + } |
| 95 | + |
| 96 | + public String getName() { |
| 97 | + return this.name; |
| 98 | + } |
| 99 | + |
| 100 | + @Override |
| 101 | + public boolean isNew() { |
| 102 | + return this.isNew; |
| 103 | + } |
| 104 | + |
| 105 | + public void setIsNew(boolean isNew) { |
| 106 | + this.isNew = isNew; |
| 107 | + } |
| 108 | + |
| 109 | +} |
| 110 | + |
| 111 | +// UserEntityListener.class |
| 112 | + |
| 113 | +public class UserEntityListener { |
| 114 | + @PostPersist |
| 115 | + @PostLoad |
| 116 | + public void setNotNew(User user) { |
| 117 | + System.out.println("@PostPersist/@PostLoad called"); |
| 118 | + user.setIsNew(false); |
| 119 | + } |
| 120 | + |
| 121 | +} |
| 122 | +</pre> |
| 123 | + |
| 124 | +<br> |
| 125 | + |
| 126 | +### persist() vs merge() |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +|구분|persist()|merge()| |
| 131 | +|--|--|--| |
| 132 | +|동작|새로운 Entity 를 영속성 컨텍스트에 등록|준영속 객체를 병합하여 관리| |
| 133 | +|SELECT 쿼리|❌|✅ 먼저 조회 후 merge| |
| 134 | +|ID 필요 여부|❌|✅| |
| 135 | +|성능|빠름 (직접 INSERT)|느릴수 있다 (SELECT + UPDATE)| |
| 136 | + |
| 137 | +<div id="notice--warning"> |
| 138 | + |
| 139 | + 🏷️ 신규 객체를 merge() 로 처리하면 불필요한 SELECT 쿼리가 발생하고 성능 저하 가능성이 존재한다. |
| 140 | + |
| 141 | +</div> |
| 142 | + |
| 143 | +<br> |
| 144 | + |
| 145 | +### 신규 Entity 판단이 중요한 이유는 무엇일까? |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +Spring Data JPA 의 `SimpleJpaRepository` 는 `save()` 에서 다음과 같이 동작한다. |
| 150 | + |
| 151 | +<pre class="prettyprint lang-java"> |
| 152 | +@Transactional |
| 153 | +public <S extends T> S save(S entity) { |
| 154 | + if (entityInformation.isNew(entity)) { |
| 155 | + entityManager.persist(entity); // INSERT |
| 156 | + } else { |
| 157 | + return entityManager.merge(entity); // SELECT → UPDATE |
| 158 | + } |
| 159 | +} |
| 160 | +</pre> |
| 161 | + |
| 162 | +ID 를 직접 설정했지만 `isNew()` 는 false 가 되어, `merge()` 를 호출하게 되고, |
| 163 | +Database 에는 해당 ID 가 존재하지 않지만, 신규 Entity 임에도 불구하고, |
| 164 | +`SELECT` 후 `UPDATE` 를 하게 되어(Database 조회) <span style="color:red"> 실패 또는 데이터 무결성 오류 </span> 가 발생할 수 있고, 비효율적이다. |
| 165 | + |
| 166 | +<div id="notice--warning"> |
| 167 | + |
| 168 | + 🏷️ 정확한 isNew() 제어는 성능, 정합성, 쿼리 효율성 측면에서 매우 중요하다. |
| 169 | + |
| 170 | +</div> |
| 171 | + |
| 172 | +<br> |
| 173 | + |
| 174 | +### 정리 |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +|상황| 처리방식 | |
| 179 | +|--|------------------------------------------| |
| 180 | +|ID 없거나 null → 신규 Entity | persist() | |
| 181 | +|ID가 존재하지만 실제 DB 에는 없음 | merge() 호출 → 실패 가능성 / 비효율 | |
| 182 | +|ID 를 직접 설정한 신규 Entity| Persistable<T> + isNew() 로 명시 필요 | |
| 183 | + |
| 184 | + |
| 185 | +<br><br> |
| 186 | + |
| 187 | + |
| 188 | +<div id="notice--success"> |
| 189 | + |
| 190 | + <p style='margin-top:1em;'> |
| 191 | + <b> 📗 요약 </b> |
| 192 | + </p> |
| 193 | + 🖐 Spring Data JPA 는 내부적으로 isNew() 를 통해서 신규 Entity 여부를 판단한다. <br> |
| 194 | + 🖐 ID 는 존재하지만 실제 DB 에 없는 경우 SELECT + UPDATE 후 merge 를 하므로 정합성이 떨어질 수 있고, 비효율적이다. <br> |
| 195 | + 🖐 ID 를 직접 설정했을 경우는 Persistable<T> + isNew() 로 명시하는 것이 필요하다. <br> |
| 196 | + |
| 197 | + <p style='margin-top:1em;' /> |
| 198 | + |
| 199 | +</div> |
| 200 | + |
| 201 | + |
| 202 | +<br><br> |
| 203 | + |
| 204 | + |
| 205 | +## Reference |
| 206 | + |
| 207 | +--- |
| 208 | + |
| 209 | +* [Spring Docs - JpaEntityInformation](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/support/JpaEntityInformation.html) |
| 210 | +* [Spring Docs - entity-persistence](https://docs.spring.io/spring-data/jpa/reference/jpa/entity-persistence.html) |
| 211 | + |
| 212 | +<br><br> |
0 commit comments