JPA - Oracle Sequence
🧩 JPA에서 Oracle 시퀀스로 String 타입 ID 생성하기
JPA를 사용하다 보면 기본적으로 @GeneratedValue(strategy = GenerationType.SEQUENCE)를 이용해 Oracle의 시퀀스를 ID로 사용하는 경우가 많습니다.
그런데 ID 타입이 Long, Integer가 아닌 String인 경우 문제가 발생합니다.
이번 글에서는 왜 그런 문제가 발생하는지 그리고 어떻게 커스텀 Generator로 해결할 수 있는지 정리해보겠습니다.
😕 문제 상황
아래처럼 String 타입의 ID 필드에 @GeneratedValue를 사용해 Oracle 시퀀스를 연결하면 어떻게 될까요?
@Entity
public class MyEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my_seq_gen")
@SequenceGenerator(name = "my_seq_gen", sequenceName = "MY_SEQ", allocationSize = 1)
private String id;
// ...
}
이유는?
JPA가 시퀀스 값을 가져올 때 내부적으로 ResultSet.getLong() 같은 숫자 전용 메서드를 사용하기 때문에, String 타입 필드에는 매핑할 수 없기 때문입니다.
✅ 해결책: 커스텀 IdentifierGenerator 구현
Hibernate에서는 org.hibernate.id.IdentifierGenerator를 구현해 ID 생성 방식을 커스터마이징할 수 있습니다.
다음은 Oracle 시퀀스를 사용하면서 String 타입으로 반환하는 커스텀 Generator 예제입니다.
📦 IdGenerator.java
public class IdGenerator implements IdentifierGenerator, Configurable {
public static final String METHOD = "method";
public static final String SEQUENCE_NAME = "sequence_name";
private String method;
private String sequenceName;
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) {
method = ConfigurationHelper.getString(METHOD, params);
sequenceName = ConfigurationHelper.getString(SEQUENCE_NAME, params);
}
@Override
public Serializable generate(SharedSessionContractImplementor session, Object obj) {
String sql = null;
if ("SEQUENCE".equals(method)) {
sql = "SELECT " + sequenceName + ".NEXTVAL FROM DUAL";
}
try (Connection con = session.getJdbcConnectionAccess().obtainConnection();
CallableStatement stmt = con.prepareCall(sql)) {
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString(1); // String 타입으로 반환
}
} catch (SQLException e) {
throw new HibernateException(e);
}
return null;
}
}
🧠 코드 한 줄씩 설명
클래스 선언
public class IdGenerator implements IdentifierGenerator, Configurable {
IdentifierGenerator: Hibernate에서 ID를 생성할 때 사용하는 인터페이스Configurable: 외부에서 설정값을 받아올 수 있도록 함
설정 키 상수 선언
public static final String METHOD = "method";
public static final String SEQUENCE_NAME = "sequence_name";
@GenericGenerator로 넘겨주는 파라미터 이름을 정의함
설정값 저장 필드
private String method;
private String sequenceName;
- 설정된 값을 클래스 내부에 보관해두는 변수
설정값 초기화 메서드
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) {
method = ConfigurationHelper.getString(METHOD, params);
sequenceName = ConfigurationHelper.getString(SEQUENCE_NAME, params);
}
- Hibernate가 Generator를 초기화할 때 호출됨
- 전달된 설정값을 꺼내서 필드에 저장
ID 생성 메서드
@Override
public Serializable generate(SharedSessionContractImplementor session, Object obj) {
String sql = null;
- Hibernate가 ID를 생성해야 할 때 호출됨
- 내부적으로 JDBC를 통해 SQL 실행
SQL 구성
if ("SEQUENCE".equals(method)) {
sql = "SELECT " + sequenceName + ".NEXTVAL FROM DUAL";
}
- method가
SEQUENCE일 경우 시퀀스를 호출하는 SQL 구성 - Oracle 문법:
NEXTVAL FROM DUAL
커넥션 획득 & SQL 실행
try (Connection con = session.getJdbcConnectionAccess().obtainConnection();
CallableStatement stmt = con.prepareCall(sql)) {
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString(1); // String 타입으로 반환
}
- JDBC 커넥션을 열고 쿼리 실행
- 결과값이 있다면
getString(1)로 받아서 ID로 반환
예외 처리 및 fallback
} catch (SQLException e) {
throw new HibernateException(e);
}
return null;
- SQL 실행 중 문제가 생기면 Hibernate 예외로 wrapping
- 아무 결과가 없을 경우 null 반환
🔧 사용하는 방법
Entity 클래스에서 @GenericGenerator와 함께 설정합니다.
@Entity
public class MyEntity {
@Id
@GeneratedValue(generator = "string-seq-generator")
@GenericGenerator(
name = "string-seq-generator",
strategy = "kr.co.com.job.utils.IdGenerator",
parameters = {
@Parameter(name = "method", value = "SEQUENCE"),
@Parameter(name = "sequence_name", value = "MY_SEQ")
}
)
private String id;
// ...
}
📌 정리
| 구분 | 내용 |
|---|---|
| 기본 시퀀스 매핑 | @GeneratedValue(strategy = GenerationType.SEQUENCE)는 Long, Integer 등 숫자 타입 필드에서만 정상 작동 |
| String 타입 ID 사용 시 | 커스텀 IdentifierGenerator 구현 필요 (직접 JDBC로 시퀀스 호출하여 String 변환) |
| 핵심 포인트 | Hibernate의 기본 시퀀스 전략은 ResultSet.getLong()을 사용 → String 타입 필드에 매핑 불가 |
| 실무 확장성 | 접두사, 날짜, 복합 규칙 등을 조합하여 커스터마이징 가능 (e.g. "ORD-20250323-0001") |
| 테스트 주의사항 | 시퀀스가 실제로 Oracle DB에 존재해야 하며, 테스트 환경에서도 같은 방식의 시퀀스 생성 필요 |
💡 추가로 알아두면 좋은 점
1. 📈 allocationSize와 성능
@SequenceGenerator를 사용할 때는allocationSize값을 조절하면 DB와의 통신 횟수를 줄여 성능 최적화 가능- 하지만 커스텀 Generator에서는 이를 직접 반영해야 하므로 주의 필요
- 커스텀 로직에
allocationSize를 추가하려면 시퀀스 값을 한 번에 여러 개 가져오는 로직 구현이 필요함
예시:
SELECT MY_SEQ.NEXTVAL FROM DUAL CONNECT BY LEVEL <= 10
2. 🔒 동시성 문제 고려
- 다수의 쓰레드가 동시에 시퀀스를 요청하는 경우에도 Oracle 시퀀스는 자동으로 순서를 보장함
- 다만
접두사 + 시퀀스로 조합할 경우 중복 방지 로직은 개발자가 따로 신경 써야 함
3. ✨ 실무 적용 예시: 접두사 + 날짜 + 시퀀스
@Override
public Serializable generate(SharedSessionContractImplementor session, Object obj) {
String sql = "SELECT MY_SEQ.NEXTVAL FROM DUAL";
String prefix = "ORD";
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
try (Connection con = session.getJdbcConnectionAccess().obtainConnection();
CallableStatement stmt = con.prepareCall(sql)) {
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
long nextVal = rs.getLong(1);
return String.format("%s-%s-%06d", prefix, date, nextVal);
}
} catch (SQLException e) {
throw new HibernateException(e);
}
return null;
}
결과 예시: ORD-20250323-000001
4. 🧪 테스트 환경에서 시퀀스 만들기
개발용 DB에서 시퀀스가 없으면 커스텀 Generator에서 NullPointerException이 발생할 수 있습니다.
따라서 테스트 환경에도 시퀀스를 다음과 같이 만들어주어야 합니다.
CREATE SEQUENCE MY_SEQ START WITH 1 INCREMENT BY 1;
✅ 마무리하며
Oracle 시퀀스를 JPA에서 사용할 때, 기본적으로는 @SequenceGenerator로 간단하게 처리할 수 있습니다.
하지만 ID가 String일 경우, 직접 커스텀 IdentifierGenerator를 구현해야 하며, JDBC를 이용한 세밀한 컨트롤이 필요합니다.
이 방식을 잘 응용하면 다음과 같은 다양한 포맷도 만들 수 있습니다.
ORD-20250323-000001USER_000123PRD-AUTO-239472
👉 즉, ID 생성 전략을 커스터마이징해야 하는 요구가 있다면 IdentifierGenerator는 강력한 도구입니다.