
개요
회사에서 프로젝트를 하던 중, JPA 기본 키 생성 전략 중 하나인 IDENTITY 전략을 사용했을 때, 쓰기 지연이 작동하지 않아 대량의 데이터를 INSERT할 때 많은 양의 트래픽이 발생했습니다.
이번 글에서는 왜 JPA IDENTITY 전략을 사용하면 "쓰기 지연"이 작동하지 않는지 공부한 내용을 정리하고자 합니다.
JPA의 쓰기 지연(Transactional Write-behind)
먼저 JPA의 쓰기 지연에 대해서 살펴보겠습니다.
JPA는 기본적으로 트랜잭션의 커밋 시점에 쿼리를 모아서 한 번에 보냅니다. 이를 "쓰기 지연"이라고 부릅니다.
@Transactional
public void saveMembers() {
Member member1 = new Member("회원1");
Member member2 = new Member("회원2");
Member member3 = new Member("회원3");
entityManager.persist(member1); // 쓰기 지연 저장소에 저장
entityManager.persist(member2); // 쓰기 지연 저장소에 저장
entityManager.persist(member3); // 쓰기 지연 저장소에 저장
// 트랜잭션 커밋 시점에 3개의 INSERT 쿼리가 한 번에 실행됨
}
쓰기 지연의 동작 원리는 다음과 같습니다.

