Post

(JPA) Persistable

(JPA) Persistable

πŸ“ μž‘μ„± λ°°κ²½

Spring Data JPA λ₯Ό μ‚¬μš©ν•˜λ©΄μ„œ λ‹€μ–‘ν•œ primary key 생성 μ „λž΅μ΄ μžˆλŠ”λ°, 이런 μ „λž΅λ“€μ„ μ‚¬μš©ν•˜μ§€ μ•Šκ³  직접 ID 값을 μ§€μ •ν•΄μ£ΌλŠ” 방식을 μ‚¬μš©ν•˜λŠ” κ²½μš°λ„ μ’…μ’… μžˆλŠ”λ° 직접 ID λ₯Ό μ§€μ •ν•΄μ„œ μ‚¬μš©ν•˜λŠ” 경우 λ°œμƒν•  수 μžˆλŠ” 상황에 λŒ€ν•œ 주의점 및 κ°œμ„  방법에 λŒ€ν•΄ κ°„λž΅ν•˜κ²Œ μ •λ¦¬ν•΄λ³΄μž ν•œλ‹€.

Spring Data JPA λ₯Ό μ‚¬μš©ν•˜λ©΄μ„œ ID 값을 주둜 직접 μ§€μ •ν•  λ•Œ ν•„μžλŠ” 주둜 UUID λ‚˜ Snowflake λ“±μ˜ ν˜•μ‹μ„ μ‚¬μš©ν•˜κ³  μžˆλ‹€. 이 두가지 방식 외에도 직접 ID λ₯Ό μ§€μ •ν•΄μ£ΌλŠ” κ²½μš°μ—λŠ” λͺ¨λ‘ λ°œμƒν•  수 μžˆλŠ” μƒν™©μ΄λ‹ˆ, 참고해보면 쒋을것 κ°™λ‹€.


πŸ‘“ μ„  3쀄 μš”μ•½

  • Spring Data JPA μ—μ„œ ID λ₯Ό 직접 μ§€μ •ν•˜λ©΄ μ˜λ„μΉ˜ μ•Šμ€ SELECT 쿼리가 μΆ”κ°€λ‘œ λ°œμƒν•¨.
  • Persistable μΈν„°νŽ˜μ΄μŠ€ κ΅¬ν˜„μœΌλ‘œ isNew() λ‘œμ§μ„ 직접 μ œμ–΄ν•˜μ—¬ λΆˆν•„μš”ν•œ 쿼리 제거 κ°€λŠ₯.
  • BaseEntity에 Persistable을 κ΅¬ν˜„ν•˜λ©΄ λͺ¨λ“  μ—”ν‹°ν‹°μ—μ„œ 일관성 있게 처리 κ°€λŠ₯.

πŸ”οΈ 예제 상황

νŠΉμ • μ„œλΉ„μŠ€μ—μ„œ User 데이터λ₯Ό μ €μž₯ν• λ•Œ Sequence λ‚˜ AutoIncrement λ₯Ό ν™œμš©ν•˜μ—¬ μ‹λ³„μž(ID) 값을 μ§€μ •ν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ μ—¬λŸ¬ 이유둜 인해 νšŒμ‚¬μ—μ„œ μ§€μ •ν•œ μ‹λ³„μž μ§€μ • 방식을 μ‚¬μš©ν•΄μ•Ό ν•œλ‹€κ³  κ°€μ •ν•΄λ³΄μž.

아이디 생성 μœ„μž„

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name = "users")
class UserEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,
    @Column(nullable = false)
    val username: String,
    @Column(nullable = false)
    val password: String
)

interface UserRepository : JpaRepository<UserEntity, Long>

예제λ₯Ό μœ„ν•΄ κ°„λ‹¨ν•˜κ²Œ UserEntity λ₯Ό μœ„μ™€ 같이 λ§Œλ“€μ—ˆλ‹€. ν•™μŠ΅ν• λ•Œ 많이 μ‚¬μš©ν•˜λŠ” μ‹λ³„μž 생성 μ „λž΅μΈ GenerationType.IDENTITY λ₯Ό μ„€μ •ν•œ λͺ¨μŠ΅μ΄λ‹€.

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
@DataJpaTest
class PersistableTest {
    @Autowired
    private lateinit var entityManager: TestEntityManager

    @Autowired
    private lateinit var userRepository: UserRepository
    
    @Test
    @DisplayName("아이디 생성을 μœ„μž„ ν–ˆμ„λ•Œ")
    fun generatedValueTest() {
        val stats: Statistics =
            entityManager.entityManager
                .unwrap(Session::class.java)
                .sessionFactory
                .statistics.also {
                    it.isStatisticsEnabled = true
                    it.clear()
                }

        val entity = UserEntity(username = "test_user", password = "1234")
        userRepository.save(entity)
        userRepository.flush()

        val queryCount: Long = stats.prepareStatementCount
        println("Query Count: $queryCount")

        Assertions.assertThat(queryCount).isGreaterThanOrEqualTo(1)
    }
}

