Hello

hibernate @TableGenerator 사용기와 Optimizer 변경하기 본문

카테고리 없음

hibernate @TableGenerator 사용기와 Optimizer 변경하기

nari0_0 2023. 9. 11. 15:26
728x90

TABLE 전략 사용 테스트

기술 스택 
java 8

hibernate 5

 

이 방법을 사용해본적이 없어서 위 과정을 알아보다 어떻게 동작하는지 궁금해져서 테스트해보았습니다.

table을 생성해 sequence처럼 동작한다고 설명되어 있어 시작 값 : 1, 증가 값 : 50 을 가질 때,

A server : db update 50 using value [1~50]

B server : db update 100 using value [51-100]

처럼 동작할 것으로 기대 했는데 예상과 다르게 동작 했다.

test case 1)

A 호출 -> B 호출 -> A 호출 -> B호출

pk 관리 테이블

데이터 테이블

- date_end를 보면 3번이 7보다 늦게 생성되었다.

-  a서버가 1-5을 b서버가 6-10을 사용할 것을 예상했는데 다른결과가 나타났다.

 

test case 2)

A 호출 -> A호출 -> B호출 ->B호출

pk 관리 테이블

데이터 테이블

- b서버 시작 id가6으로 등록될 것이라고 예상했는데 다르게 7로 등록되었다.

@Entity
@Table(name = "tb_test1")
@TableGenerator(name = "tbGenerator",
        table = "tb_increment_id",
        pkColumnName = "name",
        valueColumnName = "val",
        allocationSize=5)
public class DateEntity {
    private Integer id;
    private LocalDateTime dateEnd;
    private String serverName;
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "tbGenerator")
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
    
    public LocalDateTime getDateEnd() {
        return dateStart;
    }

    public void setDateEnd(LocalDateTime dateEnd) {
        this.dateStart = dateEnd;
    }
    
    public String getServerName() {
        return serverName;
    }

    public void setServerName(String serverName) {
        this.serverName = serverName;
    }
}

위와 같이 동작하는 이유는 TableGenerator Hibernate에서 PooledOptimizer를 기본적으로 사용하기 때문입니다.

PooledOptimizer를 알아보기에 앞서 @TableGenerator에 대해 알아보겠습니다.

 

@TableGenerator 코드 일부

initialValue 의 default 0 인 것을 알 수 있습니다.

@TableGenerator 코드 일부

위 자바 코드에서 initialValue를 지정하지 않았기 때문에 default 0이 들어가게 됩니다.

근데, 데이터 테이블 이미지를 보면 디비에서 등록된 id 값 시작 번호는 1번으로 시작합니다.

이상하지 않나요?? 이 마법은 어노테이션 바인딩 할 때 일어납니다!

AnnotationBinder.buildIdGenerator

@TableGenerator일 때,  getIdGenerationTypeInterpreter().interpretTableGenerator()를 호출합니다.

이전에 작성한 JPA 기본키 생성 전략 글 에서 FallbackInterpreter를 사용해 id 전략을 선택하는 것을 작성했습니다.

FallbackInterpreter.interpretTableGenerator 내부를 보면 여기에 비밀이 숨어 있었습니다.

initialValue + 1 로 addParam을 하기 때문에 id 시작값이 1로 등록 된 것이었습니다.

주석에 HHH-4884 를 hibernate github에서 찾아보았지만 찾을 수없었다. 어디에 가야 있나~

FallbackInterpreter.interpretTableGenerator

 

그럼 initalValue가 뭔지 알게 되었으니, hibernate optimizer 에 대해 알아보겠습니다. hibernate는 기본적으로 다음과 같은 내장 최적화 프로그램이 함께 제공됩니다.

none
최적화가 수행되지 않습니다. 생성기에서 식별자 값이 필요할 때마다 데이터베이스와 통신합니다.

pooled-lo 
pooled-lo 최적화 프로그램은 증분 값이 데이터베이스 table/sequence 구조로 인코딩된다는 원칙에 따라 작동합니다.

poole
pooled-lo와 비슷하지만 여기서 테이블/시퀀스의 값이 값 풀의 상위 끝으로 해석된다는 점만 다릅니다.

hilo; legacy-hilo
table or sequence 의 단일 값을 기반으로 값 풀을 생성하기 위한 사용자 정의 알고리즘을 정의합니다.
이 optimizer는 사용하지 않는 것이 좋습니다. 이전에 이러한 전략을 사용했던 레거시 애플리케이션에서 사용하기 위해 여기에 유지 관리 및 언급됩니다.
애플리케이션은 계약에 정의된 대로 자체 최적화 전략을 구현하고 사용할 수도 있습니다 
org.hibernate.id.enhanced.Optimizer

TableGenerator과 SequenceGenerator은 기본 적으로 pooled optimizer를 사용합니다. OptimizerFactory.determineImplicitOptimizerName는 어떤 optimizer를 사용할지 선택합니다.

OptimizerFactory.determineImplicitOptimizerName

위 코드의 return을 보면, AvailableSettings.PREFER_POOLED_VALUES_LO 옵션이 true 경우에만 pooled_lo를 사용하고 나머지는 pooled를 사용하는 것을 알 수 있습니다.

StandardOptimizerDescriptor.fromExternalName

위에서 찾은 optimizer name으로 실제 옵티마이저를 결정하게 됩니다.

 

우리는 pk 시작값 : 1, optimizer : pooled를 사용하는 것을 알게 되었습니다. 그럼, pooled가 어떻게 동작하는지 한번 알아보겠습니다.

PooledOptimizer.generate

기본키 생성을위해 hibernate.Optimizer.generate가 호출 됩니다.

