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