κ°„λ‹¨ν•œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό 톡해 아이디 생성을 μœ„μž„ν•œ μƒν™©μ—μ„œ save λ₯Ό ν–ˆμ„λ•Œ query κ°€ λͺ‡λ²ˆ λ°œμƒν•˜λŠ”μ§€ μ•Œμ•„λ³΄μž.

1
2
3
4
5
6
7
8
Hibernate: 
    insert 
    into
        users
        (password, username, id) 
    values
        (?, ?, default)
Query Count: 1

둜그λ₯Ό μ‚΄νŽ΄λ³΄λ©΄ insert 쿼리만 λ”± ν•œλ²ˆ λ°œμƒν•œκ±Έ λ³Ό 수 μžˆλ‹€.

λŒ€κ°œ Spring Data JPA λ₯Ό ν™œμš©ν•΄μ„œ ν•™μŠ΅ν• λ•Œ 아이디 생성을 μœ„μž„ν•˜λŠ” μ‹μœΌλ‘œ ν•™μŠ΅μ„ ν•˜κΈ° λ•Œλ¬Έμ— 이 뢀뢄에 λŒ€ν•΄μ„œλŠ”
λ§Žμ€ 뢄듀이 μ•Œκ³  μžˆλŠ” λ‚΄μš©μΌκ²ƒ κ°™λ‹€. 그럼 μ΄λ²ˆμ—λŠ” 아이디 생성을 μœ„μž„ν•˜λŠ” μ „λž΅μ„ μ‚¬μš©ν•˜μ§€ μ•Šκ³ , 직접 μ„€μ •ν•΄ μ£ΌλŠ” 방식을 μ‚¬μš©ν•΄λ³΄μž.

아이디 직접 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name = "users")
class UserEntity(
    @Id
    // @GeneratedValue(strategy = GenerationType.IDENTITY) ν•΄λ‹Ή 뢀뢄을 주석 처리 λ˜λŠ” μ‚­μ œ
    val id: Long = 0L,
    @Column(nullable = false)
    val username: String,
    @Column(nullable = false)
    val password: String
)

interface UserRepository : JpaRepository<UserEntity, Long>

μ‹λ³„μž 생성을 μœ„μž„ν•˜λ˜ λ°©μ‹μ—μ„œ 생성 μ „λž΅μ„ λ‚˜νƒ€λ‚΄λŠ” Annotation 을 주석 μ²˜λ¦¬ν•œ λͺ¨μŠ΅

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
@DataJpaTest
class PersistableTest {
    @Autowired
    private lateinit var entityManager: TestEntityManager

    @Autowired
    private lateinit var userRepository: UserRepository
    
    @Test
    @DisplayName("Persistable κ΅¬ν˜„ν•˜μ§€ μ•Šμ•˜μ„λ•Œ")
    fun nonPersistableTest() {
        val stats: Statistics =
            entityManager.entityManager
                .unwrap(Session::class.java)
                .sessionFactory
                .statistics.also {
                    it.isStatisticsEnabled = true
                    it.clear()
                }

        // UserEntity() λ₯Ό 생성해 쀄 λ•Œ ID 값을 직접 μ„€μ •ν•΄μ£Όκ³  μžˆλŠ” λͺ¨μŠ΅
        val entity = UserEntity(id = 1L, username = "test_user", password = "1234")
        userRepository.save(entity)
        userRepository.flush()

        val queryCount: Long = stats.prepareStatementCount
        println("Query Count: $queryCount")

        Assertions.assertThat(queryCount).isGreaterThanOrEqualTo(2)
    }
}

μ΄λ²ˆμ—λŠ” UserEntity 을 μ‹λ³„μžλ₯Ό 직접 μ„€μ •ν•΄λ³΄μž.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hibernate: 
    select
        ue1_0.id,
        ue1_0.password,
        ue1_0.username 
    from
        users ue1_0 
    where
        ue1_0.id=?
Hibernate: 
    insert 
    into
        users
        (password, username, id) 
    values
        (?, ?, ?)
Query Count: 2

둜그λ₯Ό μ‚΄νŽ΄λ³΄λ©΄, 쿼리가 2번 λ°œμƒν•œκ±Έ λ³Ό 수 μžˆλ‹€. insert 쿼리 외에 select 쿼리가 μΆ”κ°€λ‘œ λ°œμƒν•œ λͺ¨μŠ΅.
insert 쿼리가 ν•œλ²ˆλ§Œ λ°œμƒν• κ±°λΌ μƒκ°ν–ˆλŠ”λ°, λœ¬κΈˆμ—†μ΄ select 쿼리가 μΆ”κ°€λ‘œ λ°œμƒν•œ λͺ¨μŠ΅μ΄λ‹€.

λ¨Όμ € μ™œ 이런 상황이 λ°œμƒν•˜λŠ”μ§€ κ°„λ‹¨ν•˜κ²Œ μ•Œμ•„λ³΄κ³  λ„˜μ–΄κ°€μž.

