Hibernate 기본키 전략은 무엇을 사용해야 할까?

더기·2022년 4월 13일
0

JPA

목록 보기
3/3
post-thumbnail

Identity 전략 vs Sequence 전략


Identity 전략

auto-increment를 사용하기 때문에 데이터베이스에 insert 쿼리가 실행이 되어야 ID값을 알 수 있다.
그래서 JPA에서는 persist()시점에 즉시 insert 쿼리를 실행하고 데이터베이스에서 식별자를 조회하게 된다.
즉, 쓰기 지연의 이점을 누릴 수 없다.

Sequence 전략

시퀀스 전략은insert 쿼리와 상관없이 데이터베이스에서 시퀀스를 만들어서
그 시퀀스를 호출해서 사용하기 때문에 insert 쿼리가 나가지 않아도 ID를 생성할 수 있다.
이게 가능한 이유는 JPA가 데이버베이스에 있는 시퀀스를 내부적으로 조회하는 기능이 있기에 가능하다.
즉, 쓰기 지연의 이점을 누릴 수 있다.

시퀀스를 가져오기 위해서 DB 요청을 해야하는거 아닌가?

맞다. 시퀀스를 가져오기 위해 INSERT쿼리를 날리지는 않지만, 결국 시퀀스를 조회해야 한다.
그럼 100개의 insert를 날리면, 100번의 시퀀스 요청이 생기는 것이다.
결국 네트워크롤 계속 타기 때문에 성능에 영향이 있는거 아닌가? 라는 의문이다.

JPA는 이걸 initialValue와allocationSize로 해결했다.
allocationSize는 데이터베이스에 시퀀스를 설정개수만큼 미리 올려놓고,
그걸 애플리케이션에서 캐싱해놓고, 사용하고 다 쓰면 다시 설정 개수만큼 사용을 하는 방식으로 해결하였다.
initialValue는 시퀀스 초기값 또는 마지막으로 생성된 값이 기준이다.
만약 설정 개수가 1 ~ 50이면, 50개를 미리 올려놓고, 캐싱 후 사용하고 다 사용하면 51 ~ 100까지 올리고, 캐싱하여 사용하는 방식이다.
특이한 점은 시퀀스 콜을 두 번하게 된다. 이유는 시작점과 끝점을 알아야 하기 때문이다.

// call next value for sequence
// call next value for sequence

em.persist(member1); // 1, 51
em.persist(member2); // 메모리
em.persist(member3); // 메모리

// 51을 만나게 되면 다시 미리 끝지점을 다시 알아야 하기 때문에 끝 지점은 100을 알아야 하기 때문에
// 다시 call을 하게 된다.

그럼 allocationSize를 아주 크게 만개 정도 하면 안되나?

그러면 나쁘지는 않겠지만, 애플리케이션 서버를 내릴 경우 구멍이 생기므로 데이터베이스 입장에서 효율적이지 못하다.
그렇지만 구멍이 나도 크게 상관은 없다고 한다.

근데 동시성 이슈는?

미리 캐싱을 하면, 다른 쓰레드에서 사용하면 겹치는거 아닌가?

문제가 없다.

  • 단일 서버일 경우
    • 어차피 서버의 메모리 자체에 캐싱을 해두는 것이므로 모두 공유하므로 상관이 없다.
  • 다중 서버일 경우
    • 데이터베이스에 1 ~ 50까지 1번 서버가 받고 2번 서버는 51 ~ 100까지 받고 그렇기 때문에
      전혀 상관이 없다.

결론

데이터베이스에 맞춰서 사용하면 된다.

  • MySQL은 Identity
  • Oracle은 시퀀스

영속성 컨텍스트는 pk가 있어야 영속성 컨텍스트에 관리될 수 있기 때문에 반드시 pk가 필요하다.

Auto 전략을 쓰면 안되나?


https://jojoldu.tistory.com/295

스프링 부트는 Hibernate의 id 생성 전략을 그대로 따라갈지 말지 결정하는 useNewIdGeneratorMappings 설정이 있다.
스프링 부트2.0과 스프링 부트1.5는 설정이 다르다.

HibernateProperties.java(2.x)

/**
 * Whether to use Hibernate's newer IdentifierGenerator for AUTO, TABLE and SEQUENCE.
 * This is actually a shortcut for the "hibernate.id.new_generator_mappings" property.
 * When not specified will default to "true".
 */
private Boolean useNewIdGeneratorMappings;

private void applyNewIdGeneratorMappings(Map<String, Object> result) {
	if (this.useNewIdGeneratorMappings != null) {
		result.put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, this.useNewIdGeneratorMappings.toString());
	}
	else if (!result.containsKey(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS)) {
		result.put(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "true");
	}
}

javadoc 설명을 보면 hibernate의 id 생성 규칙을 따르고, 기본값이 true라는 것을 알 수 있다.

반면 부트 1.5는 디폴트가 false이고, false일 경우 hibernate5의 설정을 따라가지 않는다고 한다.
Hibernate5부터는 MySQL에서의 AUTO는 IDENTITY가 아닌 TABLE전략을 기본 전략으로 가져가게 된다.
Hibernate5이전에는 AUTO는 IDENTITY가 기본 전략이었다.

이런 이유로 부트 2.0 사용하고, Hibernate5 이상을 사용 시 TABLE 전략으로 적용되면서 시퀀스 증가가 안되는 버그가 발생할 수 있기 때문에
AUTO는 지양하는 것이 좋다.

정리

  • 스프링 부트는 Hibernate의 id 생성 전략을 그대로 따라갈지 말지 결정하는 useNewIdGeneratorMappings설정이 있다.
  • 부트 1.5에서는 기본값이 false, 2.0부터는 true이다.
  • HIbernate 5.0부터 MySQL의 AUTO는 IDENTITY가 아닌 TABLE 전략을 기본 전략으로 사용한다.
  • 즉, 1.5에서는 Hibernate5를 쓰더라도 AUTO를 따라가지 않기 때문에 IDENTITY가 선택되어 문제없음
  • 2.0에서는 true이므로 Hibernate5를 쓰면 TABLE이 선택되어 버그 발생

해결

  • application.properties/yml의 use-new-id-generator-mappings을 false로 설정한다
  • @GeneratedValue의 전략을 GenerationType.IDENTITY로 지정한다

개인적인 의견은 설정을 변경하는 것 보다는 Hibernate전략과 부트의 전략을 맞추는게 좋을 것 같아서
GenerationType.IDENTITY가 좋을 것 같다.
아무래도 버전이 변경되거나 할 경우 hibernate의 버전과 부트의 버전에 따라서 설정을 해주기는 번거롭기 때문에
명시적으로나 간편함나 더 유용할 것 같다고 생각해서이다.

profile
wwqew11

0개의 댓글