Skip to main content Link Menu Expand (external link) Document Search Copy Copied

1. Issue Description

ํ˜„์žฌ LicenseCategory ๋Š” licenseType ๊ณผ analyzeType ์„ CombinedKey ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๋‘ ๊ฐœ์˜ ํ‚ค๋Š” LicenseCategoryId ๋ผ๋Š” IdClass ๋กœ ์„ ์–ธ๋˜์–ด ์žˆ์–ด์š”.

๊ทธ๋ž˜์„œ ๊ฐ™์€ licenseType ๊ณผ analyzeType ์„ ๊ฐ€์ง€๊ฒŒ ๋œ๋‹ค๋ฉด, ์ค‘๋ณตํ‚ค ์—๋Ÿฌ๋ฅผ ํ‘œ์‹œํ•ด์•ผ๋งŒํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์—๋Ÿฌ๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์ง„ํ–‰๋˜๋ฒ„๋ ธ์–ด์š”!

1.1 Screenshots

img

1.2 Error Code

  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
@Test
    @DisplayName("๋ผ์ด์„ผ์Šค ์นดํ…Œ๊ณ ๋ฆฌ ์ค‘๋ณต ์ €์žฅ ๋ฐฉ์ง€")
    void function2() {
        // given
        LocalDateTime now = LocalDateTime.now();
        LicenseCategoryId lcId = LicenseCategoryId.builder().licenseType("basic").analyzeType("์•…์„ฑ์ฝ”๋“œ").build();
        LicenseCategoryId lcIdDuplicate = LicenseCategoryId.builder().licenseType("basic").analyzeType("์•…์„ฑ์ฝ”๋“œ").build();
        LicenseCategory lc = LicenseCategory.builder().licenseType(lcId.getLicenseType()).analyzeType(lcId.getAnalyzeType()).createdAt(now).build();
        LicenseCategory lcDuplicate = LicenseCategory.builder().licenseType(lcIdDuplicate.getLicenseType()).analyzeType(lcIdDuplicate.getAnalyzeType()).createdAt(now).build();

        // when
        licenseCategoryRepository.save(lc);
        licenseCategoryRepository.save(lcDuplicate);
        // then
        assertThatThrownBy(() -> {
            licenseCategoryRepository.save(lcDuplicate);
        }).isInstanceOf(DuplicateKeyException.class);
    }
  • ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์˜ค๋ฅ˜
Expecting code to raise a throwable.
java.lang.AssertionError: 
Expecting code to raise a throwable.
	at foxee.product.mainservice.domain.repository.LicenseCategoryRepositoryTest.function2(LicenseCategoryRepositoryTest.java:63)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	...

2. Problem

์ œ๊ฐ€ ์˜ˆ์ƒํ–ˆ๋˜ ํ”Œ๋กœ์šฐ๋Š” JPA ์— ์ฒซ save() ํ˜ธ์ถœ ์‹œ insert ๋˜๋ฉฐ, ๋‘ ๋ฒˆ์งธ ๊ฐ™์€ ID ๋กœ ๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ์‚ฝ์ž… ์‹œ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ insert ๋˜๋Š” ํ”Œ๋กœ์šฐ์—์š”.

ํ•˜์ง€๋งŒ JPA save() ์ค‘๋ณต ํ˜ธ์ถœ ์‹œ ๊ธฐ์กด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

save() ์‹œ db์— ๊ฐ™์€ id ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋“ค๊ณ ์˜ค๊ณ , ์—†๋‹ค๋ฉด insert ํ•ด์ฃผ๊ณ  ์žˆ์—ˆ์–ด์š”.

์™œ ๊ทธ๋Ÿด๊นŒ์š”? JPA ์˜ save() ๋ฉ”์†Œ๋“œ ๋™์ž‘๊ณผ์ •์„ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ์–ด์š”!

3. JPA ์—์„œ save() ๋™์ž‘ ์ˆœ์„œ