μš°μ„  이 뢀뢄을 μ΄ν•΄ν•˜λ €λ©΄ Spring Data JPA μ—μ„œ save() λ©”μ„œλ“œκ°€ λ‚΄λΆ€μ μœΌλ‘œ μ–΄λ–»κ²Œ λ™μž‘ν•˜μ§€λŠ” 이해λ₯Ό ν•΄μ•Ό ν•œλ‹€. Spring Data JPA μ—μ„œ save() λ©”μ„œλ“œλ₯Ό 직접 κ΅¬ν˜„ν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ κ΅¬ν˜„ λ˜μ–΄ μžˆλŠ”κ±Έ κ·ΈλŒ€λ‘œ μ‚¬μš©ν•œλ‹€λ©΄ JpaRepositoryImplementation λ₯Ό κ΅¬ν˜„ν•˜κ³  μžˆλŠ” κ΅¬ν˜„μ²΄μΈ SimpleJpaRepository 의 save() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜κ²Œ
λ˜λŠ”λ° ν•΄λ‹Ή λ©”μ„œλ“œλŠ” μ•„λž˜μ™€ 같이 μž‘μ„±λ˜μ–΄ μžˆλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    private final EntityManager entityManager;
    
    // ...
    
    @Override
    @Transactional
    public <S extends T> S save(S entity) {
        
        Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
        
        if (entityInformation.isNew(entity)) {
        	entityManager.persist(entity);
        	return entity;
        } else {
        	return entityManager.merge(entity);
        }
    }
    
    // ...
}

μ—¬κΈ°μ„œ μš°λ¦¬λŠ” 3κ°€μ§€λ₯Ό μ€‘μ μ μœΌλ‘œ 봐야 ν•œλ‹€.

  1. entityInformante.isNew(entity)
  2. entityManager.persist(entity)
  3. entityManager.merge(entity)

μš°μ„  첫번째 entityInformante.isNew(entity) 을 μ‚΄νŽ΄λ³΄μž.
isNew() λ©”μ„œλ“œμ˜ κ²½μš°λŠ” JpaEntityInformationSupport λ₯Ό μƒμ†ν•˜κ³  μžˆλŠ”
JpaMetamodelEntityInformation 에 μžˆλŠ” isNew() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜κ³  μžˆλŠ”λ°
ν•΄λ‹Ή λ©”μ„œλ“œλŠ” μ•„λž˜μ™€ 같이 μž‘μ„±λ˜μ–΄ μžˆλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JpaMetamodelEntityInformation<T, ID> extends JpaEntityInformationSupport<T, ID> {
    // ...
    @Override
    public boolean isNew(T entity) {
        
        if (versionAttribute.isEmpty()
        		|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
        	return super.isNew(entity);
        }
        
        BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
        
        return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
    }
    // ...
}

λ¨Όμ € μ—¬κΈ°μ„œ versionAttribute.isEmpty() 인지 확인을 ν•˜κ²Œ λ˜λŠ”λ°
이건 @Version 을 μ„ μ–Έν•œ ν•„λ“œκ°€ μžˆλŠ”μ§€ ν™•μΈν•˜λŠ” 뢀뢄이닀. ν•΄λ‹Ή λ‚΄μš©μ€ Optimistic Lock κ³Ό κ΄€λ ¨λœ λ‚΄μš©μ΄λΌ
μžμ„Έν•œ λ‚΄μš©μ€ μš°μ„ μ€ λ„˜μ–΄κ°€μž. UserEntity 에 @Version 을 μ„ μ–Έν•œ ν•„λ“œκ°€ μ—†μœΌλ‹ˆ isEnpty() λŠ” true λ₯Ό λ°˜ν™˜ν•˜κ²Œ λœλ‹€. ||(or) 쑰건이닀 λ³΄λ‹ˆ, if λ¬Έ λ‚΄λΆ€λ‘œ λ“€μ–΄κ°€κ²Œ λœλ‹€.

if λ¬Έ λ‚΄λΆ€μ—μ„œ super.isNew(entity) λ₯Ό ν˜ΈμΆœν•˜κ²Œ λ˜λŠ”λ°, 이 κ²½μš°μ—λŠ” EntityInformation 을 κ΅¬ν˜„ν•˜κ³  μžˆλŠ”
κ΅¬ν˜„μ²΄μΈ AbstractEntityInformation 에 μžˆλŠ” isNew() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜κ²Œ λœλ‹€.
ν•΄λ‹Ή λ©”μ„œλ“œλŠ” μ•„λž˜μ™€ 같이 μž‘μ„±λ˜μ–΄ μžˆλ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class AbstractEntityInformation<T, ID> implements EntityInformation<T, ID> {

    // ...
    
    @Override
    public boolean isNew(T entity) {
    
        ID id = getId(entity);           // 1. id 값을 κ°€μ§€κ³  μ˜¨λ‹€.
        Class<ID> idType = getIdType();
        
        if (!idType.isPrimitive()) {     // 2. id 값이 primitive νƒ€μž…μΈμ§€ reference νƒ€μž…μΈμ§€ 확인 
        	return id == null;
        }
        
        if (id instanceof Number n) {.   // 3. id 값이 Number instance 인지 확인
        	return n.longValue() == 0L;  // 4. id 값이 0 인지 확인
        }
        
        throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
    }
    
    // ...
}

