DevLog ๐Ÿ˜ถ

[JPA] JPA Auditing๊ณผ em.merge()์˜ ๋™์ž‘ ์›๋ฆฌ ์‚ดํŽด๋ณด๊ธฐ - ํ…Œ์ŠคํŠธ์—์„œ id ๊ฐ’์„ ์ฃผ์˜ํ•˜์ž ๋ณธ๋ฌธ

Back-end/JPA

[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๋Š” ์•Œ๋‹ค๊ฐ€๋„ ์ •๋ง ๋ชจ๋ฅด๊ฒ ๋‹ค ใ…Žใ…Ž ๐Ÿฅฒ

Comments