3.1 save() ๋™์ž‘ ์ฝ”๋“œ ํ™•์ธ

  • save() ๋Š” merge() ์™€ persist() ๋‘˜ ์ค‘ ํ•˜๋‚˜๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

JPA save() ๋‚ด๋ถ€ ์ฝ”๋“œ

        @Transactional
	@Override
	public <S extends T> S save(S entity) {

		Assert.notNull(entity, "Entity must not be null");

		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}
  • ์œ„์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์•„์‹œ๊ฒ ์ง€๋งŒ, isNew() ์˜ ๋ฐ˜ํ™˜์กฐ๊ฑด์— ๋”ฐ๋ผ์„œ, persist() ์™€ merge() ๋กœ ๋ถ„๊ธฐ๊ฐ€ ๋‚˜๋‰˜๋Š”๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    persist : ์ฆ‰์‹œ DB ์— insert ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

    merge : detached ๋˜์–ด์žˆ๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ manage ํ…Œ์ด๋ธ”(1์ฐจ ์บ์‹œ)๋กœ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ, ๋งŒ์•ฝ detached ์— ์—†์„ ๊ฒฝ์šฐ์—๋Š” DB์— select ์ฟผ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€์ ์œผ๋กœ ๋‚˜๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ๊ทธ๋ ‡๋‹ค๋ฉด isNew() ๋Š” ๋ฌด์—‡์ผ๊นŒ์š”?

isNew() ๋Š” ํ•ด๋‹น Entity๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ๋งŒ๋“ค์–ด์ง„ Entity์ธ์ง€, ํ˜น์€ ๊ธฐ์กด์— ์‚ฌ์šฉ๋˜๋˜ Entity ์ธ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

  • ๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ค์‹์œผ๋กœ Entity๊ฐ€ ์ƒˆ๋กœ์šด Entity์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ• ๊นŒ์š”? ์ฝ”๋“œ๋กœ ํ™•์ธํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
       public boolean isNew(T entity) {

		ID id = getId(entity);
		Class<ID> idType = getIdType();

		if (!idType.isPrimitive()) {
			return id == null;
		}

		if (id instanceof Number) {
			return ((Number) id).longValue() == 0L;
		}

		throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
	}

Primitive vs Wrapper ์ฐธ๊ณ 

primitive ์ž๋ฃŒํ˜•์ด๋ž€? : int, float, long, double, boolean ๊ณผ ๊ฐ™์€ ์›์‹œ์  ์ž๋ฃŒํ˜•

wrapper ์ž๋ฃŒํ˜•์ด๋ž€? : Integer, Float, Long, Double, Boolean, UUID, ์‚ฌ์šฉ์ž ์ •์˜ ํด๋ž˜์Šค

  • getId๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ–ˆ๋˜ @Id ํ•„๋“œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

    ํ˜„์žฌ ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ๋Š” LicenseCategoryId ๋ฅผ PK ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด @id ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด idType์€ LicenseCategoryId ๊ฐ€ ๋˜๊ฒ ์ฃ ?

  • isPrimitive ๋Š” int, float, long, double ์™€ ๊ฐ™์€ ํ•˜๋‚˜์˜ primitive ์ž๋ฃŒํ˜•์ธ์ง€ ํŒ๋‹จํ•˜๋Š” ๋‚ด์žฅํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

    ํ˜„์žฌ ์šฐ๋ฆฌ๋Š” LicenseCategoryId ๋ฅผ PK ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ๋•Œ๋ฌธ์— primitive ์ž๋ฃŒํ˜•์ด ์•„๋‹Œ wrapper ์ž๋ฃŒํ˜•์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด id==null ์ธ์ง€ ํ™•์ธํ•˜๊ฒŒ ๋˜๊ฒ ์ฃ ? ์ด ๋•Œ, ์ด๋ฏธ id ๊ฐ’์„ ์ •ํ•ด์„œ ์‚ฝ์ž…ํ–ˆ๊ธฐ๋•Œ๋ฌธ์— false ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ์ž! ๊ทธ๋Ÿผ ๋‹ค์‹œ ์˜ฌ๋ผ๊ฐ€์„œ isNew๊ฐ€ false ๋ผ๋ฉด ์–ด๋–ค ๊ณผ์ •์„ ๊ฐ€์ง€๊ฒŒ ๋ ๊นŒ์š”?

  • JPA save() ๋‚ด๋ถ€ ์ฝ”๋“œ-๋‹ค์‹œ
      public <S extends T> S save(S entity) {
          if (entityInformation.isNew(entity)) {
              ...
          } else {
              return em.merge(entity);
          }
      }
    
  • ๋„ค. ๋ฐ”๋กœ merge ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, detached ๋˜์–ด์žˆ๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ฒŒ ๋˜๊ฒ ์ฃ !

    ์ฆ‰, save ํ˜ธ์ถœ์‹œ ์ƒˆ๋กœ์šด ์—”ํ‹ฐํ‹ฐ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  UUID ์ž๋ฃŒํ˜•์œผ๋กœ PK ๋ฅผ ์„ค์ •ํ–ˆ๊ธฐ์— ํ•ญ์ƒ persist ๊ฐ€ ์•„๋‹Œ merge๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค

  • ์—ฌ๊ธฐ์„œ ์˜๋ฌธ์ด ์ƒ๊ธฐ์ฃ . detached ์—๋Š” ์–ด๋–ค์• ๋“ค์ด ์กด์žฌํ• ๊นŒ?๋ผ๋Š” ์˜๋ฌธ๋ง์ž…๋‹ˆ๋‹ค.