ν•΄λ‹Ή λ©”μ„œλ“œμ—μ„œ entity 의 id 값을 가져와 μ—¬λŸ¬ 쑰건에 λ§žλŠ”μ§€ ν™•μΈν•˜λŠ”λ° μš°λ¦¬κ°€ μ§€μ •ν•΄μ€€ κ°’(1L)을 κ°€μ§€κ³  ν™•μΈν•΄λ³΄μž.

  1. μš°λ¦¬κ°€ μ§€μ •ν•΄μ€€ κ°’ 1L 을 λ°˜ν™˜
  2. UserEntity 의 id λŠ” long 으둜 primitive νƒ€μž…
    • μ—¬κΈ°μ„œ id 의 νƒ€μž…μ΄ μ™œ Long 이 μ•„λ‹ˆκ³  long μΈμ§€λŠ” kotlin μ—μ„œ μ„ μ–Έν•œ Long νƒ€μž…μ΄ java 둜 λ³€ν™˜λ˜λ©΄μ„œ μ–΄λ–»κ²Œ νƒ€μž…μ΄ λ³€κ²½λ˜λŠ”μ§€ μ•Œκ³  μžˆμ–΄μ•Ό 이해가 λ˜λŠ” 뢀뢄이닀.
    • κ°„λ‹¨ν•˜κ²Œ 확인해 보고 μ‹Άλ‹€λ©΄ intellij 의 kotlin 을 java 둜 decompile ν•˜λŠ” κΈ°λŠ₯을 μ‚¬μš©ν•΄λ³΄λ©΄ 확인해 λ³Ό 수 μžˆλ‹€.
    • λΉ λ₯΄κ²Œ μ„€λͺ…ν•΄λ³΄μžλ©΄ kotlin μ—μ„œ java 둜 λ³€ν™˜ λ λ•Œ type 의 κ²½μš°λŠ” var/val 인지 nullable 인지 에 따라 νƒ€μž…μ΄ κ²°μ •λ˜λŠ”λ° Non-nullable Long(val id: Long = 0L) 의 κ²½μš°λŠ” jvm μ—μ„œ type μ΅œμ ν™”λ‘œ 인해 long 으둜 λ³€ν™˜λ˜κ²Œ λœλ‹€.
  3. id 의 값은 1L 둜 long νƒ€μž…μ΄κΈ° λ•Œλ¬Έμ— Number 의 μΈμŠ€ν„΄μŠ€κ°€ λ§žλ‹€.
  4. 이 λΆ€λΆ„ λ•Œλ¬Έμ— μ—¬κΈ°κΉŒμ§€ κΈ΄ 여정을 ν•˜κ²Œ λ˜μ—ˆλŠ”λ°, μ—¬κΈ°μ„œ id 의 값을 ν™•μΈν•˜λŠ”λ°, μ§€κΈˆ id 의 값은 1L 둜 0 이 μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— false λ₯Ό λ°˜ν™˜ν•˜κ²Œ λœλ‹€.

λ§ˆμΉ¨λ‚΄ μš°λ¦¬λŠ” isNew() λ©”μ„œλ“œκ°€ μ–΄λ–€ 값을 λ°˜ν™˜ν•˜λŠ”μ§€ ν™•μΈν•˜μ˜€λ‹€. 그럼 λ‹€μ‹œ save() λ©”μ„œλ“œλ‘œ λŒμ•„κ°€λ³΄μž.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
@Transactional
public <S extends T> S save(S entity) {

    Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
    
    if (entityInformation.isNew(entity)) {
    	entityManager.persist(entity);
    	return entity;
    } else {
    	return entityManager.merge(entity);
    }
}

save() λ©”μ„œλ“œλ₯Ό 보면 isNew() κ°€ false μΌλ•Œ merge λ₯Ό ν˜ΈμΆœν•˜λ„λ‘ λ˜μ–΄ μžˆλ‹€.
persist 와 merge 의 차이점은 κ°„λ‹¨ν•˜κ²Œ μ„€λͺ…ν•΄μ„œ insert 와 select -> insert / update 이 차이가 μžˆλ‹€.

  1. persist: insert 쿼리 λ™μž‘
  2. merge: select 쿼리 이후 ν•΄λ‹Ή μ‹λ³„μžμ— λŒ€ν•œ 값이 μžˆλ‹€λ©΄ update λ₯Ό μ—†λ‹€λ©΄ insert λ₯Ό ν•˜λ„λ‘ λ™μž‘

μœ„ λ‘κ°€μ§€μ˜ 차이점 λ•Œλ¬Έμ— ν˜„μž¬ UserEntity 에 id 값을 직접 μ§€μ •ν•΄μ€¬μ„λ•Œ select 쿼리가 μΆ”κ°€μ μœΌλ‘œ λ°œμƒν•˜κ²Œ 된 μ΄μœ λ‹€.

