들어가며
안녕하세요~! 오늘은 오랜만에 다시 JPA 관련한 내용으로 찾아왔습니다 ^-^
DB 를 운영하다보면 다양한 Id 생성 전략에 대해 고민을 하고 어떻게 설계할 것인가 에 대해 많은 고민을 하게 되는데요
오늘은 JPA Hibernate 에서 제공하는 Id generator 들에 대해 알아보는 시간을 가지도록 해볼게요
Id Generator
다양한 Id 생성을 위한 기능들을 제공하지만, 하나씩 들어가다보면 정말 종류가 다양하답니다.
오늘 우리가 탐구할 항목은 바로 @GeneratedValue
Annotation 을 보려고 해요
public @interface GeneratedValue {
/**
* (Optional) The primary key generation strategy
* that the persistence provider must use to
* generate the annotated entity primary key.
*/
GenerationType strategy() default AUTO;
/**
* (Optional) The name of the primary key generator
* to use as specified in the {@link SequenceGenerator}
* or {@link TableGenerator} annotation.
* <p> Defaults to the id generator supplied by persistence provider.
*/
String generator() default "";
}
GenerationType 을 통해 어떻게 id 를 생성할 것인지 전략을 결정하고,
Sequence / Table 전략을 따른다면 generator 를 지정하여 생성하게 되네요
Gerneration Type | Description |
TABLE | DB Table 을 활용하여 Id 를 제공하는 전략을 사용합니다 |
SEQUENCE | DB Sequence 를 활용하여 Id 를 제공하는 전략을 사용합니다 |
IDENTITY | DB Id Column 에 지정된 전략을 사용합니다 |
UUID | RFC 4122 에 따른 UUID 생성 전략을 사용합니다 |
AUTO | Hibernate 가 자동으로 설정해준다 |
그럼 제일 궁금한 AUTO 부터 한번 살펴보러 갈까요?
Auto
어떤 마술로 Auto 를 구현해놓았을까요?
놀랍게도 Auto 는 3가지 방식중의 하나를 택하게 됩니다.
- Generator 에서 increment 로 지정한 경우 : Increment 전략을 사용
- Id column type 을 UUID 로 지정한 경우 : UUID 전략을 사용
- 그 외 : Sequence 전략을 사용
따라서 Auto 는 Hibernate 내부 코드 정책에 따라 달라지게 되므로, UUID 외에는 가급적이면 사용하지 않는게 안전해보이는 군요 ㅎㅎ
대신, 만약에 UUID 전략을 사용한다면 아래와 같이 사용하면 되겠네요
@Entity
@Table(name = "student")
class Student(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
val studentId: UUID
)
Identity
일반적인 경우 가장 많이 사용되는 방식인 Identity 이네요
@Entity
@Table(name = "student")
class Student(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val studentId: Long
)
Mysql 에서 id column 에 auto_increment 를 지정하여 DB table 을 생성하고,
Identity 를 명시함으로써 Auto increment 전략을 사용하게 되는 방식이에요
하지만 Identity 를 사용하게 되는 경우, 갖게되는 치명적인 단점이 하나 존재하게 됩니다
바로 hibernate 의 batch insert 기능을 사용할 수 없게 됩니다
왜 일까요?
The only drawback is that we can’t know the newly assigned value prior to executing the INSERT statement. This restriction is hindering the transactional write-behind flushing strategy adopted by Hibernate
대량의 INSERT 문이 동시에 실행되는게 batch insert 기능인데, Identity 전략은 INSERT 문의 실행시점에 Id 값을 정의할 수 있게 됩니다.
따라서, hibernate 의 기본 transction 전략인 write-behind(쓰기 지연) 방식을 제공할 수 없어 Identity 는 제약을 갖게 되는 것이죠
이제 이해가 되었네요 ^^;
Sequence
Sequence 전략은 Databse 에서 Sequence 방식을 제공할 때 사용가능하며, 만일 지원하지 않는다면 Table 전략으로 바꾸게 됩니다
예전 oracle database 가 대세일 때는 자주 사용되었지만, mysql 이 대세가 된 현재시점에서는 굳이 사용하지 않게 되네요
@Entity
@Table(name = "student")
class Student(
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence-generator")
@GenericGenerator(
name = "sequence-generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = [
Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "user_sequence"),
Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "1"),
Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "1"),
Parameter(name = SequenceStyleGenerator.OPT_PARAM, value = "pooled"),
]
)
val studentId: String
)
generator 를 GenericGenerator 를 통해 정의하고, 어떤 전략을 사용할 것인지 결정하면 되네요
몇 가지 중요한 parameter 들이 있는데 바로 INCREMENT_PARAM 과 OPT_PARAM 입니다
이는 hibernate 가 id 생성에 대해 성능을 끌어올리기 위해 메모리에 id 들을 미리 상주시키는 구현을 택했는데,
방식이 많아 Table 전략까지 소개하고 설명드릴까 하네요 ㅎㅎ
Table
Table 전략을 Sequence 와 많이 많이 유사해요. 단지 특정 Table 을 이용해 id 를 채택하게 되죠
@Entity
@Table(name = "student")
class Student(
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "table-generator")
@TableGenerator(
name = "table-generator",
table = "hibernate-id-table",
pkColumnName = "student_id",
valueColumnName = "student_id",
initialValue = 1,
allocationSize = 1,
)
val studentId: String
)
TableGenerator annotation 을 통해 조금 더 쉽게 만들어 볼 수 있어요
마찬가지로 allocationSize 를 직접 지정함으로써 얼만큼 메모리에 id 들을 상주시킬 것인지 결정하게 됩니다
그럼 이제 Table 전략과 Sequence 전략에서 가장 중요한 Optimizer 에 대해 알아볼게요
Optimizer
Hibernate 에서 id 생성에 대해 성능적인 부분을 보완하기 위해 메모리에 미리 채번하는 기술이 바로 Optimizer 이에요
종류가 되게 많죠? 이것도 하나씩 알아보려고 합니다 ㅎㅎ
Hibernate 에서 기본으로 제공하는 optimizer 는 바로 PooledOptimizer 입니다
크게 아래와 같은 로직이죠
- Increment size 가 1 이면 optimizer 를 사용하지 않는다
- hibernate.id.optimizer.pooled.preferred 를 지정하였으면 해당 전략을 사용한다
- 그 외에는 pooled optimizer 를 사용한다
그러면 하나하나씩 알아볼게요~!
Pooled
Pooled 방식은 기본적으로 hilo alogrithm 을 기반으로 하고, id 기준을 hi 로 지정하게 되는 방식이에요
쉽게 설명드리면 lo 과 hi 가 되는 시점에 allocationSize 만큼 미리 메모리에 채번하여 Database 에 반영시키는 것이죠
lo 가 4 이고, hi 가 4인 시점에 +3 만큼 Database 에 반영하고, hi 를 7로 지정하여 사용하게 되는 방식이에요
Pooled-lo
Pooled-lo 방식도 기본 방식은 pooled 와 유사하지만, id 기준을 lo 로 가져가게 되네요.
lo 가 4라는 것을 가정했을 때, id 가 [ lo, (lo + n) -1 ] 즉, [ 4, 6 ] 라는 범위내에 id 소진을 다하게 된 경우
id 가 6이 된 시점에 query 를 발생시켜 다음 id 범위를 메모리에 가져와 상주시키게 되네요
Pooled 방식들은 기본적으로 application 배포와 동시에 hi 값을 얻어오기 위해 무조건적으로 id query 를 1번 하게 되는 것이죠
또한 동시성 제어가 필요하기 때문에 RetrantLock 기반으로 동시 접근제어를 하고 있어 성능적인 단점들도 존재합니다
Pooled-lotl
이를 해결하기 위한 것이 바로 ThreadLocal 기반으로, 즉 Thread 단위로 id 들을 만들어놓는 것이죠
조금이라도 성능적인 부분을 보완하기 위해 Thread 단위로 id 를 제공하는 전략도 제공합니다
Hilo
Hilo 는 기본적으로 아래와 같은 정의를 갖고 있어요
- upperLimit = (databaseValue * incrementSize) + 1
- lowerLimit = upperlimit - incrementSize
따라서 databaseValue 와 별도로 id 체계를 따르게 되는 것이죠
dataBaseValue 가 8이기 때문에, upperLimit 은 25 가 되고, lowerLimit 은 22 가 되는 것이죠
Hilo 방식은 직관적이지 않고 연산이 들어가는 방식이라 헷갈려 기본적으로는 잘 사용하지는 않게 됩니다
정리하며
지금까지 Hibernate 에서 제공하는 모든 Id generator 방식과 디테일한 전략들을 알아보았네요 ㅎㅎ
물론 위에서 언급한 Id generator 들을 반드시 택1 하여 사용할 필요는 없어요
성능적으로 더 좋은 솔루션이 필요하다면 직접 구현하는 방안도 가능하니, 기본적으로 제공하는 것을 사용할 것이라면 각각의 장단점을 기반으로 사용하면 좋을 것 같네요
참고
Why does Hibernate disable INSERT batching when using an IDENTITY identifier generator
An Overview of Identifiers in Hibernate/JPA
Hibernate pooled and pooled-lo identifier generators
'Developer > Spring' 카테고리의 다른 글
[Test] Junit5 에서 제공하는 Tag 에 대해 알아보자 (0) | 2024.06.20 |
---|---|
Spring Boot API server 성능 테스트(performance test)를 해보자(with python locust) (0) | 2024.02.12 |
Micrometer Tracing 에 대해 알아보자(a.k.a. spring cloud sleuth) (0) | 2023.10.28 |
Spring 에서 Transactional 을 사용할 때 Exception 이 발생하는 상황에 주의하자 (1) | 2023.06.11 |
Spring WebFlux 에서는 어떻게 Kotlin Coroutine 을 지원하고 있을까? ( feat. Context ) (3) | 2023.05.27 |