@Entity
public class TestEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
이렇게 생성한 Entity 클래스에서 ID 생성 전략과 채번은 어떻게 되는건지, 자세히 살펴보도록 하자.
@Id
package jakarta.persistence;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Specifies the primary key of an entity.
* The field or property to which the <code>Id</code> annotation is applied
* should be one of the following types: any Java primitive type;
* any primitive wrapper type;
* <code>String</code>;
* <code>java.util.Date</code>;
* <code>java.sql.Date</code>;
* <code>java.math.BigDecimal</code>;
* <code>java.math.BigInteger</code>.
*
* <p>The mapped column for the primary key of the entity is assumed
* to be the primary key of the primary table. If no <code>Column</code> annotation
* is specified, the primary key column name is assumed to be the name
* of the primary key property or field.
*
* <pre>
* Example:
*
* @Id
* public Long getId() { return id; }
* </pre>
*
* @see Column
* @see GeneratedValue
*
* @since 1.0
*/
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Id {}
@Id 는 JPA 엔티티의 기본 키(Primary Key)를 식별하기 위한 용도이다.
지원하는 타입은 다음과 같다.
자바 기본 타입 (primitive type)
기본 타입의 래퍼 클래스(primitive wrapper type)
String
java.util.Date / java.sql.Date
java.math.BigDecimal / java.math.BigInteger
또한, 필드와 프로퍼티 기반으로 적용을 할 수 있다.
필드 기반
@Entity
public class TestEntity {
@Id // @Id가 필드에 붙음
private Long id;
private String name;
}
프로퍼티 기반
@Entity
public class TestEntity {
private Long id;
private String name;
@Id // @Id가 Getter 메서드에 붙음
public Long getId() {
return this.id;
}
}
위와 같은 방식으로 기본 키(Primary Key)를 식별할 수 있도록 명시해주는 역할을 하게 된다. @Entity 를 선언해 JPA Entity 라는 것을 명시한 클래스에서는 반드시 @Id 가 필요하다.
@GeneratedValue
/**
* Provides for the specification of generation strategies for the
* values of primary keys.
*
* <p> The <code>GeneratedValue</code> annotation
* may be applied to a primary key property or field of an entity or
* mapped superclass in conjunction with the {@link Id} annotation.
* The use of the <code>GeneratedValue</code> annotation is only
* required to be supported for simple primary keys. Use of the
* <code>GeneratedValue</code> annotation is not supported for derived
* primary keys.
*
* <pre>
*
* Example 1:
*
* @Id
* @GeneratedValue(strategy=SEQUENCE, generator="CUST_SEQ")
* @Column(name="CUST_ID")
* public Long getId() { return id; }
*
* Example 2:
*
* @Id
* @GeneratedValue(strategy=TABLE, generator="CUST_GEN")
* @Column(name="CUST_ID")
* Long id;
* </pre>
*
* @see Id
* @see TableGenerator
* @see SequenceGenerator
*
* @since 1.0
*/
@Target({METHOD, FIELD})
@Retention(RUNTIME)
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 "";
}
@GeneratedValue 는 Javadoc 가장 첫 줄에 설명되어 있듯이, 기본 키 값에 대한 생성 전략에 대한 명세(specification)을 제공한다. 즉, 어떻게 기본 키 값을 생성할 것인지에 대한 정보를 제공해준다.
만약 @GeneratedValue 가 없다면, 기본 키를 직접 할당해야 한다.
두 가지 방식을 통해 기본키 생성 전략을 지정할 수 있다.
GenerationType 을 지정한다.
ENUM 값으로 5가지(TABLE, SEQUENCE, IDENTITY, UUID, AUTO) 방식을 제공한다.
사용자가 정의한 기본 키 생성기(generator)를 지정한다.
@SequenceGenerator , @TableGenerator를 사용하는 경우
별도로 아이디값 지정이 필요한 경우 (snowflake, uuid ...)
이번 글에서는 GenerationType 에 대해서만 살펴보도록 하자.
GenerationType
public enum GenerationType {
TABLE,
SEQUENCE,
IDENTITY,
UUID,
AUTO
}
Spring Boot 3 (Hibernate 6.x) 버전부터 GenerationType.UUID가 새로 도입되었다.
TABLE
데이터베이스 테이블을 사용하여 기본 키를 할당
영속성 제공자(persistence provider)가 기본 키를 할당
SEQUNCE
데이터베이스 시퀀스를 사용하여 기본 키를 할당
순차적으로 증가하는 고유한 값을 생성
IDENTITY
데이터베이스의 자동 증가(identity) 컬럼을 사용하여 기본 키를 할당
데이터베이스의 AUTO_INCREMENT 기능을 활용
UUID
전역적으로 고유한 식별자를 생성
AUTO
특정 데이터베이스에 적합한 전략을 영속성 제공자가 선택하여 기본 키를 할당
데이터베이스 리소스가 이미 존재하는 것을 기대하거나, 필요시 생성하려고 시도할 수 있음.
Hibernate의 식별자 생성 전략 내부 구현 분석
위의 5가지 전략 모두 GenerationTypeStrategy 인터페이스를 구현한 구현체로 존재한다. 구현체는 단순 명세 역할을 하며, 이를 싱글톤 패턴을 활용하여 구현되어있다.
example - IdentityGenerationTypeStrategy
이를 실제로 관리하는 주체는 StandardIdentifierGeneratorFactory 클래스이다. 클래스 내부에서 ConcurrentHashMap을 사용하여 각 GenerationType에 대응하는 식별자 생성 전략(GenerationTypeStrategy)을 관리한다.
StandardIdentifierGeneratorFactory.class
public class StandardIdentifierGeneratorFactory
implements IdentifierGeneratorFactory, BeanContainer.LifecycleOptions, Serializable {
// 생성 전략 관리 Map
private final ConcurrentHashMap<GenerationType, GenerationTypeStrategy> generatorTypeStrategyMap = new ConcurrentHashMap<>();
// ...
// 생성 전략 초기화 GenerationType : GenerationTypeStrategy(singleton instance)
private void registerJpaGenerators() {
generatorTypeStrategyMap.put( GenerationType.AUTO, AutoGenerationTypeStrategy.INSTANCE );
generatorTypeStrategyMap.put( GenerationType.SEQUENCE, SequenceGenerationTypeStrategy.INSTANCE );
generatorTypeStrategyMap.put( GenerationType.TABLE, TableGenerationTypeStrategy.INSTANCE );
generatorTypeStrategyMap.put( GenerationType.IDENTITY, IdentityGenerationTypeStrategy.INSTANCE );
generatorTypeStrategyMap.put( GenerationType.UUID, UUIDGenerationTypeStrategy.INSTANCE );
}
}
내부적으로 아래의 메서드를 통해 식별자 생성기를 생성하는 역할을 한다.
@Override
public IdentifierGenerator createIdentifierGenerator(
GenerationType generationType,
String generatedValueGeneratorName,
String generatorName,
JavaType<?> javaType,
Properties config,
GeneratorDefinitionResolver definitionResolver) {
final GenerationTypeStrategy strategy = generatorTypeStrategyMap.get( generationType );
if ( strategy != null ) {
return strategy.createIdentifierGenerator(
generationType,
generatorName,
javaType,
config,
definitionResolver,
serviceRegistry
);
}
throw new UnsupportedOperationException( "No GenerationTypeStrategy specified" );
}
주어진 GenerationType 에 맞는 전략을 Map 에서 찾아 createIdentifierGenerator 메서드를 호출하게 된다. 이 과정을 통해 적절한 IdentifierGenerator 구현체가 생성되는 것이다.
위에서 언급했듯이 모든 식별자 생성기는 IdentifierGenerator 인터페이스를 구현한다. 이 인터페이스의 핵심 메서드는 generate 로 어떤 방식으로 생성할지 정하게 되는 것이다.
example - IncrementGenerator
public class IncrementGenerator implements IdentifierGenerator, StandardGenerator {
// ...
@Override
public synchronized Object generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
if ( sql != null ) {
initializePreviousValueHolder( session );
}
return previousValueHolder.makeValueThenIncrement();
}
}
synchronized 키워드를 통해 멀티스레드 환경에서 안전하게 작동되며, previousValueHolder 를 통해 이전 값을 기반으로 새 값을 생성하고 증가시킨다.
ID 생성 전략 초기화 흐름 정리
초기화 단계: 애플리케이션 시작 시 StandardIdentifierGeneratorFactory가 초기화되고, registerJpaGenerators() 메서드를 통해 각 GenerationType에 대한 전략이 Map에 등록
엔티티 매핑 단계: 엔티티 클래스의 @GeneratedValue 어노테이션에 지정된 strategy 값에 따라 적절한GenerationTypeStrategy 선택
식별자 생성기 생성 단계: 선택된 전략의 createIdentifierGenerator 메서드가 호출되어 실제 IdentifierGenerator 구현체가 생성
식별자 생성 단계: 엔티티가 영속화될 때 생성된 IdentifierGenerator의 generate 메서드가 호출되어 새로운 식별자 값이 생성
여기서 설명한 내용들은 스프링 부트가 시작될 때 Entity를 스캔하고 각 엔티티에 적절한 ID 생성 전략을 미리 구성하는 방식으로 동작하는 것이다.
EntityManagerFactory가 초기화되면서 EntityPersister 인터페이스의 getGenerator() 메서드를 통해 각 엔티티에 맞는 ID 생성기가 할당되게 된다.
ID 채번 테스트
위에서 확인했던 내용들을 기반으로 Break Point를 걸고, SAVE 호출시에 위의 설정 과정을 어떻게 활용하는지 파악해보자.
Entity
/**
* Identity Test Entity.
*
* @author Seungjo, Jeong
*/
@ToString
@Getter
@Entity
@Table(name = "identity_test")
@NoArgsConstructor
public class IdentityEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public IdentityEntity(String name) {
this.name = name;
}
}
엔티티를 삽입하기 위한 AbstractEntityInsertAction을 생성하고, handleGenerataedId 메서드를 통해 생성된 ID를 처리하게 된다.
4. AbstractSaveEventListener.handleGeneratedId()
private static Object handleGeneratedId(boolean useIdentityColumn, Object id, AbstractEntityInsertAction insert) {
if (useIdentityColumn && insert.isEarlyInsert()) {
if (insert instanceof EntityIdentityInsertAction) {
final Object generatedId = ((EntityIdentityInsertAction) insert).getGeneratedId();
insert.handleNaturalIdPostSaveNotifications(generatedId);
return generatedId;
}
else {
throw new IllegalStateException(
"Insert should be using an identity column, but action is of unexpected type: "
+ insert.getClass().getName()
);
}
}
else {
return id;
}
}
IDENTITY 전략을 통해 즉시 삽입이 필요한 경우(useIdentityColumn && insert.isEarlyInsert()), EntityIdentityInsertAction 에서 생성된 ID를 가져오게 된다. 이는 데이터베이스에서 INSERT 쿼리가 실행된 후 얻어진 값이다.
entityManager.persist(entity) 메서드 내부에서 정말 많은 동작이 수행된다. 보면 볼수록 신기하다.
결론
JPA를 사용하는 상황에서 save() 메서드 호출 시 내부적으로 일어나는 ID 생성 전략에 따른 채번 과정을 디버깅을 통해 하나씩 흐름을 따라가며 살펴보았다. 새로운 엔티티를 판별하는 로직만 알고 있었지만, 생성 과정은 더욱 복잡한 과정을 거쳐 ID 값이 생성되고 할당되는 것을 알게 되었다.
JPA의 ID 생성 메커니즘은 단순 ID 값 생성을 넘어 영속성 컨텍스트 관리, 트랜잭션 처리, 성능 최적화 등 다양한 측면과 밀접한 관계가 있어보인다. 이러한 정보를 기반으로 커스터마이징이나 별도의 최적화가 필요한 경우 잘 적용할 수 있지 않을까?