An entity becomes detached (unmanaged) on following actions:

  • after transaction commit/rollback
  • by calling EntityManager.detach(entity)
  • by clearing the persistence context with EntityManager.clear()
  • by closing an entity manager with EntityManager.close()
  • serializing or sending an entity remotely (pass by value).

reference : https://www.logicbig.com/tutorials/java-ee-tutorial/jpa/detaching.html

์œ„์˜ ์ž๋ฃŒ์—์„œ ๋ˆˆ์—ฌ๊ฒจ ๋ณผ ๊ฒƒ์€ after transaction commit/rollback ์ž…๋‹ˆ๋‹ค. detach ์˜์—ญ์—๋Š” Transaction ๋‚ด ์ฟผ๋ฆฌ๊ฐ€ ๋๋‚˜๋ฉด ์‚ฌ์šฉ๋œ ์—”ํ‹ฐํ‹ฐ๋“ค์„ detached ์— ๋ณด๊ด€ํ•œ๋‹ค๋Š” ๋ง์ด์ฃ ! ๊ทธ๋ฆฌ๊ณ  ์ด detached ๋Š” ์ค€์˜์†์ƒํƒœ ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

  • ์ข…ํ•ฉํ•˜๊ธฐ ์ด์ „์— JPA manage ์ƒํƒœ์— ๋Œ€ํ•ด์„œ ์ž ๊น ์„ค๋ช…ํ•ด๋ณผ๊นŒํ•ด์š”.

์ด์ „ ์•ž์„œ์„œ merge ๋Š” detached ์ƒํƒœ์—์„œ manage ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๊ณ  ๋ง์”€๋“œ๋ ธ์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ ๋” ์ž์„ธํžˆ ๋งํ•˜๋ฉด, id ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์˜์† ์ปจํ…์ŠคํŠธ์˜ 1์ฐจ ์บ์‹œ ๋‚ด id ๊ฐ’์ด ์ผ์น˜ํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด ์ด๋ฅผ ๊ทธ๋Œ€๋กœ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด 1์ฐจ ์บ์‹œ ๋‚ด ์—†๋‹ค๋ฉด, DB์— select ์ฟผ๋ฆฌํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ 1์ฐจ ์บ์‹œ์— ์ง‘์–ด๋„ฃ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ id ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์˜์† ์ปจํ…์ŠคํŠธ ๋‚ด 1์ฐจ ์บ์‹œ์— ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋„ฃ์–ด์ฃผ๋Š” ๊ณผ์ •์ด ๋ฐ”๋กœ ์˜์†ํ™” ๊ณผ์ •์ด๋ฉฐ, 1์ฐจ ์บ์‹œ์— ์‚ฝ์ž…๋œ ์ƒํƒœ๋ฅผ manage ์ƒํƒœ๋ผ๊ณ  ๋ถ€๋ฆ…๋‹ˆ๋‹ค.

