필요성

실무 프로젝트를 하다보면 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>&lt;param&gt;</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

https://zet-it-story.tistory.com/1