✍ 배경
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://www.concretepage.com/hibernate/example-generated-hibernate
https://vladmihalcea.com/how-to-map-calculated-properties-with-hibernate-generated-annotation/
'Spring > JPA' 카테고리의 다른 글
JPA @Lob 이해하기 (with MariaDB) (0) | 2024.01.06 |
---|---|
JPA에서 Lombok 사용시 주의할 점 (0) | 2023.12.30 |
Querydsl Paging & Count Query 최적화 (0) | 2023.11.11 |
[JPA] orphanRemoval 옵션을 사용할 때 Update 주의사항 (0) | 2023.10.28 |
@Transactional 특징, 사용방법, 주의점 (0) | 2023.08.15 |
댓글