필요성
실무 프로젝트를 하다보면 Custom Id 에 대한 로직에 대한 요구사항을 마주합니다.
일부 Custom Id 는 string 만으로도 구현이 가능하기는 한데, UUID 나 UULD 외에도 DB 와 소통하는 방법이 필요한 순간이 있습니다.
배타적 락을 걸며 DB 의 sequence 에서 nextval 을 생성한 후 prefix, postfix 등을 붙이는 작업으로 Custom Id 를 생성할 수 있습니다. 혹은 Sequence 가 아닌 count 쿼리를 이용하여 nextval 을 생성할 수도 있습니다.
방법 적용 (단순 구글링)
1. Seuquence 에 postfix, prefix 를 다는 경우
많은 방법들이 있는데, 조사했을 떄 가장 깔끔한 방법은 SequenceStyleGenerator 를 상속하여 Custom 한 generator 를 구현하는 것입니다. 코드 예시는 아래와 같습니다.
public class CustomIdSequenceGenerator extends SequenceStyleGenerator {
@Override
public Serializable generate(SharedSessionContractImplementor session,
Object object) throws HibernateException {
return String.join("-",
"custom",
super.generate(session, object).toString() // 이 부분이 sequence 를 맡고 있음
);
}
@Override
public void configure(Type type, Properties params,
ServiceRegistry serviceRegistry) throws MappingException {
super.configure(LongType.INSTANCE, params, serviceRegistry);
}
}@Entity
public class CustomSequenceIdEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "custom_seq")
@GenericGenerator(
name = "custom_seq",
strategy = "com.study.idgenerator.CustomIdSequenceGenerator")
private String id;
}헌데 구글링시 이 SequenceStyleGenerator 를 쓰는 경우가 거의 없습니다. 보통은 IdentifierGenerator 를 사용하는데요, SequenceStyleGenerator가 IdentifierGenerator 를 상속한 것이었네요. IdentifierGenerator 를 제대로 확인해봅니다.
IdentifierGenerator
우선 IdentifierGenerator 는 순수하게 Hibernate 의 것으로 보입니다. JPA 에서 이러한 spec은 찾지 못했습니다. 블로그 글들을 참고해보면 Custom Id Generator 클래스를 만들 때 IdentifierGenerator 와 Configurable 두가지 인터페이스를 모두 구현해서 만들던데 현재 버전에서는 IdentifierGenerator 가 Configurable 을 extends 하고 있기 때문에 IdentifierGenerator 만 구현해도 됩니다.
SequenceStyleGenerator 내부는 어떠한가?
일단 SequenceStyleGenerator 는 IdentifierGenerator 를 구현한 ‘concrete class’ 이기에 IdentifierGenerator 를 가장 잘 구현한 모범 케이스일 것입니다. SequenceStyleGenerator 를 분석하며 사용의 모범사례를 생각해보았습니다.
configure 메소드
IdentifierGenerator.configure 함수에서는 jdbc dialect 값등을 가져와서 jdbc 환경을 마련하고, DB와 소통할 준비를 마칩니다. 그 후 databaseStructure 와 optimizer 라는 필드를 채워 넣는데, 이 databaseStructure 가 DB 에서 sequence 등을 조회하는 핵심 역할을 하게 됩니다. IdentifierGenerator.buildDatabaseStructure 에서는 db 가 sequence 기능을 물리적으로 제공하는 녀석인지 아닌지를 판단하고, table 에 구현하거나 실제 sequence 를 이용하는 방식으로 물리적인 DB 의 차이에 대해 추상화를 진행합니다. ( 해당 정보는 모두 dialect 속에 담겨 있습니다. )
generate 메소드
IdentifierGenerator.generate 함수 내부에서는 이전에 만들어둔 databaseStruecture 를 이용하여 sequence 값을 가져옵니다.
분석 결과 사실상 DB 를 잘 활용하도록 한 것이 전부란 걸 알았습니다. 보통 Custom Id Generator 를 만들면 JDBC connection 을 가지고와서 세션을 열어서 sequence 에 접근하는 로직을 구현하는데, 가장 스마트하고 hibernate 답게 구현한 것 같습니다.
IdentifierGenerator 를 이용하는 법
일단 이 Interface 를 이용하는 곳까지 쭉 파보기보다는 정확한 활용 방법을 파악하는 것이 목적이다. IdentifierGenerator 에서 configure 와 generate 메소드를 구현하면 된다.
configure 메소드
/**
* Configure this instance, given the value of parameters
* specified by the user as <tt><param></tt> elements.
* <p>
* This method is called just once, following instantiation, and before {@link #registerExportables(Database)}.
*
* @param type The id property type descriptor
* @param params param values, keyed by parameter name
* @param serviceRegistry Access to service that may be needed.
* @throws MappingException If configuration fails.
*/
@Override
default void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
}type 에는 Long.class, String.class 등 Id 의 type 이 있습니다. params 에는 해당 IdGenerator 를 여러 엔터티에서 활용하게 될 때 활용할만한 것들이 들어있습니다. 예를 들면 entity 이름, 혹은 sequence 를 이용한다면 sequence 이름 등이죠. 이후 Entity 에서 @GenericGenerator 에서 넣어주는 @Param 등 값을 넣어주면 여기 params 에서 받아집니다.
generate 메소드
generate 에는 session 이 있어서 이 session 을 이용해서 쿼리를 이용할 수 있습니다.
/**
* Generate a new identifier.
*
* @param session The session from which the request originates
* @param object the entity or collection (idbag) for which the id is being generated
*
* @return a new identifier
*
* @throws HibernateException Indicates trouble generating the identifier
*/
Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException;예시코드
@Entity
public class CustomIdEntity {
@Id @GeneratedValue(generator = "testGenerator")
@GenericGenerator(name = "testGenerator", strategy = "com.jjh2613.study.idgenerator.CustomIdGenerator")
private String id;
private String name;
}public class CustomIdGenerator implements IdentifierGenerator {
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
IdentifierGenerator.super.configure(type, params, serviceRegistry);
}
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return String.join("-", ((CustomIdEntity) object).getName(), "test");
}
}- test코드
@Test
public void test2() {
CustomIdEntity customIdEntity = new CustomIdEntity();
customIdEntity.setName("name1");
CustomIdEntity customIdEntity2 = new CustomIdEntity();
customIdEntity2.setName("name2");
entityManager.persist(customIdEntity);
entityManager.persist(customIdEntity2);
entityManager.getEntityManager().getTransaction().commit();
assertEquals("name1-test", customIdEntity.getId());
assertEquals("name2-test", customIdEntity2.getId());
}count 이용시 주의할 점
count 를 이용하는 방법은 query 를 날려 특정 조건에 대하서 count 하는 것인데, 충분히 쿼리로 구현할 수 있습니다. 헌데 이 경우 한 트랜잭션에서 N 개의 엔터티를 동시에 persist 하면 문제가 발생할 수 있습니다. Transaction의 단위에서 count 는 이미 쓰기가 된 경우에 대해서만 count 해오기 때문에 동일 트랜잭션 내에서 이전에 generate 된 id 가 중복되어 generate 될 수 있습니다.
예를 들면 이미 5개 row 가 있는 테이블에 대해 한 트랜잭션에서 2건의 row를 만들게 되면 1번째 것의 id 는 count 시 5를 기반으로 id 를 만들게 되고, 2번째 것도 마찬가지. count 시 6 이 아닌 5 를 기반으로 id 를 만들게 됩니다.
결국 count 쿼리를 사용했을 때에는 잘못된 코드가 만들어질 가능성이 있습니다. 정 필요하다면 적절한 transaction level 을 이용하거나, Entity 바깥 혹은 Entity 내부에서 따로 count 를 저장하고 있다가 혹은 repository 에서 count 해온 것을 @Transient 를 통해 Entity 에 주입하여 사용하면 됩니다. ( 허나 안타깝지만 이 방법은 서버 다중화시 전혀 의미가 없는 방법이 됩니다. )
결론
- 결론적으로
IdGenerator에서 count 쿼리 등 custom 쿼리를 활용할 일은 sequence 를 활용할 때를 제외하고는 없습니다. 그렇다면SequenceStyleGenerator를 이용하면 해당 케이스는 모두 커버될 것있니다. 굳이 구글링해서 많이 나오는 예제들처럼 Sequence를 날리는 쿼리를 직접 짤 필요가 없다는 의미이다. - Sequence 를 사용하는 경우가 아니라면
IdGenerator를 직접 가져다 만들 필요가 있는데 이때에도PersistenceIdGenerator라고 하는IdGenerator를 상속한 인터페이스를 활용하는 것이 좀 더 맥락에 맞는 것 같습니다. - Count 쿼리 등 커스텀한 쿼리를
IdGenerator내부에서 활용할 일은 없는 것이 낫다고 봅니다.
예상했던 것보다 생각할, 고민할거리가 많은 주제였네요.
참고
https://thorben-janssen.com/custom-sequence-based-idgenerator/
JPA 실무 적용시 ID 에 대한 세미나 :
https://www.youtube.com/watch?v=br_P5HSnSbE&ab_channel=springcamp.io