사싀 μš°λ¦¬κ°€ λ°”λΌλ˜ λ™μž‘μ€ λ‹¨μˆœ insert 인데, λ‚΄λΆ€μ μœΌλ‘œ 생각보닀 λ³΅μž‘ν•œ 둜직이 λŒμ•„κ°€κ³  μžˆμœΌλ©΄μ„œ, 약간은 λΆˆν•„μš”ν•˜λ‹€
생각될 수 μžˆλŠ” 쿼리인 select 쿼리가 ν•œλ²ˆ 더 λ°œμƒν•œλ‹€λŠ” λΆ€λΆ„μ—μ„œ 이 뢀뢄은 κ°œμ„ μ‹œν‚¬ 수 μžˆλŠ” 방법은 없을지 μ°Ύμ•„λ³΄μž.

♻️ κ°œμ„  λ°©μ•ˆ

Spring Data JPA μ—μ„œ id λ₯Ό 직접 μ§€μ •ν–ˆμ„λ•Œ select 쿼리가 λ°œμƒν•˜μ§€ μ•Šλ„λ‘ κ°œμ„  λ°©μ•ˆμ— λŒ€ν•΄ μ•Œμ•„λ³΄μž.

  1. save() λ©”μ„œλ“œλ₯Ό 직접 κ΅¬ν˜„ν•˜μ—¬ μ‚¬μš©ν•œλ‹€.
  2. Spring Data JPA λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. 예?
  3. Persistable 을 직접 κ΅¬ν˜„ν•œλ‹€.

ν•„μžλŠ” 3κ°€μ§€ 정도 방법을 생각해보고 μ μš©μ‹œμΌœ λ΄€λ‹€.(ν˜Ήμ‹œ κ°œμ„ ν•  수 μžˆλŠ” 방법이 더 μžˆλ‹€λ©΄ κ³΅μœ ν•΄μ£Όμ„Έμš”!)

μš°μ„  첫번째 방법.
save() λ©”μ„œλ“œλ₯Ό Spring Data JPA μ—μ„œ κ΅¬ν˜„λ˜μ–΄ μžˆλŠ” λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ λ‚΄κ°€ 직접 κ΅¬ν˜„ν•΄μ„œ μ‚¬μš©ν•œλ‹€!
사싀 μœ„ λ¬Έμ œλŠ” save() λ©”μ„œλ“œμ—μ„œλ§Œ λ°œμƒν•˜λŠ” λ¬Έμ œλŠ” μ•„λ‹ˆλ‹€. delete λ‚˜ update μ—μ„œλ„ λ¬Έμ œκ°€ λ°œμƒ ν•  μˆ˜λ„ μžˆλ‹€. 그리고 맀번 save() λ₯Ό 직접 κ΅¬ν˜„ν•˜λŠ”κ±΄ 번거둜운 일이닀.

λ‘λ²ˆμ§Έ 방법.
Spring Data JPA λ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€!
κ·Έλ ‡λ‹€. Spring μ—μ„œ DB λ₯Ό λ‹€λ£°λ•Œ Spring Data JPA 만 μžˆλŠ”κ²Œ μ•„λ‹ˆλ‹€,
κ·Έλƒ₯ JPA λ₯Ό μ‚¬μš©ν•΄λ„ 되고, JDBC Template 을 μ‚¬μš©ν•΄λ„ 되고 Mybatis λ₯Ό μ‚¬μš©ν•΄λ„ λœλ‹€.
λ‹€λ§Œ, 이 문제 λ•Œλ¬Έμ— 이미 ν”„λ‘œμ νŠΈμ— 적용 λ˜μ–΄ 있고, μ μš©ν•˜κΈ°λ‘œν•œ 기술 자체λ₯Ό λ°”κΎΈλŠ”κ±΄ 쑰금 생각해볼 μ—¬μ§€κ°€ μžˆλ‹€.
save, update, delete μ •λ„λ§Œ JDBC λ₯Ό μ‚¬μš©ν•΄λ„ 되긴 ν•œλ‹€. 이 뢀뢄은 μ–΄λŠμ •λ„ μ·¨ν–₯ 차이가 μžˆμ„κ²ƒ κ°™λ‹€.

μ„Έλ²ˆμ§Έ 방법.
save() μ—μ„œ μ‚¬μš©λ˜λŠ” λ©”μ„œλ“œ 듀을 λ‚΄κ°€ 직접 κ΅¬ν˜„ν•˜μ—¬ μ‚¬μš©ν•˜λ„λ‘ ν•œλ‹€.
이 방법을 이야기 ν•˜κΈ° μœ„ν•΄ μ§€κΈˆκΉŒμ§€ λ§Žμ€ 과정을 μ„€λͺ…ν–ˆλ‹€κ³  해도 λ¬΄λ°©ν•˜λ‹€.
사싀 이걸 μ˜λ„ν•œκ±΄μ§€ 잘 λͺ¨λ₯΄κ² μœΌλ‚˜, Persistable 을 직접 κ΅¬ν˜„ν•˜μ—¬ μ‚¬μš©ν•  수 μžˆλ‹€.

Persistable 은 μ•„λž˜μ™€ 같이 μ„ μ–Έλ˜μ–΄ μžˆλŠ” interface λ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Persistable<ID> {

    /**
    * Returns the id of the entity.
    *
    * @return the id. Can be {@literal null}.
    */
    @Nullable
    ID getId();
    
    /**
    * Returns if the {@code Persistable} is new or was persisted already.
    *
    * @return if {@literal true} the object is new.
    */
    boolean isNew();
}