- persist() 호출 시 엔티티를 영속성 컨텍스트의 1차 캐시에 저장
- PK를 키로 엔티티 객체를 저장 (예: {1: member1})
- 동시에 INSERT 쿼리를 쓰기 지연 SQL 저장소에 보관
- 트랜잭션 커밋 시점에 모아둔 쿼리를 한 번에 데이터베이스로 전송
이러한 방식은 다음과 같은 장점을 제공합니다.
- 성능 최적화: 여러 쿼리를 배치로 처리 가능
- 네트워크 호출 최소화: DB 왕복 횟수 감소
- 트랜잭션 일관성: 모든 변경사항을 한 번에 반영
IDENTITY 전략이란❓
IDENTITY 전략은 기본 키 생성을 데이터베이스에 위임하는 전략입니다. MySQL의 AUTO_INCREMENT, PostgreSQL의 SERIAL 등이 이에 해당합니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // DB가 자동으로 생성
private String name;
}
기본 키 자동 생성(@GeneratedValue) 전략과 차이점
| IDENTITY | 데이터베이스 (AUTO_INCREMENT) | ❌ |
| SEQUENCE | 데이터베이스 시퀀스 | ✅ |
| TABLE | 별도 키 관리 테이블 | ✅ |
| AUTO | DB에 따라 자동 선택 | DB 전략에 따름 |
[JPA] Entity란 무엇일까❓(Entity: 데이터베이스와 자바 객체 간의 다리)
자바 애플리케이션에서 데이터베이스와 상호작용할 때, Entity는 중요한 역할을 합니다. JPA(Java Persistence API)에서 Entity는 데이터베이스의 테이블과 자바 클래스 간의 매핑을 정의하며, 데이터베
pixx.tistory.com
IDENTITY 전략에서 쓰기 지연이 작동하지 않는 이유
1. 핵심 원인 : PK 값을 즉시 알아야 한다.
- JPA는 엔티티를 영속성 컨텍스트에 저장할 때 반드시 PK 값이 필요합니다.
영속성 컨텍스트(1차 캐시)는 내부적으로 Map 구조로 이루어져 있습니다.
- Key: @Id로 지정한 식별자 (PK)
- Value: 엔티티 객체 자체
Map에 데이터를 넣을 때 key 값이 null이면 저장할 수 없듯, JPA도 엔티티를 관리하기 위한 최소한의 조건인 PK가 없으면 영속 상태로 만들 수 없습니다.
2. IDENTITY 전략의 딜레마
문제는 IDENTITY 전략(MySQL의 AUTO_INCREMENT 등)의 특징에 있습니다. 이 방식은 "일단 DB에 데이터를 넣어봐야" 비로소 다음 PK 값이 무엇인지 결정됩니다.
- JPA 입장
- "얘를 영속성 컨텍스트에 넣으려면 지금 당장 PK가 필요해!"
- DB 입장
- "INSERT 쿼리를 날려주기 전까지는 PK가 뭔지 알려줄 수 없어."
즉, IDENTITY 전략의 문제는 PK 값을 데이터베이스에 INSERT를 해봐야만 알 수 있다는 점입니다.
3. 결국, 쓰기 지연을 포기
Member member = new Member("홍길동");
System.out.println(member.getId()); // null (아직 저장 전)
// 1. persist 호출 시점에 즉시 INSERT 실행! (쓰기 지연 X)
entityManager.persist(member);
// 2. DB가 생성한 ID를 JDBC가 즉시 받아옴 (Statement.getGeneratedKeys())
// 3. 받아온 ID를 엔티티의 @Id 필드에 채워넣음
System.out.println(member.getId()); // 1 (DB에서 생성된 값 확인 가능)
// 4. 이제 PK가 생겼으므로 영속성 컨텍스트에 정상 저장
persist()호출- 즉시 INSERT 쿼리 실행 (쓰기 지연 X)
- DB에서 생성된 PK 값을 조회
- 조회한 PK 값을 엔티티에 설정
- PK를 키로 사용하여 영속성 컨텍스트에 저장
이러한 이유로 IDENTITY 전략에서는 persist() 호출 즉시 INSERT가 실행되며, 쓰기 지연이 동작하지 않습니다.
SEQUENCE 전략과의 비교
그렇다면 JPA의 기본키 자동생성 전략 중 SEQUENCE 전략은 왜 쓰기 지연이 가능할까요❓
앞서 IDENTITY 전략이 ID를 알기 위해 즉시 INSERT를 날려야 했다면, 오라클이나 PostgreSQL에서 주로 사용하는 SEQUENCE 전략은 동작 방식이 완전히 다릅니다.
핵심 차이점 : INSERT 전에 ID를 미리 알 수 있다.
SEQUENCE 전략의 핵심은 DB에 실제 데이터를 넣기 전에, 번호만 따로 생성해주는 전용 객체(Sequence)가 존재한다는 점입니다.
entityManager.persist(member1);
entityManager.persist(member2);
- entityManager.persist(member1);
- DB 시퀀스 객체에 "다음 번호 하나 줘!"라고 요청 (SELECT NEXT VALUE)
- DB로부터 '1번'이라는 값을 받아 엔티티 ID에 할당
- 이제 ID(Key)가 생겼으니 영속성 컨텍스트에 저장 가능!
- INSERT 쿼리는 아직 날리지 않고 '쓰기 지연 저장소'에 보관 ✅
- entityManager.persist(member2);
- 같은 방식으로 '2번' 번호표를 미리 받아오고 쿼리는 저장소에 보관
IDENTITY vs SEQUENCE
두 전략의 결정적인 차이를 '번호표' 비유로 정리하면 이렇습니다.
- IDENTITY (식당 결제 방식)
- "일단 음식을 먹어봐야(INSERT) 얼마인지(ID) 알고 영수증을 처리할 수 있다." ➡️ 기다릴 수 없음.
- SEQUENCE (은행 번호표 방식)
- "입구에서 번호표(ID)를 먼저 뽑았으니, 실제 업무(INSERT)는 나중에 순서대로 처리해도 된다." ➡️ 쓰기 지연 가능.
따라서, SEQUENCE는 INSERT 전에 미리 PK 값을 알 수 있기 때문에 쓰기 지연이 가능합니다.
그렇다면, 그냥 SEQUENCE 전략을 쓰면되는거 아닌가 하는 생각이 듭니다.
그러나 SEQUENCE 전략은 모든 DBMS에서 사용할 수 있는 것은 아닙니다.
| 구분 | 지원 여부 |
| 지원함 | Oracle, PostgreSQL, DB2, H2 |
| 지원 안함 | MySQL, MariaDB, SQL Server (구버전) |
- MySQL이나 MariaDB는 시퀀스 객체 대신 AUTO_INCREMENT라는 방식을 사용하기 때문에, 기본적으로 IDENTITY 전략을 사용해야 합니다.
IDENTITY 전략의 문제
IDENTITY 전략의 가장 큰 성능 문제는 JDBC Batch Insert를 사용할 수 없다는 점입니다.
Batch Insert란❓
- Batch Insert는 여러 개의 INSERT 쿼리를 하나의 네트워크 호출로 묶어서 전송하는 최적화 기법입니다.
-- 일반적인 INSERT (3번의 네트워크 호출)
INSERT INTO member (name) VALUES ('회원1');
INSERT INTO member (name) VALUES ('회원2');
INSERT INTO member (name) VALUES ('회원3');
-- Batch INSERT (1번의 네트워크 호출)
INSERT INTO member (name) VALUES ('회원1'), ('회원2'), ('회원3');
IDENTITY 전략에서의 문제
@Transactional
public void saveMembers() {
for (int i = 1; i <= 1000; i++) {
Member member = new Member("회원" + i);
entityManager.persist(member);
// 매번 즉시 INSERT 실행 → 1000번의 DB 호출!
}
}
persist()호출마다 즉시 INSERT 실행- 각 INSERT마다 개별적인 네트워크 호출 발생
- Batch Insert 최적화 불가능
- 대량 데이터 저장 시 성능 저하
성능 비교
1000개의 엔티티를 저장하는 경우
| 전략 | 네트워크 호출 횟수 | 예상 시간 |
| IDENTITY (Batch 불가) | 1000번 | ~2-3초 |
| SEQUENCE (Batch 가능) | 10-20번 | ~0.2-0.5초 |
IDENTITY 전략의 문제 해결방법
1. SEQUENCE 전략으로 변경
@Entity
@SequenceGenerator(
name = "member_seq_generator",
sequenceName = "member_seq",
allocationSize = 50 // 성능 최적화
)
public class Member {
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "member_seq_generator"
)
private Long id;
private String name;
}
장점
- 쓰기 지연 가능
- Batch Insert 가능
allocationSize를 통한 추가 최적화 가능
단점
- MySQL 5.7 이하에서는 SEQUENCE 미지원
- DB 마이그레이션 필요
2. JDBC Batch Size 설정 (Sequence)
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 50 # 한 번에 50개씩 배치 처리
order_inserts: true # INSERT 순서 정렬
order_updates: true # UPDATE 순서 정렬
Hibernate 설정만으로 여러 개의 INSERT를 하나로 묶어 네트워크 비용을 줄일 수 있습니다.
단, 앞서 설명했듯, 이 설정은 IDENTITY 전략에서는 무용지물입니다. Hibernate가 ID를 확인하기 위해 매번 INSERT를 실행해야 하므로 배치가 작동하지 않습니다.
3. JDBC Template 직접 사용
MySQL/MariaDB 환경에서 대량의 데이터를 가장 빠르게 넣는 방법입니다. JPA를 거치지 않고 직접 SQL을 날려 쓰기 지연과 ID 조회 과정을 생략합니다.
@Repository
@RequiredArgsConstructor
public class MemberBulkRepository {
private final JdbcTemplate jdbcTemplate;
public void bulkInsert(List<Member> members) {
String sql = "INSERT INTO member (name) VALUES (?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, members.get(i).getName());
}
@Override
public int getBatchSize() {
return members.size(); // 한 번의 네트워크 통신으로 처리할 데이터 양
}
});
}
}
- JPA 영속성 컨텍스트를 거치지 않으므로 1
차 캐시 등에 의한 메모리 부하가 없습니다.- 단, DB에 직접 넣는 것이기 때문에, 실행 후 영속성 컨텍스트를 새로고침(clear)하거나 동기화에 신경 써야 합니다.
4. TABLE 전략 사용
모든 DB에서 시퀀스처럼 미리 번호를 따올 수 있도록 별도의 '키 생성 전용 테이블'을 만드는 전략입니다.
@Entity
@TableGenerator(
name = "member_id_generator",
table = "id_generator",
pkColumnValue = "member_id",
allocationSize = 50
)
public class Member {
@Id
@GeneratedValue(
strategy = GenerationType.TABLE,
generator = "member_id_generator"
)
private Long id;
}
- 장점
- IDENTITY와 달리 번호를 미리 가져올 수 있어 쓰기 지연과 Batch Insert가 가능해집니다.
- 단점
- 번호를 딸 때마다 id_generator 테이블을 업데이트해야 하므로, 아주 빈번한 삽입 시 DB 락(Lock)이나 오버헤드가 발생할 수 있습니다.
결론
IDENTITY 전략의 특징 요약
장점
- 설정이 간단함
- DB의 네이티브 기능 활용
- AUTO_INCREMENT와 자연스럽게 매칭
단점
- ❌ 쓰기 지연 불가능
- ❌ Batch Insert 불가능
- ❌ 대량 데이터 저장 시 성능 저하
권장 사항
- 소규모 애플리케이션: IDENTITY 전략 사용해도 무방
- 대량 데이터 처리 필요: SEQUENCE 전략으로 변경 권장 (단 지원 가능 DBMS만)
- MySQL 사용 중: MySQL 8.0+ 버전에서 SEQUENCE 지원 확인 후 전략 선택
- 이미 IDENTITY 사용 중: 대량 INSERT가 필요한 부분만 JDBC Template 활용
프로젝트의 요구사항과 데이터 규모를 고려하여 적절한 키 생성 전략을 선택하는 것이 중요합니다.
참고 자료
'Framework > JPA' 카테고리의 다른 글
| [Query DSL] Transform & GroupBy: 평면 조인 결과를 계층 구조로 쉽게 변환하기 (0) | 2026.01.09 |
|---|---|
| [JPA] JPA 방언(Dialect)이란❓ (1) | 2025.01.05 |
| [QueryDSL] QueryDSL - @QueryProjction 어노테이션이란❓ (0) | 2024.10.06 |
| [JPA] @Modifying 어노테이션이란 무엇일까❓ (1) | 2024.10.01 |
| [JPA] 프록시(proxy)객체란 무엇일까❓ (1) | 2024.09.15 |