본문 바로가기
Spring/JPA

[JPA] PK가 아닌 필드에 Auto Increment 사용하기

by 흑시바 2023. 12. 9.

✍ 배경

PK를 String으로 사용하는 테이블이 있는데, 별도 필드에 AUTO INCREMENT를 적용해야 하는 경우가 생기게 되었다. 흔하지 않은 상황이지만 JPA를 사용할 때 이런 경우 어떻게 해결해야 할지 찾아본 내용을 공유하고자 한다. 

🔎 테스트 환경

- MariaDB 10.6

- HeidiSQL

- Spring Boot 2.7.2

 

MySQL/MariaDB 외에 다른 데이터베이스를 사용하는 경우는 해당 방식이 적용되지 않을 수 있다는 점을 알아뒀으면 좋겠다. 하지만 방식은 거의 유사할거라고 생각한다.

🔎 공통

필자가 찾아낸 방식은 2 가지 인데, 이 방식들은 모두 공통적으로 처리해야 하는 부분이 있었다.

1. 데이터베이스에 시퀀스를 무조건 등록해야한다.

create sequence SHIBA_SEQ increment by 1 start with 1;

 

Auto Increment 기능이 필요한 필드에서 사용할 시퀀스를 하나 생성한다.

2. 하나의 로직 내에서 자동 증가한 값을 확인/사용하기 위해서는 반드시 Flush 해야 한다.

하나의 로직 내에서 Auto Increment가 적용된 Sequence 값을 즉시 확인하거나 사용해야 하는 경우가 있을 수 있다. 이런 경우는 반드시 Flush 처리를 해주어야 한다.

🖊️ 방법 1. @Generated 사용하기

첫 번째 방법은, @Generated 어노테이션을 사용하는 방법이다.

 

해당 방법을 사용하기 위해서는 대상 테이블 DDL을 직접 수정해야 한다.

 