이걸 UserEntity μ—μ„œ κ΅¬ν˜„μ„ ν•˜κ²Œ 되면 μ•„λž˜μ™€ 같이 κ΅¬ν˜„ν•  수 μžˆλŠ”λ°.

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
@Entity
@Table(name = "users")
class UserEntity(
    @Id
    // @GeneratedValue(strategy = GenerationType.IDENTITY) ν•΄λ‹Ή 뢀뢄을 주석 처리 λ˜λŠ” μ‚­μ œ
    val id: Long = 0L,
    @Column(nullable = false)
    val username: String,
    @Column(nullable = false)
    val password: String
) : Persistable<Long> {
    @Transient
    private var isNew = true

    override fun isNew(): Boolean = isNew

    override fun getId(): Long? = id

    @PrePersist
    @PostLoad
    private fun markNotNew() {
        isNew = false
    }
}

interface UserRepository : JpaRepository<UserEntity, Long>

μ΄λ ‡κ²Œ κ΅¬ν˜„μ„ ν•˜κ²Œ 되면 SimpleJpaRepository μ—μ„œ save() λ©”μ„œλ“œλ₯Ό 호좜 ν–ˆμ„λ•Œ isNew() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜κ²Œ λ˜λŠ”λ°
AbstractEntityInformation 의 isNew() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ
JpaPersistableEntityInformation 의 isNew() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜κ²Œ λœλ‹€.

μ™œ JpaPersistableEntityInformation 을 μ‚¬μš©ν•˜λŠ”μ§€λŠ” μ’€ 더 λ‚΄λΆ€μ μœΌλ‘œ 봐야 ν•˜λŠ”λ° κ°„λ‹¨ν•˜κ²Œ μ„€λͺ…ν•˜μžλ©΄
Persistable 을 κ΅¬ν˜„ν•˜μ§€ μ•Šμ€ μ—”ν‹°ν‹°μ˜ κ²½μš°λŠ” κΈ°λ³Έ κ΅¬ν˜„μ²΄μΈ AbstractEntityInformation 을 ν˜ΈμΆœν•˜κ²Œ 되고
κ΅¬ν˜„ν•œ κ²½μš°μ—λŠ” JpaPersistableEntityInformation κ°€ 호좜 λ˜λ„λ‘ Spring Data JPA λ‚΄λΆ€ 둜직이 μž‘μ„±λ˜μ–΄ μžˆλ‹€κ³  μƒκ°ν•˜λ©΄ λœλ‹€.

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
public class JpaPersistableEntityInformation<T extends Persistable<ID>, ID>
        extends JpaMetamodelEntityInformation<T, ID> {
    
    /**
    * Creates a new {@link JpaPersistableEntityInformation} for the given domain class and {@link Metamodel}.
    * 
    * @param domainClass must not be {@literal null}.
    * @param metamodel must not be {@literal null}.
    * @param persistenceUnitUtil must not be {@literal null}.
    */
    public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel,
        	PersistenceUnitUtil persistenceUnitUtil) {
        super(domainClass, metamodel, persistenceUnitUtil);
    }
    
    @Override
    public boolean isNew(T entity) {
        return entity.isNew();
    }
    
    @Nullable
    @Override
    public ID getId(T entity) {
        return entity.getId();
    }
}

μ—¬κΈ°μ„œ entity.isNew() λ₯Ό ν˜ΈμΆœν•˜κ³  return ν•˜λŠ”λ° 이게 λ°”λ‘œ Persistable 의 isNew() λ©”μ„œλ“œλ‹€.
우린 Persistable 을 UserEntity μ—μ„œ κ΅¬ν˜„ν•˜κ³  있기 λ•Œλ¬Έμ— μš°λ¦¬κ°€ κ΅¬ν˜„ν•œ isNew() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ μ‚¬μš©ν•˜κ²Œ λœλ‹€.

그럼 UserEntity μ—μ„œ κ΅¬ν˜„ν•œ Persistable κ΄€λ ¨ λ©”μ„œλ“œμ™€ markNotNew λ©”μ„œλ“œμ— λŒ€ν•΄ μ•Œμ•„λ³΄μž.

ν•˜λ‚˜μ”© μ„€λͺ… ν•˜μžλ©΄

  1. isNew : true, false 의 값을 μ €μž₯ν•˜κ³  μžˆλŠ” ν”„λ‘œνΌν‹°
  2. isNew() : isNew ν”„λ‘œνΌν‹°μ˜ 값을 λ°˜ν™˜ν•˜λŠ” λ©”μ„œλ“œ, μ—¬κΈ°μ„œ true λ₯Ό λ°˜ν™˜ν•˜λ©΄ insert λ₯Ό false λ₯Ό λ°˜ν™˜ν•˜λ©΄ update κ°€ λ™μž‘ν•˜κ²Œ λœλ‹€.
  3. getId() : ν˜„μž¬ μ—”ν‹°ν‹°μ˜ μ‹λ³„μž(id) λ₯Ό λ°˜ν™˜ν•˜λŠ” λ©”μ„œλ“œ
  4. markNotNew() : 이게 정말 μ€‘μš”ν•œ λ©”μ„œλ“œμΈλ°, 이건 isNew ν”„λ‘œνΌν‹°μ˜ 값을 μ„€μ •ν•΄ μ£ΌλŠ” λ©”μ„œλ“œλ‘œ isNew κ°€ 항상 같은 값을 λ°˜ν™˜ν•˜κ²Œ 되면 insert λž‘ update 쀑 ν•œκ°€μ§€ λ™μž‘λ§Œ ν•˜κ²Œ 되기 λ•Œλ¬Έμ— κ·Έκ±Έ λ°©μ§€ ν•˜κΈ° μœ„ν•œ λ©”μ„œλ“œ

