2025년 3월 23일조회 693분 읽기
JPA

JPA - Oracle Sequence

🧩 JPA에서 Oracle 시퀀스로 String 타입 ID 생성하기

JPA를 사용하다 보면 기본적으로 @GeneratedValue(strategy = GenerationType.SEQUENCE)를 이용해 Oracle의 시퀀스를 ID로 사용하는 경우가 많습니다.

그런데 ID 타입이 Long, Integer가 아닌 String인 경우 문제가 발생합니다.

이번 글에서는 왜 그런 문제가 발생하는지 그리고 어떻게 커스텀 Generator로 해결할 수 있는지 정리해보겠습니다.


😕 문제 상황

아래처럼 String 타입의 ID 필드에 @GeneratedValue를 사용해 Oracle 시퀀스를 연결하면 어떻게 될까요?

java
@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

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: 외부에서 설정값을 받아올 수 있도록 함

설정 키 상수 선언

java
    public static final String METHOD = "method";
    public static final String SEQUENCE_NAME = "sequence_name";
  • @GenericGenerator로 넘겨주는 파라미터 이름을 정의함

설정값 저장 필드

java
    private String method;
    private String sequenceName;
  • 설정된 값을 클래스 내부에 보관해두는 변수

설정값 초기화 메서드

java
    @Override
    public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) {
        method = ConfigurationHelper.getString(METHOD, params);
        sequenceName = ConfigurationHelper.getString(SEQUENCE_NAME, params);
    }
  • Hibernate가 Generator를 초기화할 때 호출됨
  • 전달된 설정값을 꺼내서 필드에 저장

ID 생성 메서드

java
    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object obj) {
        String sql = null;
  • Hibernate가 ID를 생성해야 할 때 호출됨
  • 내부적으로 JDBC를 통해 SQL 실행

SQL 구성

java
      if ("SEQUENCE".equals(method)) {
           sql = "SELECT " + sequenceName + ".NEXTVAL FROM DUAL";
       }
  • method가 SEQUENCE일 경우 시퀀스를 호출하는 SQL 구성
  • Oracle 문법: NEXTVAL FROM DUAL

커넥션 획득 & SQL 실행

java
        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

java
        } catch (SQLException e) {
            throw new HibernateException(e);
        }
        return null;
  • SQL 실행 중 문제가 생기면 Hibernate 예외로 wrapping
  • 아무 결과가 없을 경우 null 반환

🔧 사용하는 방법

Entity 클래스에서 @GenericGenerator와 함께 설정합니다.

java
@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를 추가하려면 시퀀스 값을 한 번에 여러 개 가져오는 로직 구현이 필요함

예시:

sql
SELECT MY_SEQ.NEXTVAL FROM DUAL CONNECT BY LEVEL <= 10

2. 🔒 동시성 문제 고려

  • 다수의 쓰레드가 동시에 시퀀스를 요청하는 경우에도 Oracle 시퀀스는 자동으로 순서를 보장
  • 다만 접두사 + 시퀀스로 조합할 경우 중복 방지 로직은 개발자가 따로 신경 써야 함

3. ✨ 실무 적용 예시: 접두사 + 날짜 + 시퀀스

java
@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이 발생할 수 있습니다.
따라서 테스트 환경에도 시퀀스를 다음과 같이 만들어주어야 합니다.

sql
CREATE SEQUENCE MY_SEQ START WITH 1 INCREMENT BY 1;

✅ 마무리하며

Oracle 시퀀스를 JPA에서 사용할 때, 기본적으로는 @SequenceGenerator로 간단하게 처리할 수 있습니다.
하지만 ID가 String일 경우, 직접 커스텀 IdentifierGenerator를 구현해야 하며, JDBC를 이용한 세밀한 컨트롤이 필요합니다.

이 방식을 잘 응용하면 다음과 같은 다양한 포맷도 만들 수 있습니다.

  • ORD-20250323-000001
  • USER_000123
  • PRD-AUTO-239472

👉 즉, ID 생성 전략을 커스터마이징해야 하는 요구가 있다면 IdentifierGenerator는 강력한 도구입니다.


📎 참고 문서