CREATE TABLE `tb_generate` (
	`id` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_general_ci',
	`name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8mb4_general_ci',
	`seq` BIGINT(20) NULL DEFAULT nextval(SHIBA_SEQ),
	PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB;

 

`seq` DEFAULT nextval(SHIBA_SEQ)  = 시퀀스를 설정하는 부분이다.

 

JPA에서 기본적으로 제공하는 DDL Auto 기능을 사용해서 테이블을 생성해서는 적용되지 않는다. 따라서, 대상 테이블의 DDL을 직접 조작해야 한다.

 

방법으로는 위 코드처럼 테이블을 직접 추가해도 되고,

 

 

위 이미지처럼 표현식에 Sequence를 직접 등록하는 방식으로 설정할 수 있다.

 

@Entity
@Table(name = "tb_generate")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class GenerateEntity {

    @Id
    @Column(name = "id")
    private String id;

    private String name;

    @Generated(GenerationTime.INSERT)
    @Column(name = "seq", insertable = false)
    private Long seq;

    @Builder
    private GenerateEntity(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

그다음, Auto Increment 대상 필드를 엔티티에서 설정해야 한다.

 

Hibernate는 Insert 또는 행 Update 시 생성된 데이터베이스 값에 대한 엔티티를 업데이트하는 기능을 제공한다.

 

@Generated속성의 값이 데이터베이스에 의해 생성되도록 지정하는 어노테이션이다.

 

GenerateionTime (Hibernate 6부터 Deprecated) 은 특정 시점에 값이 지정되도록 할지 결정하는 것인데, INSERT, ALWAYS, NEVER 3가지 옵션을 제공한다. ( ALWAYS 설정을 하면 Insert, Update 시점을 모두 포함한다.)

 

주의할 점은, GenerationTime Insert를 사용할 경우 @Column에 insertable = false로 설정해서 옵션을 추가해야 하고, Always를 사용할 경우에는 insertable = false, updatable = false로 설정해야 한다고 한다.

 

어노테이션을 추가할 때 @Generated 어노테이션이 여러 개 있으므로 hibernate 패키지로 주의해서 설정하도록 하자.

 

@SpringBootTest
@Transactional
class GenerateEntityTest {

    @Autowired
    GenerateEntityRepository repository;

    @Test
    void generateTest() {
        GenerateEntity gen1 = GenerateEntity.builder()
                .id("TEST1")
                .name("테스트1")
                .build();

        GenerateEntity gen2 = GenerateEntity.builder()
                .id("TEST2")
                .name("테스트2")
                .build();

        GenerateEntity save1 = repository.saveAndFlush(gen1);
        GenerateEntity save2 = repository.saveAndFlush(gen2);

        System.out.println(save1);
        System.out.println(save2);
    }
}

 

간단하게 테스트 코드를 만들어서 실행시키면,

 

Sequence를 별도로 지정하지 않았어도 자동으로 seq 필드의 값이 추가되는 결과가 나오는 것을 확인할 수 있다.

🖊️ 방법 2. GeneratorType 활용하기

두 번째 방법은, @GeneratorType를 사용하는 것이다.

 

해당 방법을 사용하기 위해서는 ValueGenerator를 구현한 클래스를 생성해야 한다.

 

public class ShibaGenerator implements ValueGenerator<Long> {
    public Long generateValue(Session session, Object owner) {
        session.setHibernateFlushMode(FlushMode.COMMIT);
        return ((BigInteger) session.createNativeQuery("select nextval(SHIBA_SEQ)").getSingleResult()).longValue();
    }
}

 

FlushMode를 반드시 COMMIT 방식으로 해야 한다. 기본 방식인 AUTO를 사용하도록 하면 오버 플로우가 발생한다.

이후, 데이터베이스에 추가한 시퀀스 정보를 조회하는 쿼리를 작성한다.

 

...

    @GeneratorType(type= ShibaGenerator.class, when=GenerationTime.INSERT)
    @Column(name = "seq")
    private Long seq;

...

 

엔티티에서 생성한 구현체를 해당 필드에 등록한다.

참고로, 해당 방법에서 필드에 insertable = false, updatable = false 옵션을 사용해서는 안된다.

 

설정 후 1번 방법과 동일한 테스트 코드를 실행하면 같은 결과가 나오는 것을 확인할 수 있다.

🔎 차이점

@Generated = 대상 테이블에 표현식을 직접 등록해야 한다.

@GeneratorType  = 별도의 클래스를 생성해야 한다.

🔎 문제점

PK가 아닌 필드를 Auto Increment 하는 해당 방식에는 문제점이 존재한다.

1. 많은 쿼리 발생

테스트해보면서 쿼리가 1건당 3번씩 나가는 것을 확인할 수 있었다.

(위에서 소개한 2가지 방식은 서로 쿼리가 나가는 순서는 다르지만 결국 동일한 횟수가 나간다.)

 

이미지처럼 SELECT 1번, INSERT 1번, SELECT 1번 총 3번씩 나간다.

마지막 SELECT 쿼리는 대상 테이블과 Sequence가 동기화되어 있는지 확인하는 데 사용된다.

 

로직에 따라 속도 이슈가 있을 수 있으므로, 필요에 따라 최적화 과정이 필요하다.

2. 시퀀스를 관리하는 별도 테이블 생성

create sequence를 실행 시킴과 동시에 테이블 목록을 확인해 보면 시퀀스를 관리하는 별도의 테이블이 생기는 것을 확인할 수 있다. (MariaDB 기준)

🤔 결론

Sequence 생성 등 데이터베이스도 직접 다뤄야 하고, 엔티티 설정, 사용 시 주의해야 할 점 기억 등 여러 가지 고려를 해야 하는 만큼 불편한 것은 분명하다.

 

다른 설계를 고려해 보고, 반드시 별도의 Auto Increment가 사용되는 필드가 필요하다고 판단되는 경우에만 사용하는 게 좋을 것 같다.

📚 REFERENCE

https://docs.jboss.org/hibernate/orm/5.5/userguide/html_single/Hibernate_User_Guide.html#mapping-generated-Generated

https://www.concretepage.com/hibernate/example-generated-hibernate

https://stackoverflow.com/questions/60198528/is-it-possible-to-have-autoincrement-number-without-it-being-an-id

https://vladmihalcea.com/how-to-map-calculated-properties-with-hibernate-generated-annotation/

댓글