img

img

3.2 ํŠธ๋žœ์ ์…˜์— ๋”ฐ๋ฅธ ์‹ค์ œ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ํ…Œ์ŠคํŠธ

๋™์ผ ํŠธ๋žœ์ ์…˜ ๋‚ด ๋™์ผ Id ๋ฅผ ๊ฐ€์ง€๋Š” ์—”ํ‹ฐํ‹ฐ ์ €์žฅ

  • ์ฝ”๋“œ
       transactionTemplate.execute((status)->{
            System.out.println("์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘");
            licenseCategoryRepository.save(lc);
            System.out.println("์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ");
            System.out.println("๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘");
            licenseCategoryRepository.save(lcDuplicate);
            System.out.println("๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ");
            return null;
        });
  • ๊ฒฐ๊ณผ
์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘

Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?)) 
//  ์ด ์—”ํ‹ฐํ‹ฐ๋Š” 1์ฐจ ์บ์‹œ์— ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค

์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ

๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘
// ๊ทธ๋ฆฌ๊ณ  detached ์˜์—ญ๊ณผ 1์ฐจ ์บ์‹œ ์˜์—ญ์„ ํ™•์ธ ํ•˜๊ณ , 1์ฐจ ์บ์‹œ์— ๋™์ผ ID๊ฐ€ ์กด์žฌํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜์˜€์œผ๋‹ˆ ์Šคํ‚ตํ•ฉ๋‹ˆ๋‹ค

๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ
Hibernate: insert into license_category (created_at,analyze_type,license_type) values (?,?,?)

๋‹ค๋ฅธ ํŠธ๋žœ์ ์…˜ ๋‚ด ๋™์ผ Id ๋ฅผ ๊ฐ€์ง€๋Š” ์—”ํ‹ฐํ‹ฐ ์ €์žฅ

  • ์ฝ”๋“œ
        transactionTemplate.execute((status)->{
            System.out.println("์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘");
            licenseCategoryRepository.save(lc);
            System.out.println("์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ");
            return null;
        });

        transactionTemplate.execute((status)->{
            System.out.println("๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘");
            licenseCategoryRepository.save(lcDuplicate);
            System.out.println("๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ");
            return null;
        });
  • ๊ฒฐ๊ณผ
์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘

Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?)) 
//  ์ด ์—”ํ‹ฐํ‹ฐ๋Š” 1์ฐจ ์บ์‹œ์— ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค

์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ 

Hibernate: insert into license_category (created_at,analyze_type,license_type) values (?,?,?) 
//  ํŠธ๋žœ์ ์…˜์ด ๋๋‚˜๊ณ  flush์™€ commit ์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์—”ํ‹ฐํ‹ฐ๋Š” 1์ฐจ ์บ์‹œ์—์„œ ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค

๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘

Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
// ์ค€์˜์†์— ์กด์žฌํ•˜๋Š” ์• ๊ฐ€ 1์ฐจ ์บ์‹œ์— ์žˆ๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•˜๊ณ  ์—†์œผ๋ฉด ๋‹ค์‹œ select ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค

๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ

์ด select ์ฟผ๋ฆฌ์˜ ๋œป์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. db ์— ์ €์žฅ๋œ row ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋ณ€๊ฒฝ์ ์ด ์žˆ๋Š”์ง€ ์—†๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์—†๋‹ค๋ฉด no ์ฟผ๋ฆฌ, ์žˆ๋‹ค๋ฉด update ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆด๊ฒƒ์ด๋‹ค!