처음 호출
1) hiValue == null => true
1-1) callback.getNextValue를 통해 다음 값을 읽음 hiValue = 1
1-2) value를 넣는 조건문에서 hiValue == initialValue => true 통과 된다.
1-3) value = 1
return value.makeValueThenIncrement ( value++;)

두번째 호출
1) hiValue == null => false
1-1) value >hiValue => true (2 > 1)
1-2) callback.getNextValue 호출해 hiValue 갱신 hiValue = 5
1-3) value =  hiValue.subtract(5-1) ->value -= 4; value = 2
return value.makeValueThenIncrement ( value++;)


hiValue가 5가 아니라 1이 리턴되는 것에 의문이 들것입니다. 분명 증가 값으 5로 작성했는데 왜 시작값인 1이 리턴이 되었는지..!
이유는 TableGenerator.generate에 있습니다.!!

각각의 Generator는 AccessCallback 를 구현해 optimizer에서 호출할 수 있도록 합니다.

TableGenerator.generate new AccessCallback()

아래 이미지에서 표시된 부분을 보면 select, insert update 작업이 일어납니다.
차례대로 설명하면,
select 시 조회된 값이 없으면 insert 후 update
select 시 조회된 값이 있으면 update
작업을 진행합니다.

select 과정을 보면 조회된 값이 있을 때도 없을 때도 value.initalize로 값을 넣어주게 됩니다.
이떄, 최초 호출 시 value는 0이 담기게 됩니다. (initialValue -1)
update는 value를 변경하지 않고 db 값만 변경하게 됩니다.
return을 보면 value.increment()를 호출해 리턴하는데 이때 value++; 를 동작 하고나서 자신을 리턴해 1이 리턴됩니다.

TableGenerator.generate db처리

 

TableGenerator.generate return

pooled는 최초 호출 시 init value를 hiValue로 갖게되어 두번 째 호출이 발생했을 때 db에서 새로 값을 받아 hiValue를 변경하게 됩니다.

글이 좀 뒤죽 박죽으로 작성되어 있는데, 잘 정리해 놓은 블로그가 있어 가져 왔습니다.

위 블로그에 작성된 pooled optimizer following diagram을 가져와 봤습니다. 제가 위에서 장황하게 설명한것을 그림으로 깔끔하게 정리가 되어있습니다.

내용을 공부하면서 내 의도대로 key를 업데이트 하기위해서는 pooled-lo를 사용해야 한다는 것을 알게 되었습니다.

pooled-lo optimizer

이 방법은 pooled 와 다르게 db에서 읽은 값, limit 값, pk value 3개의 필드가 있으며 db에서 읽은 값과 limit값을 분리해 관리하고 있는점이 다릅니다.

 

1) lastSourceValue가 null 이거나 value>upperLimitValue 일 때 callback.getNextValue()를 호출
1-1) lastSourceValue = 1;
1-2) upperLimitValue = 1+5;
1-3) value =1;
return value ++

pooled-lo 적용 방법

1) properties를 통해 전역으로 설정

1-1) spring.jpa.properties.hibernate.id.optimizer.pooled.preferred=pooled-lo
1-2) spring.jpa.properties.hibernate.id.optimizer.pooled.prefer_lo=true
1-2 방법은 deprecated 되었습니다 1-1방법을 사용해야합니다.
/**
 * When using pooled {@link org.hibernate.id.enhanced.Optimizer optimizers}, prefer interpreting the
 * database value as the lower (lo) boundary.  The default is to interpret it as the high boundary.
 *
 * @deprecated Use {@link #PREFERRED_POOLED_OPTIMIZER} instead
 */
@Deprecated
String PREFER_POOLED_VALUES_LO = "hibernate.id.optimizer.pooled.prefer_lo";

/**
 * When a generator specified an increment-size and an optimizer was not explicitly specified, which of
 * the "pooled" optimizers should be preferred?  Can specify an optimizer short name or an Optimizer
 * impl FQN.
*/
String PREFERRED_POOLED_OPTIMIZER = "hibernate.id.optimizer.pooled.preferred";

2) genericGenerator를 통해 개별 설정

@TableGenerator에 작성하던 값을 TableGenerator.class에 매핑되년 필드에 값 넣을 수 있도록 설정

@TableGenerator에는 optimizer를 변경할 수 없지만 아래와 같이 작성하면 OPT_PARAM을 통해 사용하고 싶은 optimizer를 적용할 수 있다.!

     @Id
     @GenericGenerator(name = "tbGenerator1", strategy = "org.hibernate.id.enhanced.TableGenerator", parameters = {
            @org.hibernate.annotations.Parameter(name = org.hibernate.id.enhanced.TableGenerator.OPT_PARAM,value = "pooled-lo"),
            @org.hibernate.annotations.Parameter(name = org.hibernate.id.enhanced.TableGenerator.TABLE_PARAM,value = "tb_increment_id"),
            @org.hibernate.annotations.Parameter(name = org.hibernate.id.enhanced.TableGenerator.VALUE_COLUMN_PARAM,value = "val"),
            @org.hibernate.annotations.Parameter(name = org.hibernate.id.enhanced.TableGenerator.INITIAL_PARAM, value = "1"),
            @org.hibernate.annotations.Parameter(name = org.hibernate.id.enhanced.TableGenerator.INCREMENT_PARAM,value = "50")
    })
    @GeneratedValue(generator = "tbGenerator1")
    public Integer getId() {
        return id;
    }

 

참고 : https://vladmihalcea.com/hibernate-hidden-gem-the-pooled-lo-optimizer/

https://vladmihalcea.com/why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate/

https://lumme.dev/2022/04/03/hibernate-table-generator-optimizers.html

https://stackoverflow.com/questions/25204019/how-to-use-the-pooled-lo-optimizer-with-hibernate

https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#identifiers-generators-optimizer

 

 

 

728x90