잘 보지 않으면 헷갈리는 ID 에 대한 고찰
- -
첫 사진부터 놀래켜드려서 죄송하구요
JPA에서 ID를 할당하는 방법이 여러가지가 있죠, 뭐 수동 할당, 자동 할당, IDENTITY, SEQUENCE, TABLE, AUTO 등이 있는데요
이거 잘 보지 않으면 진짜 헷갈립니다
같이 한번 정리해보죠
스따뜨
수동 할당
수동 할당은 직접 ID를 채워 넣어주는 방식인데요, 한번 해보겠습니다
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Study {
@Id
private String id;
private String name;
}
이 엔티티를
@Transactional
@SpringBootTest
public class StudyTest {
@Autowired
StudyRepository studyRepository;
@Test
@Commit
void test() {
System.out.println("=====Start Test=====");
Study study = new Study("id", "name");
System.out.println("=====Before Persist=====");
studyRepository.save(study);
System.out.println("=====After Persist=====");
System.out.println("study.getId() = " + study.getId());
System.out.println("=====End Test=====");
}
}
이렇게 테스트를 돌려주면
=====Start Test=====
=====Before Persist=====
Hibernate: select s1_0.id,s1_0.name from study s1_0 where s1_0.id=?
=====After Persist=====
study.getId() = id
=====End Test=====
Hibernate: insert into study (name,id) values (?,?)
이렇게 나옵니다
여기서 의문점. 왜 select 쿼리가 나갔을까요?
그 이유는 저희가 ID를 수동할당 해주었기 때문입니다
ID가 수동할당 된 경우, Persistable<T> 인터페이스를 통해 구현된 isNew()가 동작합니다
실제로 JpaRepository 인터페이스의 구현체인 SimpleJpaRepository를 보시면 save는 다음과 같이 구현되어 있습니다
@Override
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
즉 새로운 엔티티인지 판정 후 새로운 엔티티면 persist를 호출하고 그렇지 않다면 merge를 호출하는 걸 볼 수 있습니다
merge의 경우 테이블에서 엔티티들을 싹 읽은 다음 해당 엔티티가 존재하면 그 엔티티를 업데이트하고 없으면 새로 추가하는데요, 이거 때문에 select 쿼리가 나간겁니다
Hibernate: select s1_0.id,s1_0.name from study s1_0 where s1_0.id=?
다시보면 실제로 id에 대해 select 쿼리가 실행됐죠?
그럼 수동할당을 하더라도 Persistable 인터페이스를 구현해주면 어떻게 될까요?
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Study implements Persistable<String> {
@Id
private String id;
private String name;
@Override
public boolean isNew() {
return true;
}
}
이렇게 임시로 어떤 객체가 들어오던간에 새로운 객체로 간주하도록 true를 리턴해줬습니다
그럼
=====Start Test=====
=====Before Persist=====
=====After Persist=====
study.getId() = id
=====End Test=====
Hibernate: insert into study (name,id) values (?,?)
이렇게 select 조회 없이 insert 쿼리 한번만 발생하게 됩니다
자동 할당
다음엔 자동 할당에 대해 알아보겠습니다
자동 할당엔 IDENTITY, SEQUENCE, TABLE, AUTO 등이 있습니다
IDENTITY
기본 키 생성을 DB에 위임하는 전략으로 엔티티 생성 시 쓰기 지연이 적용되지 않습니다
쓰기 지연은 영속성 컨텍스트에 변경이 발생했을 때, 바로 데이터베이스로 쿼리를 보내지 않고 SQL 쿼리를 버퍼에 모아놨다가, 영속성 컨텍스트가 flush 하는 시점에 모아둔 SQL 쿼리를 데이터베이스로 보내는 기능인데요
진짜 적용이 안될까요?
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Study {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Study(String name) {
this.name = name;
}
}
진짜?
=====Start Test=====
=====Before Persist=====
Hibernate: insert into study (name) values (?)
=====After Persist=====
study.getId() = 1
=====End Test=====
쓰기 지연이 적용되지 않고 바로 persist 실행 시점에 나가는걸 볼 수 있습니다
JPA에서는 엔티티를 영속시키기 위해선 식별자가 필요한데, 이 때 데이터를 insert하면서 자동 생성된 pk를 받아오기 때문입니다
그래서 커밋과 상관없이 일단 바로 insert가 쏴지는걸 볼 수 있습니다
다행히 아래 코드와 같이
Statement stmt = connection.createStatement();
stmt.executeUpdate(
"INSERT INTO member (name) VALUES ('홍길동')",
Statement.RETURN_GENERATED_KEYS
);
ResultSet rs = stmt.getGeneratedKeys();
if (rs.next()) {
long id = rs.getLong(1); // 생성된 PK
}
Statement.getGeneratedKeys()를 통해 생성된 pk를 받아올 수 있어서 별도의 조회 쿼리는 안나가는거 같더라구요
즉, 추가쿼리 없이 생성된 pk를 받아올 수 있고, 트랜잭션과 관련없이 em.persist가 호출되면 insert 쿼리가 실행됩니다
SEQUENCE
시쿼스 전략은 초기값과 한번에 증가할 크기를 설정할 수 있습니다. 그리고 어떤 시퀀스를 사용할지 @SequenceGenerator로 설정할 수 있습니다.
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Study {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
public Study(String name) {
this.name = name;
}
}
그럼 시퀀스 상태로 한번 실행해보겠습니다
=====Start Test=====
=====Before Persist=====
Hibernate: select next_val as id_val from study_seq for update
Hibernate: update study_seq set next_val= ? where next_val=?
=====After Persist=====
study.getId() = 1
=====End Test=====
Hibernate: insert into study (name,id) values (?,?)
아주 난리가 났습니다
우선 데이터베이스 시퀀스를 조회하고 그걸 가져와서 id를 할당한 다음에
이번엔 트랜잭션이 끝나고 데이터베이스에 저장되는 쓰기 지연이 적용됐습니다
근데 놀라운 사실
현재 실험하고 있는 디비는 MySQL인데요, MySQL은 SEQUENCE를 지원하지 않습니다
그럼 저건 뭐냐구요?
SEQUENCE를 지원하지 않기 때문에 TABLE 전략으로 SEQUENCE를 자체적으로 흉내낸 겁니다
만약 SEQUENCE를 지원한다면 이렇게 나올겁니다
=====Start Test=====
=====Before Persist=====
Hibernate: select next value for study_seq
=====After Persist=====
study.getId() = 1
=====End Test=====
Hibernate: insert into study (name,id) values (?,?)
즉 뒤에 나올 테이블 전략보다 실제로 통신을 한번 덜 하는 효과가 있습니다
TABLE
TABLE은 키 생성 전용 테이블을 만들어 시퀀스를 흉내내는 전략입니다
위에서 봤듯이
=====Start Test=====
=====Before Persist=====
Hibernate: select next_val as id_val from study_seq for update
Hibernate: update study_seq set next_val= ? where next_val=?
=====After Persist=====
study.getId() = 1
=====End Test=====
Hibernate: insert into study (name,id) values (?,?)
ID 전용 테이블을 만들어서 관리를 하는 방식입니다
값을 조회하면서 SELECT 쿼리를 사용하며 증가를 위해 UPDATE 쿼리를 사용합니다
SEQUENCE 전략보다 DB와 한번 더 통신한다는 점에서 성능이 안좋다는 단점이 있지만, 모든 DB에 적용할 수 있다는 장점이 있습니다
AUTO
AUTO 방식은 데이터베이스 방언에 따라서 IDENTITY, SEQUENCE, TABLE 중 하나를 자동으로 선택합니다
데이터베이스를 변경해도 코드를 수정할 필요가 없다는 장점이 있습니다
정리하자면
수동 생성 시 isNew에 따른 삽입 문제를 잘 고려하고, 자동 생성 시 적절한 전략을 골라서 사용하자
입니다
ID 자동 생성과 bulk insert와 관련하여 생기는 문제가 있는데
그건 다음에 말해보겠습니다
넵
마치겠습니다
'JPA' 카테고리의 다른 글
반년 만에 다시 이해한 N+1 문제 (6) | 2025.08.06 |
---|---|
엔티티가 비영속이었다가 영속됐다가 준영속됐다가 다시 영속됐다가 ...더보기 (1) | 2025.08.05 |
MultipleBagFetchException 해결할래 말래 (0) | 2025.06.28 |
N+1 문제 해결 후기 (0) | 2025.02.06 |
두 엔티티 간에 영속성 전이가 제대로 이루어지려면? (0) | 2025.02.06 |
소중한 공감 감사합니다