์•„๋ž˜๋Š” ๋™์ผ ID ์˜ ๋‹ค๋ฅธ createdAt๊ฐ’์„ ๋„ฃ์—ˆ์„ ๋•Œ ์ฟผ๋ฆฌ๋˜๋Š” ๊ตฌ๋ฌธ๋“ค์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์š”.

    @Test
    @DisplayName("๋ผ์ด์„ผ์Šค ์นดํ…Œ๊ณ ๋ฆฌ ์ค‘๋ณต ์ €์žฅ ๋ฐฉ์ง€ - JPA Seperated Transaction")
    void function2() {
        // given
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime now2 = LocalDateTime.now().plusDays(5);
        LicenseCategoryId lcId = LicenseCategoryId.builder().licenseType("basic0").analyzeType("์•…์„ฑ์ฝ”๋“œ").build();
        LicenseCategoryId lcIdDuplicate = LicenseCategoryId.builder().licenseType("basic0").analyzeType("์•…์„ฑ์ฝ”๋“œ").build();
        LicenseCategory lc = LicenseCategory.builder().licenseType(lcId.getLicenseType()).analyzeType(lcId.getAnalyzeType()).createdAt(now).build();
        LicenseCategory lcDuplicate = LicenseCategory.builder().licenseType(lcIdDuplicate.getLicenseType()).analyzeType(lcIdDuplicate.getAnalyzeType()).createdAt(now2).build();

        // when
        transactionTemplate.execute((status)->{
            System.out.println("์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘");
            licenseCategoryRepository.save(lc);
            System.out.println("์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ");
            return null;
        });

        transactionTemplate.execute((status)->{
            System.out.println("๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘");
            licenseCategoryRepository.save(lcDuplicate);
            System.out.println("๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ");
            return null;
        });
    }
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0
์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
์ฒซ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ
Hibernate: insert into license_category (created_at,analyze_type,license_type) values (?,?,?)
๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์‹œ์ž‘
Hibernate: select l1_0.analyze_type,l1_0.license_type,l1_0.created_at from license_category l1_0 where (l1_0.analyze_type,l1_0.license_type) in ((?,?))
๋‘ ๋ฒˆ์งธ LC ์ €์žฅ ์™„๋ฃŒ
Hibernate: update license_category set created_at=? where analyze_type=? and license_type=?

4. ์œ„์˜ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ ์ข…ํ•ฉํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค

  1. ์ฒซ ๋ฒˆ์งธ save()๋Š” ์—”ํ‹ฐํ‹ฐ์˜ @id ๊ฐ€ UUID ๋กœ wrapper ์ž๋ฃŒํ˜•์ด๊ณ  id๊ฐ’์„ ๊ฐ™์ด ์ž…๋ ฅํ•ด์ฃผ์—ˆ๊ธฐ๋•Œ๋ฌธ์— ์กด์žฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— em.merge(entity) ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  2. ํ•˜์ง€๋งŒ detached ์— ์กด์žฌํ•˜์ง€ ์•Š๊ณ , 1์ฐจ ์บ์‹œ๋˜ํ•œ ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— db ์— SELECT ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

    em.persist() ์˜ ๊ฒฝ์šฐ 1์ฐจ ์บ์‹œ์— ์กด์žฌํ•˜๋Š” entity๋กœ ์ธ์ง€ํ•˜๊ณ , ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฐ”๋กœ INSERT ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  3. ๋™์ผ ID ๋ฅผ ๊ฐ€์ง€๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ€ db์— ์กด์žฌํ•œ๋‹ค๋ฉด, UPDATE ๋ฌธ์„ ๋ฐฐ์น˜ํ•˜๊ณ  ์ตœ์‹ ์—”ํ‹ฐํ‹ฐ๋ฅผ 1์ฐจ ์บ์‹œ์— ์‚ฝ์ž… ๋ฐ ํ•ฉ๋‹ˆ๋‹ค.

    ๋™์ผ ID ๊ฐ€ db์— ์—†๋‹ค๋ฉด INSERT ๋ฌธ์„ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค

  4. ํŠธ๋žœ์ ์…˜์ด ๋๋‚œ ๋’ค ๋ฐฐ์น˜๋œ SQL ๋ฌธ์„ flush, commit, detach ํ•ฉ๋‹ˆ๋‹ค.
  5. ๋‘ ๋ฒˆ์งธ save() ๋˜ํ•œ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ em.merge() ๊ฐ€ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.
  6. ๊ทธ๋ฆฌ๊ณ  detached ์˜์—ญ์— ์กด์žฌํ•˜๋Š” ๋™์ผ ID ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  7. ๊ทธ๋ฆฌ๊ณ  1์ฐจ ์บ์‹œ์— ์กด์žฌํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์‹œ SELECT ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
  8. ์ด ID ๋Š” db์— ์กด์žฌํ•˜๋Š” ID ์ด๊ธฐ ๋•Œ๋ฌธ์—, UPDATE ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