λ™μž‘μ„ 흐름에 따라 μ‚΄νŽ΄λ³΄μžλ©΄

  1. UserEntity 생성 -> isNew : true
  2. userRepository.save(enttiy) 호좜
  3. JpaPersistableEntityInformation.isNew(entity) 호좜
  4. isNew() β†’ true β†’ persist() (INSERT μˆ˜ν–‰) -> isNew : true
  5. INSERT 직전에 @PrePersist β†’ markNotNew() 호좜 -> isNew : false
  6. 이후 DB 쑰회 μ‹œ @PostLoad β†’ markNotNew() 호좜 -> isNew : false

4번 λ™μž‘ 이후 isNew κ°€ false 둜 λ³€κ²½λ˜κΈ° λ•Œλ¬Έμ— 그리고 이후 DB μ—μ„œ 쑰회 μ‹œ isNew κ°€ false 둜 λ³€κ²½ 되기 λ•Œλ¬Έμ— μ΅œμ΄ˆμ—λ§Œ insert κ°€ λ°œμƒν•˜κ³  μ΄ν›„μ—λŠ” update κ°€ λ°œμƒν•˜κ²Œ λœλ‹€.

μ΄λ ‡κ²Œ κ΅¬ν˜„ν•˜κ³  ν…ŒμŠ€νŠΈ μ½”νŠΈλ₯Ό 톡해 직접 쿼리가 μ–΄λ–»κ²Œ λ°œμƒλ˜λŠ”μ§€ μ‚΄νŽ΄λ³΄μž.

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
@DataJpaTest
class PersistableTest {
    @Autowired
    private lateinit var entityManager: TestEntityManager

    @Autowired
    private lateinit var userRepository: UserRepository
    
    @Test
    @DisplayName("Persistable κ΅¬ν˜„ ν–ˆμ„λ•Œ")
    fun persistableTest() {
        val stats: Statistics =
            entityManager.entityManager
                .unwrap(Session::class.java)
                .sessionFactory
                .statistics.also {
                    it.isStatisticsEnabled = true
                    it.clear()
                }

        // UserEntity() λ₯Ό 생성해 쀄 λ•Œ ID 값을 직접 μ„€μ •ν•΄μ£Όκ³  μžˆλŠ” λͺ¨μŠ΅
        val entity = UserEntity(id = 1L, username = "test_user", password = "1234")
        userRepository.save(entity)
        userRepository.flush()

        val queryCount: Long = stats.prepareStatementCount
        println("Query Count: $queryCount")

        Assertions.assertThat(queryCount).isGreaterThanOrEqualTo(1)
    }
}

ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” Persistable 을 κ΅¬ν˜„ν•˜μ§€ μ•Šμ•˜μ„λ•Œ μž‘μ„±ν•œ μ½”λ“œμ™€ Assertions λΆ€λΆ„ 만 μ œμ™Έν•˜κ³  λ™μΌν•˜λ‹€.

1
2
3
4
5
6
7
8
Hibernate: 
    insert 
    into
        users
        (password, username, id) 
    values
        (?, ?, ?)
Query Count: 1

둜그λ₯Ό 확인해 보면 Persistable 을 κ΅¬ν˜„ν•˜μ§€ μ•Šμ•˜μ„λ•Œ λ°œμƒν–ˆλ˜ select κ°€ 사라진걸 λ³Ό 수 μžˆλ‹€.

μ„Έλ²ˆμ§Έ 방법인 β€œPersistable 을 직접 κ΅¬ν˜„β€ 을 ν™œμš©ν•˜κ²Œ 되면 μ‘°κΈˆμ€ λ³΅μž‘ν•˜μ§€λ§Œ Spring Data JPA 의 κΈ°λŠ₯을 λͺ¨λ‘ ν™œμš©ν•  수 μžˆμœΌλ©΄μ„œ λΆˆν•„μš”ν•œ 쿼리 λ°œμƒμ„ 쀄일 수 μžˆλŠ”κ²Œ λœλ‹€.

3κ°€μ§€ 방법듀 쀑 정닡은 μ—†λ‹€. μƒν™©μ΄λ‚˜ μ·¨ν–₯에 맞게 방법을 μ„ νƒν•˜κ³  ν™œμš©ν•˜λ©΄ 쒋을것 κ°™λ‹€.

