[Spring Data JPA] @GeneratedValue와 MySQL을 사용할 때 주의점

simhani1·2023년 11월 9일

Backend

목록 보기
1/6
post-thumbnail

상황

프로젝트를 진행하며 H2 DB를 사용하고 있었다. 대부분의 API를 완성하게 되어 프론트와 연동하기 위해 배포 계획을 세웠다. AWS의 RDS로 MySQL 서버를 구축했기에 기존 H2에서 MySQL로 데이터베이스를 변경했다. 그러나 MySQL로 DB를 변경하자 기존에 동작하던 API가 실패했고 아래와 같이 IdentifierGenerationException에러가 발생했다.

2023-11-10 00:55:42.143 ERROR 93898 --- [nio-8080-exec-1] o.hibernate.id.enhanced.TableStructure : could not read a hi value - you need to populate the table: hibernate_sequence
2023-11-10 00:55:42.152 ERROR 93898 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.orm.jpa.JpaSystemException: could not read a hi value - you need to populate the table: hibernate_sequence; nested exception is org.hibernate.id.IdentifierGenerationException: could not read a hi value - you need to populate the table: hibernate_sequence] with root cause

로그를 살펴보면 회원정보를 저장하기 위해 hibernate_sequence테이블에서 PK를 조회하면서 에러가 발생하고 있었다.
이는 엔티티의 Id 생성 전략과 관련한 문제였다.

@GenratedValue의 옵션

엔티티를 사용하기 위해 @Id어노테이션과 함께 작성하는 어노테이션이다. 하이버네이트는 식별자를 정의하는 세 가지 방법을 제공한다.

AUTO

기본 옵션이다. 이는 DB 방언 종류에 따라 하이버네이트가 자동으로 적절한 전략을 선택한다.

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    ...

SEQUENCE

시퀀스를 사용할 수 있는 DB라면 하이버네이트가 만드는 시퀀스를 사용하여 기본키를 생성한다. 만약 DB가 시퀀스 사용이 불가능하면 TABLE전략으로 바꿔서 동작한다.

@Entity
public class Member {
    @Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    ...

IDENTITY

기본 키 생성을 하이버네이트가 아닌 DB에 위임한다. 예를 들어 MySQL은 AutoIncrement를 사용하여 PK를 생성한다.

@Entity
public class Member {
    @Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    ...

JPA는 쓰기 지연 저장소에 쿼리를 모아놨다가 트랜잭션 커밋 시점에 모든 쿼리를 DB로 날린다. INSERT문도 커밋 시점에 날라간다. 그러나 MySQL의 AUTO_INCREMENT는 DB에 값을 INSERT한 이후 값을 알 수 있다. 따라서 이 전략을 사용하면 JPA는 엔티티를 영속화하는 시점에 바로 INSERT문을 DB로 날려 식별자를 조회하고 엔티티를 1차 캐시에 저장한다.

TABLE

기본 키 생성용 테이블을 별도로 사용한다. 별도 테이블을 사용하기 때문에 SequenceAutoIncrement를 지원 여부에 상관없이 모든 DB가 사용할 수 있는 전략이다. 그러나 기본키 테이블을 하나로 사용할 때 클라이언트의 수가 많아진다면 성능적으로 손해를 볼 가능성이 있다.

@Entity
public class Member {
    @Id
	@GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;
    ...

에러 발생의 원인?

H2를 사용할 때 JPA는 TABLE전략을 선택하여 동작했다. AutoIncrement를 지원하는 MySQL을 사용하게 된다면 JPA는 IDENTITY전략을 선택할거라 믿었던 것이 나의 잘못이었다. MySQL로 변경했지만 JPA는 TABLE전략을 선택하고 있었다. 따라서 애플리케이션을 로딩하고 Member 엔티티를 영속화하는 과정에서 PK를 조회했지만 기본 키 테이블은 아무런 데이터가 없었기에 에러가 발생한 것이다.
MySQL의 AutoIncrement를 사용하기 위해 기본키 생성 전략을 IDENTITY로 변경했고 정상적으로 동작할 수 있었다.

테스트 코드로 TABLE, IDENTITY 전략의 차이점을 살펴보자

테스트는 내장 H2를 사용하여 진행했다.

테스트 코드

@Slf4j
@SpringBootTest
class RepositoryTest {

    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private EntityManager em;

    @Test
    @Transactional
    void identity() {
        log.info("=============");
        Member member = new Member("테스트유저");
        memberRepository.save(member);
        System.out.println("memberID: " + member.getId());
        log.info("=============");
        em.flush();
    }
}

1. TABLE 전략

@Entity
@Getter
@AllArgsContructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;

    @Column(nullable = false)
    private String name;
}
  • 결과

2. IDENTITY 전략

@Entity
@Getter
@AllArgsContructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;
}

결과

기본키 생성전략 선택권을 하이버네이트에게 맡기는 것은 의도치 않은 문제를 발생시킬 수 있을 것 같다..

0개의 댓글