DevLog ๐ถ
[JPA] JPA Auditing๊ณผ em.merge()์ ๋์ ์๋ฆฌ ์ดํด๋ณด๊ธฐ - ํ ์คํธ์์ id ๊ฐ์ ์ฃผ์ํ์ ๋ณธ๋ฌธ
[JPA] JPA Auditing๊ณผ em.merge()์ ๋์ ์๋ฆฌ ์ดํด๋ณด๊ธฐ - ํ ์คํธ์์ id ๊ฐ์ ์ฃผ์ํ์
dolmeng2 2023. 9. 10. 13:44๐ฑ ๋ค์ด๊ฐ๊ธฐ ์
์๋ ์ ์ผ๋ ๊ธ์ธ๋ฐ, ์์ฆ ๋ธ๋ก๊ทธ๋ฅผ ๋๋ฌด ์ ์ด ๊ฒ ๊ฐ์์ ์ค๋๋ง์ ์ฌ๋ฆฌ๋ ๊ธ ๐ถ
ํผ์ ๊ณต๋ถํ ๊ฒธ ์์ฑํ ๊ธ์ด์์ด์ ๋ค์ ์ค๋ช ์ด ๋ถ์กฑํฉ๋๋ค...!
๐ฑ ์ํฐํฐ ์๊ฐ
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
User ํด๋์ค๋ ์ฌ์ฉ์์ ๋ํ ์ํฐํฐ์ด๋ฉฐ, id ์ ๋ต์ IDENTITY์ด๋ค.
์ด๋, createdAt, updatedAt์ ๋ํด์ JPA auditing ๊ธฐ๋ฅ์ ์ฌ์ฉํ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ฐ๋ฆฌ๋ ์ฌ์ฉ์ ์ํฐํฐ์ ๋ํ ์ ์ฅ์ ์ํด์ ๊ฐ๋จํ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณผ ์์ ์ด๋ค.
public class UserTest {
public static final User JAVAJIGI = new User(1L, "javajigi", "password", "name", "javajigi@slipp.net");
public static final User SANJIGI = new User("sanjigi", "password", "name", "sanjigi@slipp.net");
}
์ ํด๋์ค๋ ํ ์คํธ์์ ์ฌ์ฉํ ๊ฐ๋จํ ํฝ์ค์ฒ์ด๋ค.
์ฌ๊ธฐ์ JAVAJIGI์ ๋ํด์๋ id ๊ฐ์ด ์ด๊ธฐํ๊ฐ ๋์ด ์๊ณ , SANJIGI ๋ณ์์ ๋ํด์๋ id ๊ฐ์ด ์ด๊ธฐํ ๋์ด ์์ง ์์ ์ํ์์ ์ ์ํ์.
๐ฑ ๋ฌธ์ ์ํฉ
@Test
@DisplayName("์ ์ ๋ฅผ ์ ์ฅํ๋ค.")
void save1() {
final User actualUser = userRepository.save(UserTest.JAVAJIGI);
assertThat(actualUser).usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(UserTest.JAVAJIGI);
}
@Test
@DisplayName("์ ์ ๋ฅผ ์ ์ฅํ๋ค.")
void save2() {
final User actualUser = userRepository.save(UserTest.SANJIGI);
assertThat(actualUser).usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(UserTest.SANJIGI);
}
์ฌ์ฉ์๋ฅผ ์ ์ฅํ๋ 2๊ฐ์ ํ ์คํธ๋ฅผ ์์ฑํ์๋ค.
๋จ์ํ ์ฌ์ฉ์๋ฅผ ์ ์ฅํ๋ ํ ์คํธ์์๋, ์ฒซ ๋ฒ์งธ ๋ฉ์๋์ ํ ์คํธ์์๋ ์๋์ ๊ฐ์ด ์ค๋ฅ๊ฐ ๋ฐ์ํ์๋ค.
์ค๋ฅ ์ํฉ์ ๋ณด๋ฉด, User ์ํฐํฐ์ createdAt, updatedAt์ ๊ฐ์ด ์ฑ์์ ธ ์์ง ์์ ๋ฐ์ํ ๊ฒ์ ๋ณผ ์ ์๋ค.
save()๋ฅผ ์งํํ๊ฒ ๋๋ฉด ์์์ฑ ์ปจํ
์คํธ์ ๊ด๋ฆฌ์ ์ํด์ UserTest์ JAVAJIGI ๋ณ์์ actualUser๊ฐ ๊ฐ์ ๊ฐ์ฒด๋ฅผ ๋ฐ๋ผ๋ณด๊ณ ์์ด์ auditing์ ์ํด User ์ํฐํฐ์ ๊ฐ์ด ์
๋ฐ์ดํธ๊ฐ ๋๋ฉด JAVAJIGI ๋ณ์์ ๊ฐ๋ ์
๋ฐ์ดํธ๊ฐ ๋ ์ค ์์๋๋ฐ ๊ทธ๋ ์ง ์์๋ ๊ฒ์ด๋ค.
๐ฑ JPA Auditing์ ๋์ ์๋ฆฌ ์ดํด๋ณด๊ธฐ
์ฒ์์๋ Auditing์ ๋ํด ๋ฌธ์ ๋ผ๊ณ ์๊ฐ์ด ๋ค์ด์, ๋๋ฒ๊น ์ ํตํด ์ธ์ ๊ฐ์ด ์ฑ์์ง๋์ง ์ดํด๋ณด๊ธฐ๋ก ํ๋ค.
๋จผ์ , Auditing ์ ๋์ํ๋ AuditingEntityListener ํด๋์ค์ touchForCreate() ๋ฉ์๋๋ฅผ ํ์ธํด๋ณด์.
์ ๋ฉ์๋์์ ์ธ์๋ก target ๊ฐ์ JAVAJIGI๊ฐ ๋ค์ด์จ ๊ฒ์ ๋ณผ ์ ์๋ค.
์์ง auditing์ด ๋์ํ์ง ์์๊ธฐ ๋๋ฌธ์ createdAt, updatedAt ๋ชจ๋ null ๊ฐ์ผ๋ก ์ฑ์์ง ๊ฒ์ ๋ณผ ์ ์๋ค.
object.markCreated() ๋ฉ์๋๋ก ํ๊ณ ๋ค์ด๊ฐ๋ณด์.
์ฒซ ๋ฒ์งธ ๋ผ์ธ์์ target์ ๊ฐ์ผ๋ก JAVAJIGI๊ฐ ๋ค์ด์์ ์ด๋ ํ Wrapper ๊ฐ์ฒด๋ฅผ ์์ฑํ ๊ฒ์ ๋ณผ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ , ๋ ์ง์ ๋ํ auditing์ ์ํด touchDate() ๋ฉ์๋๋ก ์ ์ด๋ฅผ ์ด๋ํด์ ์ดํด๋ณด์.
์ฝ๋๋ฅผ ๋ณด๋ฉด touchAuditor๋ฅผ ํตํด auditor์ ๋ํ ์ ๋ณด๊ฐ ํ์ํ๋ฉด ์ฑ์๋ฃ๊ฒ ๋๋ฉฐ (@CreatedBy ๊ฐ์ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ ๋ ๋์ํ๋ ๋ฏํ๋ค.) ์ฐ๋ฆฌ ์ฝ๋์์๋ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก touchDate() ๋ฉ์๋๋ก ํ ๋ฒ ์ด๋ํด๋ณด์.
์ฒซ ๋ฒ์งธ ๋ผ์ธ์์ ํ์ฌ ์๊ฐ์ ๊ฐ์ ธ์จ ๋ค์, wrapper::setCreatedDate, wrapper::setLastModifiedDate๋ฅผ ํตํด์ JAVAJIGI์ ๊ฐ์ ์ฑ์๋ฃ๋ ๊ฒ์ ๋ณผ ์ ์๋ค!
์ค์ ๋ก, target์ธ JAVAJIGI์ ์๊ฐ ๊ด๋ จ ํ๋๊ฐ ์ฑ์์ ธ ์๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
๐ฑ JPA์ save๋ ์ด๋ป๊ฒ ๋์ํ ๊น?
๊ทธ๋ ๋ค๋ฉด ์ ๋ง ์ด์ํ๋ค. auditing๊น์ง ์ ์์ ์ผ๋ก ๋์ํ์์๋ ๋ถ๊ตฌํ๊ณ ์ createdAt, updatedAt์ด null์ด์๋ ๊ฒ์ผ๊น?
์ด๋ฅผ ์์๋ณด๊ธฐ ์ํด์ JPA์ save() ๋ฉ์๋๋ฅผ ํ ๋ฒ ๋์ฌ๊ฒจ ๋ณผ ํ์๊ฐ ์๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก userRepository.save() ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด, isNew()๋ผ๋ ์ด๋ ํ ๋ฉ์๋๋ฅผ ํตํด ์๋ก์ด ๊ฐ์ฒด์ธ์ง ํ๋จํ๋ค.
๋ง์ฝ ์๋ก์ด ๊ฐ์ฒด๋ผ๋ฉด persist ํ ๊ธฐ์กด์ ์ํฐํฐ๋ฅผ ๋ฐํํ๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด merge ํ "์๋ก์ด ๊ฐ์ฒด๋ฅผ ๋ฐํ" ํ๋ค๋ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์ ํํธ๋ฅผ ์ป์๋ค. ํน์ ์๋ก์ด ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ฉด์ null๋ก ๋ ๊ฒ์ด ์๋๊น?
๊ทธ๋์, ์๋ก์ด ๊ฐ์ฒด๋ฅผ ํ๋จํ๋ ๊ธฐ์ค์ ์ดํด๋ณด์๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก id (@Id ์ด๋ ธํ ์ด์ ์ด ๋ถ์ด์๋ ํ๋)๊ฐ primitive ํ์ ์ด ์๋๋ผ๋ฉด null ์ฌ๋ถ๋ฅผ ํตํด ์๋ก์ด ๊ฐ์ฒด์์ ํ๋จํ๊ณ ,
primitive ํ์
์ค Number ํ์
์ด๋ผ๋ฉด ๊ฐ์ด 0L์ธ์ง ์ฒดํฌํ์ฌ ์๋ก์ด ๊ฐ์ฒด์ธ์ง๋ฅผ ํ๋จํ๊ฒ ๋๋ค.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
์ฐ๋ฆฌ๊ฐ ์ค๊ณํ๋ ์ํฐํฐ๋ ์ด๋ ์๊น?
User ์ํฐํฐ์ ์์ด๋๋ Long์ด๊ณ , ReferenceType์ด๊ธฐ ๋๋ฌธ์ null์ด ์๋ 1L์ ๊ฐ์ง๊ฒ ๋๋ฉด isNew() ๋ฉ์๋์์ false๋ฅผ ๋ฐํํ๊ฒ ๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์, DB์ ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ๋ ๋ง๋ persist๊ฐ ์๋ merge() ๋ฅผ ์งํํ๊ฒ ๋ ๊ฒ์ด์๋ค!
์ค์ ๋ก, em.merge() ๋ฉ์๋๋ฅผ ํ๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด ์๋์ ๊ฐ์ด MergeEventListener์ ๋ํ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ฌ๊ธฐ์ MergeEventListener::onMerge ๋ฉ์๋๋ฅผ ํ๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด, entityState ๋ผ๋ ๋ณ์์ ‘DETACHED’๋ผ๋ ๊ฐ์ด ๋ค์ด๊ฐ๋ค.
์ฐ๋ฆฌ๋ ์ด์ ์ JAVAJIGI ์ํฐํฐ๋ฅผ ์ ์ฅํ๊ฑฐ๋ ํ์ง ์์์์๋ id๋ฅผ ์ง์ ํ๊ธฐ ๋๋ฌธ์ ๋ถ๋ฆฌ๋ ์ํ๋ผ๊ณ ํ๋จํ๊ฒ ๋ ๊ฒ์ด๋ค.
๊ทธ๋ฆฌ๊ณ , entityIsDetached() ๋ฉ์๋๋ฅผ ๋ ํ๊ณ ๋ค์ด๊ฐ๋ฉด ์์ ๊ฐ์ ๋ถ๋ถ์ ๋ง๋ ์ ์๋ค.
Transientํ ์ํ์ ์ํฐํฐ๋ฅผ ์ ์ฅํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๋๋ฐ, ๋ด๋ถ์ ์ผ๋ก ํ๊ณ ๋ค์ด๊ฐ๋ณด์.
๊ทธ๋ฌ๋ฉด ์์ ๊ฐ์ด callbackRegistry.preCreate() ๋ผ๋ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
ํด๋น ๋ฉ์๋๋ฅผ ๋ด๋ถ์ ์ผ๋ก ํ๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด, ๋ฑ๋ก๋ ์ฝ๋ฐฑ ๋ฉ์๋์ ๋ํด์ ์ํํ๋ฉฐ ํ๋์ฉ ์ฝ๋ฐฑ ๋ฉ์๋๋ฅผ ์คํํ๋ค.
์ด๋, ์ธ์๋ก ๋๊ฒจ์ฃผ๋ bean์ ๊ฒฝ์ฐ createdAt, updatedAt์ด ์ฑ์์ง์ง ์์ ์ํ์ JAVAJIGI ๊ฐ์ฒด ์ ๋ณด์ด๋ค.
์ฌ๊ธฐ์ callback์ ํ์ธํด๋ณด๋ฉด User ์ํฐํฐ์์ ์ค์ ํ๋ AuditingEntityListener๊ฐ ์คํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๋ํ, ์ฝ๋ฐฑํ์ ์ญ์ PRE_PERSIST๋ก ์ค์ ์ด ๋์ด ์๋๋ฐ, ์ด๊ฑด ๋ฌด์์ ๋ปํ๋ ๊ฒ์ผ๊น?
๋ฐ๋ก, ์์์ ๋ดค๋ JPA Auditing ๊ธฐ๋ฅ์์ ์กด์ฌํ๋ touchForUpdate() ๋ฉ์๋ ์์ ๋ฌ๋ ค์๋ ์ด๋
ธํ
์ด์
์ด์๋ ๊ฒ์ด๋ค.
์ฆ, ์ ๋ฆฌํ์๋ฉด save() → em.merge() ๋์ → merge ์ด๋ฒคํธ ์ JPA Auditing ๋์ → ํ๋ ์ฑ์์ง ์์๋ก ๋์ํ๊ฒ ๋๋ ๊ฒ์ด๋ค.
๐ฑ ์๋, ๊ทธ๋์ ํ
์คํธ๋ ์ ๊นจ์ง ๊ฑด๋ฐ์?
๋ค์ ๋์๊ฐ์, em.merge()๊ฐ ์ํ๋ ๋ ํธ์ถ๋๋ fireMerge() ๋ฉ์๋๋ฅผ ๋ณด์.
์์ ์ธ๊ธํ์ง๋ง, JPA์์ save ์ isNew()๊ฐ false๋ผ๋ฉด em.merge()๊ฐ ์ํ๋ ๊ฒฐ๊ณผ๊ฐ์ save์์ ๋ฐํํ๋ค.
์ฐ๋ฆฌ์ ํ ์คํธ ํฝ์ค์ฒ JAVAJIGI์ ๊ฒฝ์ฐ id์ ํ์ ์ด ๋ ํผ๋ฐ์ค ํ์ ์ธ Long์ด์์ผ๋ฉฐ, 1L๋ก ์ฑ์์ ธ์๊ธฐ ๋๋ฌธ์ isNew()์์ false๊ฐ ๋ฐํ๋์๋ ์ํ์ด๋ค.
์ด ๋ฉ์๋์์๋ event.getResult() ๋ผ๋ ๊ฐ์ด ๋ฐํ๋๋๋ฐ, result์์๋ createdAt, updatedAt ๊ฐ์ด ์ฑ์์ง ๊ฐ์ฒด๊ฐ ๋ฐํ๋๋ค.
ํ์ง๋ง, ์ฌ๊ธฐ์ ์ค์ํ ๊ฑด ์๋ณธ ๊ฐ์ฒด (ํ ์คํธ ํฝ์ค์ฒ์ธ JAVAJIGI)์ ๋ฐํ๋๋ ๊ฐ์ฒด๊ฐ ์์ ๋ค๋ฅธ ๊ฐ์ฒด๋ผ๋ ๊ฒ์ด๋ค.
original์ ๊ฒฝ์ฐ User@8627์ด์์ผ๋, result์ ๊ฒฝ์ฐ User@8631์ผ๋ก, ์์ ๋ค๋ฅธ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๊ณ ์๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
@Test
@DisplayName("์ ์ ๋ฅผ ์ ์ฅํ๋ค.")
void save1() {
final User actualUser = userRepository.save(UserTest.JAVAJIGI);
assertThat(actualUser).usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(UserTest.JAVAJIGI);
}
์ฐ๋ฆฌ์ ํ ์คํธ ์ฝ๋์์๋ save๋ก ๋ฐํ๋ ๊ฐ์ฒด์ JAVAJIGI๊ฐ ์์ ๋ค๋ฅธ ๊ฐ์ฒด๊ฐ ๋ ๊ฒ์ด๊ณ , save๋ก ์ธํด ๊ธฐ์กด์ JAVAJIGI ์ํฐํฐ์ ๊ฐ์ ๋ณ๊ฒฝํ ๊ฒ์ด๋ผ๋ ์์๊ณผ ๋ค๋ฅด๊ฒ '๊ฐ์ด ์ฑ์์ง ์ํ์ ์๋ก์ด ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด ๋ฐํ'ํ๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
์ฆ, JAVAJIGI์ createdAt, updatedAt์ ๊ทธ๋๋ก null์ด๊ณ , auditing์ ์๋ก์ด ๊ฐ์ฒด์์ ์งํํ๊ฒ ๋์ด ๊ฐ์ด ์ฑ์์ ธ ๋ฐํ๋ ๊ฒ์ด๋ค.
@Test
@DisplayName("์ ์ ๋ฅผ ์ ์ฅํ๋ค.")
void save1() {
final User actualUser = userRepository.save(UserTest.JAVAJIGI);
assertThat(entityManager.contains(UserTest.JAVAJIGI))
.isFalse();
assertThat(entityManager.contains(actualUser))
.isTrue();
}
๋ํ, ๊ธฐ์กด์ JAVAJIGI ๊ฐ์ฒด๋ ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌ๋์ง ์๊ณ merge ์ดํ ์๋กญ๊ฒ ๋ฐํ๋ฐ์ ๊ฐ์ฒด๊ฐ ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๐ฑ ๊ทธ๋ ๋ค๋ฉด, save2()์์๋ ์ด๋ป๊ฒ ๋์ํ์๊น?
save2()์์ ์ฌ์ฉ๋ SANJIGI์ ๊ฒฝ์ฐ id๊ฐ ์ง์ ๋์ง ์์ ์ฒ์ save() ์ merge๊ฐ ์๋ persist() ๋ฉ์๋๊ฐ ํธ์ถ๋๋ค.
em.persist() ๋ฉ์๋๋ฅผ ํ๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด ์๋์ ๊ฐ์ด persist์ ๋ํ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ฌ๊ธฐ์ ๋ฉ์๋์ ์ฐจ์ด๋ ์๋๋ฐ, fireMerge()์ ๊ฒฝ์ฐ event์ result ๊ฐ์ ๋ฐํํ์์ง๋ง ์ฌ๊ธฐ์๋ ๋ฉ์๋์ ๋ฐํ๊ฐ์ด void์ธ ๊ฒ์ ๋ณผ ์ ์๋ค. ์ฆ, ์๋ก์ด ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ๊ฒ์ด ์๋ ๊ธฐ์กด์ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ๋ค๋ ๊ฒ์ ์ถ๋ก ํ ์ ์๋ค.
PersistEventListener::onPersist๋ฅผ ํ๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด ์๋์ ๊ฐ์ด entityState๊ฐ ‘TRANSIENT’ ์ํ์ธ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด๋ entityManager์ ์ํด ๊ด๋ฆฌ๋์ง ์๋ ์์ํ ์ํ๋ฅผ ์๋ฏธํ๋ค.
์ ๋ฉ์๋์์ entityIsTransient() ๋ฉ์๋๋ฅผ ํ๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด, ์์ ๊ฐ์ ์ํฉ์ ๋ง๋ ์ ์๋ค.
์ฌ๊ธฐ์ ์ธ์๋ก ๋๊ธฐ๋ entity์ ๋ํ ์ ๋ณด๋ ์ฐ๋ฆฌ๊ฐ ๋๊ธด SANJIGI ๊ฐ์ฒด์ธ ๊ฒ์ด๋ค!
saveWithGeneratedId()์ ์ดํ์ ๊ณผ์ ์ save1()์ ์์ ์์ ๋ดค๋ ๋์ ์๋ฆฌ๋ ๋๊ฐ๋ค.
๋ค๋ง, createdAt, updatedAt์ ๋ํ ๊ฐ์ด ์ค์ SANJIGI ๊ฐ์ฒด์์ ์ฑ์์ง๋ค๋ ๊ฒ์ด ํฐ ํน์ง์ด๋ค.
cf. ์์์ ๋ค๋ฃจ์ง ์์ ๊ฒ ๊ฐ์ ๋งํ์ง๋ง Id ๊ฐ ์ญ์ ์ด ๋ฉ์๋์์ ์ฑ์์ง๋ค!
์ด๋ค ์ ๋๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ๋์ง์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ค. (IdentifierGenerator ํ์ฉ)
์ค์ ๋ก save ์ ํ์ ๊ฐ์ ์ดํด๋ณด๋ฉด ๋์ผํ ์ํฐํฐ์ ๋ํด์ (User@8547) ๊ฐ์ด ์ฑ์์ ธ์ ๋ค์ด๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
@Test
@DisplayName("์ ์ ๋ฅผ ์ ์ฅํ๋ค.")
void save2() {
final User actualUser = userRepository.save(UserTest.SANJIGI);
assertThat(entityManager.contains(UserTest.SANJIGI))
.isTrue();
assertThat(entityManager.contains(actualUser))
.isTrue();
}
๋ํ, save2()์์๋ save ์ดํ SANJIGI์ actualUser ๋ชจ๋ ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌํ๋ ๊ฒ์ด ํน์ง์ด๋ค.
๐ฑ em.merge() ์์ ๋ฐํ๋ฐ์ ์ํฐํฐ๋ง ์ฌ์ฉํ์
@Test
@DisplayName("์ ์ ๋ฅผ ์ ์ฅํ๋ค.")
void save1_duplicated_save() {
final User actualUser1 = userRepository.save(UserTest.JAVAJIGI);
final User actualUser2 = userRepository.save(UserTest.JAVAJIGI);
assertThat(actualUser1).usingRecursiveComparison()
.ignoringFields("id", "createdAt", "updatedAt")
.isEqualTo(UserTest.JAVAJIGI);
assertThat(actualUser2).usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(UserTest.JAVAJIGI);
}
save1() ๋ฉ์๋์์ ํ ๋ฒ ๋ ๊ฐ์ฒด๋ฅผ ์ ์ฅํ์ ๋์ ์คํ์ด๋ค.
actualUser1์ ๊ฒฝ์ฐ JAVAJIGI์ actualUser1์ด ๋ค๋ฅด๊ฒ ๊ด๋ฆฌ๋์๊ธฐ ๋๋ฌธ์ ๋ ๊ฐ์ด ๋ฌ๋์๋ค. (createdAt, updatedAt์ด ์ฑ์์ง)
ํ์ง๋ง, ํ ๋ฒ ๋ ์ ์ฅํ actualUser2์ ๊ฒฝ์ฐ JAVAJIGI์ ๋์ผํ๊ฒ createdAt, updatedAt์ด null์ด ๋๋ค. ์ ๊ทธ๋ฌ๋ ๊ฒ์ผ๊น?
๋จผ์ , actualUser1์ ํํ๋ ์์ ๊ฐ์ด createdAt, updatedAt์ด ์ฑ์์ ธ ์๋ ํํ์ด๋ค.
actualUser1์ด ์ ์ฅ๋๋ ์์ ์ merge() ๋ก์ง์ ๋ค์ ํ ๋ฒ ๋ณด์.
์ฒซ ๋ฒ์งธ save() ๋ก์ง์์๋ MergeEventListener::onMerge ๋ฉ์๋์์ (merge ์ด๋ฒคํธ ๋ฐ์ํ ๋) ๋ด๋ถ๋ฅผ ๋ฐ๋ผ๊ฐ๋ค๋ณด๋ฉด ์์์ฑ ์ปจํ
์คํธ์์ ๋์ผํ ํค๊ฐ์ผ๋ก ๊ด๋ฆฌ๋๋ ์ํฐํฐ๊ฐ ์๋์ง ํ์ธํ๋ค. ์ฒ์ save()๋ฅผ ํ ๋๋ ์ ์ฅ๋ ์ํฐํฐ๊ฐ ์๊ธฐ ๋๋ฌธ์ ์๋ค๊ณ ๋์จ๋ค.
๋ํ, entityState๊ฐ null์ด์์ง๋ง, getEntityState๋ฅผ ํตํด DETACHED์ธ ์ํ๋ผ๊ณ ํ๋จํ๊ฒ ๋๋ค.
๊ทธ๋ฆฌ๊ณ , ์ฒ์ ๋ดค์ ๋์ฒ๋ผ entityState๊ฐ DETACHED์ฌ์ entityIsDetached() ๋ฉ์๋๋ฅผ ํ๊ฒ ๋๋ค.
ํ์ง๋ง, ์์์ฑ ์ปจํ ์คํธ ๋ด๋ถ์๋ id = 1์ด๋ฉด์ entityName์ด User์ธ ๊ฐ์ฒด๊ฐ ์กด์ฌํ์ง ์์ result ๊ฐ์ด null์ด ๋์ด,
DETACHED ์ํ์์๋ ์ค์ ๋ก๋ ์์ํ๊ฐ ํ์ํ ๊ฐ์ฒด๋ผ๊ณ ํ๋จํ์ฌ ์์ํ๋ฅผ ์งํํ๊ฒ ๋๋ค.
ํ์ง๋ง, ๋ ๋ฒ์งธ save() ์์๋ ๋ก์ง์ด ๋ฌ๋ผ์ง๋ค.
em.merge()๋ฅผ ์งํํ๊ฒ ๋๋ฉด ์ธ์๋ก ๋๊ฒจ์ค ๊ฐ์ฒด๊ฐ ์๋, ๋ฐํ๋ฐ์ ๊ฐ์ฒด๋ฅผ ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌํ๋ค๊ณ ํ์๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ฒซ ๋ฒ์งธ save() ์์ ๋ฐํ๋ actualUser1๋ง ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌํ๊ณ , JAVAJIGI๋ ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌํ์ง ์๊ฒ ๋๋ค. ํ์ง๋ง, ๋ ๋ฒ์งธ save ์ id ๊ฐ์ด ์ด๋ฏธ 1L๋ก ์ฑ์์ง ๊ฐ์ฒด๊ฐ ์ธ์๋ก ๋ค์ด์๊ณ , ์์์ฑ ์ปจํ ์คํธ์๋ id = 1L์ธ actualUser๊ฐ ๊ด๋ฆฌ๋๊ณ ์๊ธฐ ๋๋ฌธ์ entry ๊ฐ์ด null์ด ์๋๊ฒ ๋๋ค.
์ค์ ๋ก entry ๊ฐ์ด ์์ ๊ฐ์ด createdAt, updatedAt์ด ์ฑ์์ง actualUser1 ๊ฐ์ฒด์์ ํ์ธํ ์ ์๋ค.
์ดํ, entityState๊ฐ DETACHED์ฌ์ entityIsDetached() ๋ฉ์๋๋ฅผ ํ๊ฒ ๋๋ค.
๋ด๋ถ๋ฅผ ๋ณด๋ฉด ์ฒซ ๋ฒ์งธ save์ ๋ค๋ฅด๊ฒ id=1, entityName์ด User์ธ actualUser1๊ฐ ๊ด๋ฆฌ๋๊ณ ์์ด result๊ฐ ์กด์ฌํ๊ฒ ๋๋ค.
๊ทธ๋์ else ๊ตฌ๋ฌธ์ ์๋ ์๋ก์ด ๋ก์ง์ด ์คํ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
MergeContext.put()์ ๊ฒฝ์ฐ, ์ฒซ ๋ฒ์งธ ์ธ์๋ก ๋ณํฉํ ์ํฐํฐ๋ฅผ, ๋ ๋ฒ์งธ ์ธ์๋ก ์ด๋ฏธ ๊ด๋ฆฌ๋๊ณ ์๋ ์ํฐํฐ๋ฅผ ๋ฐ์์ ๋ณํฉํ๋ค.
์ค์ ํธ์ถ๋ถ๋ฅผ ๋ณด๋ฉด ๋ณํฉํ ์ํฐํฐ๋ก ๊ธฐ์กด์ JAVAJIGI ๊ฐ์ฒด๊ฐ, ์ด๋ฏธ ๊ด๋ฆฌ๋๊ณ ์๋ ์ํฐํฐ๋ก ์์์ฑ ์ปจํ ์คํธ์ ์ํด ๊ด๋ฆฌ๋๋ actualUser1 ๊ฐ์ด ๋ค์ด๊ฐ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ดํ copyValues๋ฅผ ํตํด JAVAJIGI ๊ฐ์ฒด์ ๋ํ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก actualUser1์ ๊ฐ์ฒด์ ์ ๋ณด์ ๋ณต์ฌ๊ฐ ์งํ๋๋ค.
์ด๋ก ์ธํด JAVAJIGI๊ฐ ๊ฐ์ง createdAt, updatedAt์ null ๊ฐ์ด ์ ํ๋ ๊ฒ์ด๋ค!
์ต์ข ์ ์ผ๋ก ๊ฒฐ๊ณผ๋ฅผ ์ธํ ํด์ฃผ๋ ๋ถ๋ถ์ ๋ณด๋ฉด null๋ก ์ ๋ฐ์ดํธ ๋์ด์ result๋ฅผ ์ธํ ํด์ฃผ๋ ๊ฒ์ ์ฝ๋๋ก ๋ณผ ์ ์๋ค.
๊ทธ๋์, actualUser2์ createdAt, updatedAt ๊ฐ์ด null์ด ๋์ด์ JAVAJIGI๋ ๊ฐ์ ๊ฐ์ ๊ฐ์ง๊ฒ ๋๋ ๊ฒ์ด๋ค.
๐ฑ ๊ฒฐ๋ก
- em.merge()๋ ํ๋ผ๋ฏธํฐ๋ก ๋์ด์จ ์ํฐํฐ์ '์๋ณ์ ๊ฐ'์ผ๋ก ์์์ฑ ์ปจํ ์คํธ๋ฅผ ์กฐํํ๊ณ , ์ฐพ๋ ์ํฐํฐ๊ฐ ์์ผ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์กฐํํ๋ค.
- ๋ง์ฝ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์๋ ๋ฐ๊ฒฌํ์ง ๋ชปํ๋ฉด ์๋ก์ด ์ํฐํฐ๋ฅผ ์์ฑํ์ฌ ๋ณํฉํ๋ค.
- ๋ณํฉ์ ์ค์์ ์ํ์ ๋น์์ ์ํ๋ฅผ ์ ๊ฒฝ์ฐ์ง ์๊ธฐ ๋๋ฌธ์, ์๋ณ์ ๊ฐ์ผ๋ก ์ํฐํฐ๋ฅผ ์กฐํํ ์ ์์ผ๋ฉด ์กฐํํ์ฌ ๋ณํฉํ๊ณ , ์กฐํํ ์ ์์ผ๋ฉด ์๋ก ์์ฑํ์ฌ ๋ณํฉํ๋ค.
- ๋ฐ๋ผ์ ๋ณํฉ์ save or update ๊ธฐ๋ฅ์ ์ํํ๋ค.
- em.merge๋ฅผ ์ฌ์ฉํ ๋๋ ๋ฐํ๋ฐ์ ๊ฐ์ฒด๋ง ์์์ฑ ์ปจํ ์คํธ์์ ๊ด๋ฆฌํ๋ค๋ ์ ์ ์ ์ํ์!
์๋ ์ ์งํํ๋ ํธ๋ฌ๋ธ ์ํ ์ด์๋๋ฐ ๋ค์ ์ ๋ฆฌํ๋๊น ์๋กญ๋ค.
JPA๋ ์๋ค๊ฐ๋ ์ ๋ง ๋ชจ๋ฅด๊ฒ ๋ค ใ ใ ๐ฅฒ