μ•„ 참고둜 β€œUserEntity 외에 λ‹€λ₯Έ 수 λ§Žμ€ Entity κ°€ μžˆλŠ”λ° 이걸 λͺ¨λ‘ κ΅¬ν˜„ν•΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•˜λ‚˜μš”?” λΌλŠ”
질문이 μžˆμ„ 수 μžˆλŠ”λ°, @MappedSuperclass λ₯Ό μ‚¬μš©ν•˜μ—¬ BaseEntity λ₯Ό λ§Œλ“€κ³  κ·Έκ±Έ 상속 λ°›λŠ” ν˜•νƒœλ‘œ κ΅¬ν˜„ν•˜λ©΄ λͺ¨λ“  Entity 에 직접 κ΅¬ν˜„ν•  ν•„μš”κ°€ μ—†μ–΄μ§€κ²Œ λœλ‹€.

κ°„λ‹¨ν•˜κ²Œ μ‚¬μ΄λ“œ ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©μ€‘μΈ BaseEntity 의 μ½”λ“œλ₯Ό 보면 μ•„λž˜μ™€ κ°™λ‹€.

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
@MappedSuperclass
internal abstract class BaseEntity() : Persistable<Long> {
    @Id
    val id: Long = SnowflakeIdCreator.nextId()

    @Column(nullable = false)
    val createdAt: Instant = Instant.now()

    @Column(nullable = false)
    var updatedAt: Instant = Instant.now()

    @Column(nullable = false)
    val createdBy: Long = 0L

    @Column(nullable = false)
    var updatedBy: Long = 0L

    @Transient
    private var isNew = true

    override fun isNew(): Boolean = isNew

    override fun getId(): Long? = id

    @PrePersist
    @PostLoad
    private fun markNotNew() {
        isNew = false
    }
}

πŸ’‘ 마무리

사싀 이 글은 Persistable 의 κ΅¬ν˜„ 방법을 μ„€λͺ…ν•˜λŠ” κΈ€ 이기도 ν•˜μ§€λ§Œ, ν•™μŠ΅ν• λ•Œ μ‚¬μš©ν–ˆλ˜ 방식듀이 μ•„λ‹Œ λ‹€λ₯Έ λ°©μ‹μœΌλ‘œ Spring Data JPA λ₯Ό μ‚¬μš©ν–ˆμ„λ•Œ κ³ λ €ν•  뢀뢄이 μžˆλ‹€λŠ” λ‚΄μš©μ„ ν•¨κ»˜ μ•Œλ €μ£Όκ³  μ‹Άμ–΄ μž‘μ„±ν•œ 글이닀.
λ‹¨μˆœνžˆ Select 쿼리 ν•˜λ‚˜κ°€ 더 λ°œμƒν•œκ²Œ 뭐가 큰 λ¬Έμ œλƒκ³  ν•  수 μžˆκ² μ§€λ§Œ, 별거 μ•„λ‹ˆλΌκ³  κ·Έλƒ₯ λ„˜μ–΄κ°€κΈ° λ³΄λ‹€λŠ”
μ™œ λ‚΄ μ˜λ„μ™€ λ‹€λ₯Έ λ™μž‘μ„ ν•˜κ²Œ λœκ±΄μ§€ 확인해보고 νŒŒμ•…ν•˜λŠ” μŠ΅κ΄€μ΄ κ°–λŠ”κ²Œ μ€‘μš”ν•œκ²ƒ κ°™λ‹€.
μΆ”κ°€λ‘œ κ·Έ ν˜„μƒμ„ μ–΄λ–»κ²Œ ν•˜λ©΄ κ°œμ„ μ‹œν‚¬ 수 μžˆμ„μ§€λ„ ν•¨κ»˜ μ‚΄νŽ΄λ³΄λ©΄ λ”λ”μš± 쒋을것 κ°™λ‹€.

μ˜€λžœλ§Œμ— Spring Data JPA λ‚΄λΆ€ μ½”λ“œλ₯Ό 디버깅 λͺ¨λ“œλ‘œ ν•˜λ‚˜μ”© λ”°λΌκ°€λ³΄λ©΄μ„œ μ‚΄νŽ΄λ΄€λŠ”λ°,
μ˜ˆμ „μ΄λž‘ 달라진 μ½”λ“œλ“€μ΄ μžˆλŠ”κ±Έ μ•Œμˆ˜ μžˆμ—ˆλ‹€. 정말 λΉ λ₯΄κ²Œ λ³€ν™”ν•˜λŠ” 기술 νŠΉμ„±μƒ λͺ¨λ“ κ±Έ νŒŒμ•…ν•˜κ³  μžˆμ„μˆœ μ—†μ§€λ§Œ
μ’…μ’… λ‚΄λΆ€ 둜직이 μ–΄λ–»κ²Œ λ˜μ–΄ μžˆλŠ”μ§€ μ‚΄νŽ΄λ³΄λŠ” μŠ΅κ΄€μ„ κ°–λŠ”κ²ƒλ„ 쒋을것 κ°™λ‹€.(일단 λ‚˜λΆ€ν„°)

This post is licensed under CC BY 4.0 by the author.