5. ๊ฒฐ๋ก ์€?

  • save() ๋Š” isNew() ๋ฅผ ํ†ตํ•ด์„œ ์ƒˆ๋กœ์šด ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ persist(), ์•„๋‹ˆ๋ฉด merge()๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
  • isNew() ๋Š” primitive + Number type ์ด๋ฉด 0, IdClass์™€ ๊ฐ™์ด un-primitive ์ธ ๊ฒฝ์šฐ์—๋Š” null ์ธ์ง€๋ฅผ ํ™•์ธํ•จ์œผ๋กœ์จ ์ƒˆ๋กœ์šด ๊ฐ์ฒด์ธ์ง€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • LicenseCategory๋Š” un-primitive ์ธ IdClass ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๊ฐ’์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋ฏ€๋กœ ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™€์„œ ์ด๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ๊ฒฐ๋ก ์€ Persistable ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ isNew() ํŒ๋‹จ ์กฐ๊ฑด์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค!

  • isNew()๋ฅผ ํ†ตํ•ด Entity๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ๋งŒ๋“ค์–ด์ง„ entity๋กœ ์ธ์ง€, ํ˜น์€ ๊ธฐ์กด์— ์‚ฌ์šฉ๋˜๋˜ Entity ์ธ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. persist() ์˜ ๊ฒฝ์šฐ ๊ธฐ์กด์— ์กด์žฌํ•˜๋˜ entity๋กœ ์ธ์ง€ํ•˜๊ณ , ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฐ”๋กœ insert ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ merge() ์˜ ๊ฒฝ์šฐ, ๋ฐ€์–ด ๋„ฃ์œผ๋ ค๋Š” ๊ฐ’์˜ id๊ฐ€ ํ…Œ์ด๋ธ”์— ์žˆ๋Š”์ง€๋ฅผ ์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•ด๋ณด๊ธฐ ์œ„ํ•ด์„œ select ์ฟผ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€์ ์œผ๋กœ 1ํšŒ ๋‚˜๊ฐˆ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. (1์ฐจ ์บ์‹œ์— ์—†๋Š” ๊ฒฝ์šฐ) ์ด ๋•Œ๋ฌธ์— merge ์‚ฌ์šฉ์‹œ saveAll() ๊ณผ ๊ฐ™์ด N๊ฐœ์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ save ํ•˜๊ฒŒ ๋˜๋Š”๊ฒฝ์šฐ, ๋ถˆํ•„์š”ํ•œ ์ฟผ๋ฆฌ(select) N๋ฒˆ์ด ์ถ”๊ฐ€์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜์–ด ์„ฑ๋Šฅ์— ์ด์Šˆ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ merge๋Š” pk๊ฐ€ ๊ฐ™์€ ๊ธฐ์กด์— entity๋ฅผ ๋Œ€์ฒดํ•ด๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์—, entity๋‚ด์˜ ํ•„๋“œ๊ฐ’๋“ค์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์‚ฌ๋ผ์ง€๊ฑฐ๋‚˜ ๋ณ€๊ฒฝ๋˜๋Š” ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

reference : ID UUID ์ €์žฅ์‹œ ๊ณ ๋ ค