ID란?
JPA의 `Repository` 인터페이스는 두 가지 타입 인자를 받는다.
public interface Repository<T, ID> { }
- T: 도메인 엔티티
- ID: 도메인 클래스의 식별자(ID) 타입
JPA에서 ID는 엔티티의 식별자이다. `Repository` 인터페이스에는 이 ID를 기반으로 동작하는 여러 메서드들이 존재한다.
Optional<T> findById(ID id);
boolean existsById(ID id);
void deleteById(ID id);
이러한 메서드들은 엔티티의 ID를 참조해 DB 작업을 수행한다. ID는 데이터베이스의 기본 키와 매핑되어 엔티티의 고유성을 보장하고, 영속성 컨텍스트에서 엔티티를 식별하는 데 사용된다.
ID는 다음과 같은 특성을 갖는다:
- Uniqueness: 동일한 타입의 엔티티 간에 고유한 값
- Immutability: 생성 후 변경되지 않음
- Not-null: null이 아닌 값을 가짐
- Persistence: DB의 기본 키와 매핑됨
단순 식별자(Simple Identifiers)
ID를 정의하는 가장 간단한 방법은 `@Id` 애노테이션을 사용하는 것이다. `@Id`를 사용하면 자바의 기본형, 래퍼 클래스와 같은 참조형 타입 필드와 매핑된다.
@Entity
public class Product {
@Id
private int productId;
private String name;
public Product(String name) {
this.name = name;
}
}
위 예시에선 기본형 `int`타입을 ID로 사용했다. 기본형 타입의 특성상 명시적인 할당이 없다면 기본값 (여기서는 0) 을 갖는다. 이로 인해 다음과 같은 문제가 발생할 수 있다.
Product product = new Product("노트북");
// productId를 할당하지 않음
repository.save(product); // 0이라는 ID로 저장됨
하지만 데이터베이스에 이미 ID가 0인 `Product`가 있다면, 기존 데이터가 덮어씌워질 위험이 있다. 또 ID값이 0인 여러 엔티티가 저장될 경우 고유 무결성을 해칠 수 있다.
@Entity
public class Product {
@Id
private Integer productId;
private String name;
public Product(String name) {
this.name = name;
}
}
`int`의 래퍼 클래스인 `Integer`를 ID로 사용하면, 초기값은 `null`이 된다. 이 경우 ID를 명시적으로 할당하지 않고 엔티티를 저장하려면:
Product product = new Product("스마트폰");
// productId는 null 상태
repository.save(product); // IdentifierGenerationException 발생
JPA는 `null` ID값으로 엔티티를 저장할 수 없으므로 `IdentifierGenerationException`을 발생시킨다.
따라서, ID를 명시적 할당을 강제하려면 기본형 타입보단 래퍼 클래스(참조형)를 사용하는 것이 더 안전하다. 이 단순 식별자는 ID를 명시적으로 할당해야 하기 때문에 직접 할당 방식이라고도 한다.
생성된 식별자(Generated Identifiers)
JPA는 `@Id`와 `@GeneratedValue` 애노테이션을 함께 사용하면 데이터베이스에 엔티티를 저장할 때 기본 키 값을 자동으로 생성한다. 개발자는 직접 ID값을 할당할 필요가 없어 편리하다.
생성 유형(Generation Type)
`@GeneratedValue` 애노테이션에는 4 가지 유형이 있다:
- AUTO: 기본값, 데이터베이스와 데이터 유형에 따라 가장 적합한 전략을 자동 선택
- IDENTITY: 데이터베이스의 자동 증가 컬럼을 사용 (AUTO_INCREMENT)
- SEQUENCE: 데이터 베이스 시퀀스 객체 사용
- TABLE: 키 생성용 별도 테이블 사용
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GeneratedValue {
GenerationType strategy() default GenerationType.AUTO;
String generator() default "";
}
public enum GenerationType {
AUTO,
IDENTITY,
SEQUENCE,
TABLE
}
1. AUTO 생성
AUTO 생성 전략을 사용할 때, 영속성 제공자(JPA 구현체)는 기본 키 속성의 타입에 따라 타입을 결정한다. 이 타입은 숫자나 `UUID`일 수 있다. 숫자의 경우 `SEQUENCE` 또는 `TABLE`을 기반으로, `UUID` 값은 `UUIDGenerator`를 기반으로 생성된다.
@Entity
public class Student {
@Id
@GeneratedValue
private long studentId;
// ...
}
`UUIDGenerator`는 Hibernate 5부터 도입된 기능으로, `UUID` 타입의 ID에 `@GeneratedValue`를 사용하면 자동으로 `UUIDGenerator`를 호출한다.
@Entity
public class Course {
@Id
@GeneratedValue
private UUID courseId;
// ...
}
이는 " 8dd5f315-9788-4d00-87bb-10eed9eff566"와 같은 형식의 고유 식별자를 생성한다. 이는 `UUIDGenerator`의 `UUID.randomUUID()`메서드를 사용해 생성되며, 충돌 가능성이 매우 낮다.
`UUID`는 분산 시스템에서도 충돌 없이 고유성을 보장하고, 순차적이지 않아 보안에 강점을 갖는다.
동작방식
- Hibernate는 사용 중인 데이터베이스 방언(dialect)을 확인
- 데이터베이스의 유형과 ID 필드 타입에 따라 생성 전략을 선택
- 데이터베이스마다 다르게 처리:
- MySQL/MariaDB: `IDENTITY` or `TABLE`
- Oracle/PostgresSQL: `SEQUENCE`
- H2: `IDENTITY` or `SEQUENCE`
2. IDENTITY 생성
`IDENTITY` 생성 전략은 데이터베이스의 자동 증가(auto-increment) 기능을 사용하여 기본키를 생성한다. 이 방식은 새로운 레코드가 삽입될 때마다 고유한 식별자를 할당하고 그 값은 자동으로 증가한다. 이 전략을 사용하면 엔티티를 생성할 때 쓰기 지연이 적용되지 않는다. 왜냐하면 JPA에서 엔티티를 영속하기 위해선 식별자가 필요한데, IDENTITY 전략에서는 이 식별자(ID)가 DB에 저장된 후에만 할당되기 때문이다. ➡️ 엔티티를 생성할 때 즉시 INSERT 쿼리가 실행되어야 한다. `IDENTITY` 생성 전략을 사용하려면 `strategy`매개변수만 할당하면 된다:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long studentId;
// ...
}
동작 방식
- 엔티티가 `persist()`메서드로 저장될 때, Hibernate는 INSERT 쿼리를 실행한다.
- 데이터베이스는 자동 증가 컬럼을 통해 ID값을 생성한다.
- 생성된 ID값은 데이터베이스에서 객체로 다시 가져와 엔티티에 할당된다.
장점:
- 구현이 간단함
- 대부분의 데이터베이스에서 최적화
- 엔티티가 저장되는 즉시 ID값을 알 수 있음
`IDENTITY` 전략은 배치 처리를 비활성화한다는 단점이 있다. `IDENTITY` 전략은 엔티티가 데이터베이스에 저장된 후에야 ID를 알 수 있다고 위에서 언급한 적이 있다. 배치 처리를 위해선 여러 SQL문을 한 번에 데이터베이스로 보내야 하는데, `IDENTITY`전략은
INSERT 쿼리 ➡️ 데이터베이스에 엔티티 저장 ➡️ ID 반환 ➡️ 엔티티의 ID 할당 ➡️ 영속화
의 과정을 거치기 때문에, 쿼리를 하나씩 호출해 엔티티를 할해야 한다.
💡 엔티티를 디비에 저장하고 반환된 ID를 특정 자료구조에 저장했다가 한번에 각 엔티티에 ID를 할당하고 영속화 하면 안될까?
이 생각을 했다면 영속성 컨텍스트에 대한 이해가 부족한 것이다(내가 그랬다....).
영속성 컨텍스트는 엔티티를 식별할 수 있는 ID가 있어야 엔티티를 관리할 수 있다. ID를 나중에 할당한다면, 그 사이에 영속성 컨텍스트는 엔티티를 어떻게 관리해야 할지 알 수 없다.
또, 그렇기 때문에 JPA는 `persist()` 호출 후 엔티티는 즉시 영속 상태가 되어야 한다고 명시한다. ID를 나중에 할당한다면, JPA와 충돌이 발생할 것이다.
그렇기 때문에 `IDENTITY` 전략에선 엔티티를 생성할 때 쓰기 지연을 허용하지 않는다.
3. SEQUENCE 생성
데이터베이스가 `SEQUENCE`를 지원하는 경우 사용할 수 있다. 데이터베이스 시퀀스란, 유일한 값을 순차적으로 자동 생성하는 데이터베이스 객체이다. auto_increment와 달리 초기 값과 한번에 증가할 크기를 설정할 수 있다. 어떤 시퀀스를 사용할 것인지를 `SequenceStyleGenerator` 클래스로 설정할 수 있다.
동작 방식
- 시퀀스는 데이터베이스에 삽입하기 전에 ID값을 미리 얻을 수 있다.
- `persist()` 호출 시 Hibernate는 시퀀스에서 다음 값을 가져온다.
- 이 값을 엔티티에 할당한다.
- 실제 INSERT 쿼리는 트랜잭션 커밋 시점까지 지연되어(쓰기 지연) 여러 엔티티의 INSERT 쿼리를 한 번에 배치 처리할 수 있다.
@Entity
public class User {
@Id
@GeneratedValue(generator = "sequence-generator")
@GenericGenerator(
name = "sequence-generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = {
@Parameter(name = "sequence_name", value = "user_sequence"),
@Parameter(name = "initial_value", value = "4"),
@Parameter(name = "increment_size", value = "1")
}
)
private long userId;
// ...
}
`@GenericGenerator`란?
`@GenericGenerator`는 표준 JPA의 `@GeneratedValue`보다 더 세밀한 설정이 가능하다.
- name: 생성기의 이름을 지정한다. 이 이름은 `@GeneratedValue`의 generator 속성에서 참조된다.
- strategy: 사용할 생성기 클래스를 지정한다.
- parameters: 생성기의 다양한 매개변수를 할당한다.
- sequence_name: 사용할 데이터베이스 시퀀스의 이름
- initial_value: 시퀀스의 시작 값
- increment_size: 각 할당 시 증가할 값
💡시퀀스 이름을 명시적으로 지정하지 않으면 Hibernate는 서로 다른 엔티티에 동일한 sequence를 공유한다.
1. User 엔티티를 생성하면 ID가 1
2. Product 엔티티를 생성하면 ID가 2
3. 다시 User 엔티티를 생성하면 ID가 3
이처럼 엔티티 타입과 관계없이 순차적으로 ID가 할당된다.
시퀀스 이름을 지정하면 각 엔티티 타입마다 독립적인 시퀀스를 사용할 수 있다.
1. `user_sequence`를 사용하는 User 엔티티의 ID: 1, 2, 3....
2. `product_sequence`를 사용하는 Product 엔티티의 ID: 1, 2, 3....
엔티티 타입별로 ID를 관리하려면 시퀀스 이름을 명시적으로 지정하는 것이 좋다.
💡increment_size는 무슨 역할을 할까?
initial_value가 4이고 increment_size가 10이라면 첫 엔티티의 ID에 4를 할당하고 그 다음 엔티티의 ID에 14를 할당하는게 아니다.
[애플리케이션] -> "10개 ID 주세요" -> [데이터베이스]
[데이터베이스] -> "시작 ID는 4이고, 다음 시작은 14입니다" -> [애플리케이션]
애플리케이션은 4부터 13까지의 ID를 메모리에 캐싱하고, 최대 10개의 엔티티를 생성할 때까지 추가로 데이터베이스에 접근할 필요가 없다(increment_size가 1이라면 매번 새 엔티티를 만들 때마다 데이터베이스에 접근해야 한다.). 캐싱된 ID를 모두 사용하면 데이터베이스에 접근해 다음 ID들을 받아온다.
따라서, increment_size는 "한 번에 몇 개의 ID를 받아와 캐싱할 것 인가?"를 결정하는 값이다. 이를 통해 데이터베이스 접근 횟수를 줄일 수 있어 성능이 향상된다.
이러한 전략 방식 때문에 `SEQUENCE`는 쓰기 지연이 가능하고, `IDENTITY` 전략과 달리 배치 처리를 통한 성능 최적화가 가능하다.
4. TABLE 생성
데이터베이스에서 키 생성 전용 테이블을 만들어 시퀀스를 흉내내는 전략이다.
| seq_id | seq_value |
| Dept | 1 |
| User | 1 |
| Product | 1 |
- `seq_id`: 시퀀스를 식별하는 키 컬럼(엔티티 이름이나 임의의 식별자가 될 수 있음)
- `seq_value`: 현재 시퀀스 값을 저장하는 컬럼
동작 방식
- 새로운 엔티티를 생성할 때, Hibernate는 지정된 테이블에서 해당 엔티티 유형의 현재 시퀀스 값을 조회한다.(SELEC T 쿼리)
- 해당 값을 가져와서 엔티티의 ID로 할당한다.
- 테이블의 시퀀스 값을 증가시킨다(UPDATE 쿼리)
- 트랜잭션이 커밋될 때 실제 INSERT 쿼리가 실행된다.
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "table-generator")
@TableGenerator(
name = "table-generator",
table = "dep_ids", // ID 값을 저장할 테이블 이름
pkColumnName = "seq_id", // 시퀀스 식별자를 저장할 컬럼
valueColumnName = "seq_value", // 시퀀스 값을 저장할 컬럼
pkColumnValue = "department_id", // 이 엔티티의 시퀀스 식별자 값
initialValue = 1000, // 시작값
allocationSize = 50 // 한번에 50개 ID를 메모리에 할당
)
private long depId;
// ...
}
`@TableGenerator`란?
`@TableGenerator`는 키 생성 전용 테이블을 생성한다.
- name: 생성기의 이름을 지정한다. 이 이름은 `@GeneratedValue`의 generator 속성에서 참조된다.
- table: ID 값을 저장할 테이블 이름을 정한다.
- pkColumnName: 시퀀스 이름을 저장할 컬럼 이름(기본값 "sequence_name")
- valueColumnName: 시퀀스 값을 저장할 컬럼 이름(기본값 "next_val")
- pkColumnValue: 특정 엔티티 타입의 시퀀스를 식별하는 값(기본값은 엔티티 이름)
- initialValue: 시퀀스의 초기 값(기본값 0)
- allocationSize: 메모리에 캐싱할 시퀀스 값의 단위(`SEQUENCE`의 increment_size와 유사)
TABLE 전략은 모든 데이터베이스와 호환되기 대문에 시퀀스를 지원하지 않은 데이터베이스에 유용하다. `SEQUENCE` 전략과 유사하게 배치 처리가 가능하고 엔티티 타입별로 ID를 관리할 수 있다.
하지만, 타 전략에 비해 UPDATE 쿼리가 추가로 필요해 성능이 좋지 않고 여러 서버에서 동시에 같은 테이블에 접근할 때 문제가 발생할 수 있다.
일반적으로는 `SEQUENCE` 전략을 사용하고, 시퀀스를 지원하지 않는 데이터베이스의 경우 `TABLE` 전략을 고려하는 것이 좋다.
References
https://www.baeldung.com/hibernate-identifiers
https://www.maeil-mail.kr/question/69
매일메일 - 기술 면접 질문 구독 서비스
기술 면접 질문을 매일매일 메일로 보내드릴게요!
www.maeil-mail.kr
'Spring > Spring Data JPA' 카테고리의 다른 글
| [Spring Data JPA] @Transactional(readOnly = true)를 사용해야 하는 이유 (3) | 2025.08.14 |
|---|---|
| [Spring Data JPA] @OneToOne 연관관계에서 Lazy Loading이 동작하지 않는 경우 (2) | 2025.07.11 |
| [Spring Data JPA] JPA의 N + 1 문제 (0) | 2025.03.07 |
| [Spring Data JPA] 엔티티 매니저(Entity Manager) (0) | 2025.03.05 |
| [Spring Data JPA] 영속성 컨텍스트(Persistence Context) (0) | 2025.03.04 |