안녕하세요. 비즈상품 개발팀 권순규 입니다.
작년에 입사한 이후로 매번 주변 동료분들에게 배움을 얻기만 해왔는데, 이번에는 배운 내용을 공유해보고자 합니다.

이제껏 해온 기본키 매핑

엔티티에 기본키를 매핑할때 간편하게 매핑할 수 있고, 비즈니스 환경이 변하더라도 기본키를 바꿀 필요가 없다는 이유 등으로 주로 데이터베이스의 자동 증가값을 아래와 같이 매핑 하여 사용해왔습니다.

@Id  
@Column(name = "id")  
@GeneratedValue(strategy = GenerationType.IDENTITY)  
private Long id;

장애물

위와같은 형태로 대부분의 엔티티에서 자동증가값을 사용하여 간편하게 기본키를 매핑해왔었지만, 자동증가 값을 사용할 수 없는 장애물에 나타나게 되었습니다. 바로, 기존에 동작하고 있던 코드와의 호환이었습니다.

기존의 코드에서는 데이터베이스의 자동 증가값을 사용하지 않고, 프로시저를 호출하여 프로시저에서 직접 생성한 기본키를 사용하고 있었기 때문에 아쉽지만 자동증가 값을 매핑하는 코드를 보내주어야 했습니다.

해결책

이제는 엔티티를 저장할때 프로시저가 만들어준 키를 기본키로 저장해야 합니다.
그래서 JPA 표준 스펙에서 제공하는 기능은 아니지만, 많은 분들이 JPA 구현체로 사용하고 있는 하이버네이트에서 제공하는 @GenericGeneratorIdentifierGenerator 인터페이스, Configurable 인터페이스를 사용하였습니다.
@GenericGenerator 애노테이션은 엔티티의 아이디 필드와 키생성 전략을 매핑해 주는데 사용하였고, IdentifierGenertor 인터페이스와 Configurable 인터페이스는 기존의 프로시저를 호출하여 키생성을 하는 곳에 사용하였습니다.

기본키 생성 클래스

기존 프로시저를 호출하여 키 생성을 해주는 클래스는 IdentifierGenerator 인터페이스와 Configurable 인터페이스를 구현하여 아래와 같이 생성하였습니다.

 public class SeqGenerator implements IdentifierGenerator, Configurable {
     public static final String SEQ_GENERATOR_PARAM_KEY = "procedureParam";
 
     private String procedureParam;
 
     @Override
     public Serializable generate(SharedSessionContractImplementor session,
                                  Object object) {
 
         ProcedureCall procedureCall = session.createStoredProcedureCall("SEQ_GENERATOR"); // 호출할 프로시저
 
         procedureCall.registerParameter("InputParam", String.class, ParameterMode.IN); // 프로시저에 선언된 입력받는 변수명 등록
         procedureCall.registerParameter("ID", String.class, ParameterMode.OUT); // 프로시저에 선언된 반환하는 변수명 등록
 
         procedureCall.setParameter("InputParam", procedureParam); // 프로시저의 변수에 전달할 파라미터 설정
 
         ProcedureOutputs outputs = procedureCall.getOutputs(); // 프로시저에서 반환하는값들
 
         return (String) outputs.getOutputParameterValue("ID");
     }
 
     @Override
     public void configure(Type type,
                           Properties params,
                           ServiceRegistry serviceRegistry) {
 
         this.procedureParam = ConfigurationHelper.getString(SEQ_GENERATOR_PARAM_KEY, params); // @GenericGenerator에서 넘겨준 파라미터 꺼내기
     }
 }

기본키 생성은 IdentifierGenerator 인터페이스만을 구현하여 generate() 메소드안에 SQL쿼리 호출이나 프로시저를 호출하는 등의 키 생성 로직을 작성해주고, @GenericGenerator 애노페이션의 strategy modifier에 구현 클래스 명을 선언해 주기만 하면 됩니다.

기본키 생성로직을 작성하는 클래스에 IdentifierGenerator 인터페이스와 더불어 Configurable 인터페이스까지 구현한 이유는 기본키를 생성하는 프로시저에 값을 넘겨줘야 했기 때문입니다.

기본키 생성시 특정 값을 입력받아야 할 필요가 있으면 Configurable 인터페이스를 구현한 뒤 configure() 메소드의 Properties params 에서 꺼내서 사용하면 됩니다. 위의 코드에서는 procedureParam 필드에 Properties params에서 꺼낸 값을 할당하여 IdentifierGenerator 인터페이스의 generate() 메소드 안에서 사용하도록 하였습니다.

Properties params 에서 값을 꺼낼때 ConfigurationHelper 클래스를 사용한 이유는 Properties 클래스의 getProperty() 메소는 String 값만을 반환하지만, ConfigurationHelper클래스에는 getString(), getBoolean() 등의 다양한 값을 반환하는 메소드들이 있어서 사용하였습니다.

위의 예제는 프로시저를 호출해야 했기 때문에 session.createStoredProcedureCall()을 사용하였지만, Connection을 직접 얻어서 사용할 경우에는 session.connection() 을 호출하여 Connection을 얻을 수 있습니다.

IdentifierGenerator 인터페이스를 구현시 주의점

IdentifierGenerator 인터페이스구현 클래스는 반드시 public 으로 선언된 기본 생성자가 있어야 합니다.

엔티티

위의 SeqGenerator클래스에서 생성한 기본키를 사용하는 엔티티의 아이디 필드는 아래와 같이 매핑하였습니다.

@Entity
@Table(name="table")
public class Entity {

    private static final String PROCEDURE_PARAM = "AAA"; // 프로시저에 넘겨줄 파라미터값.

    @Id
    @GenericGenerator(name = "idGenerator", // @GeneratedValue의 generator modifier에서 사용할 이름
            strategy = "SeqGenerator", // IdentifierGenerator 인터페이스를 구현한 클래스 이름. 전체 패키지를 포함한 클래스 이름을 적어야 합니다.
            parameters = @org.hibernate.annotations.Parameter( // Configurable 인터페이스 구현 클래스에 넘겨줄 파라미터 설정
                    name = SeqGenerator.SEQ_GENERATOR_PARAM_KEY, // 파라미터의 키 이름. SeqGenerator 클래스에 선언해둔 상수를 사용
                    value = PROCEDURE_PARAM // 위의 name modifier에 선언한 키에 넘겨줄 파라미터 값
            )
    )
    @GeneratedValue(generator = "idGenerator") // @GenericGenerator의 name modifier 에 지정한 이름
    @Column(name = "ID")
    private String id;
}

엔티티의 아이디 필드에 하이버네이트에서 제공하는 @GenericGenerator 애노테이션을 선언 후, @GeneratedValue 애노테이션의 generator modifier에는 @GenericGenerator에 선언한 name을 적어주어 엔티티 저장시 @GenericGeneratorstrategy에 지정한 클래스에서 생성한 기본키를 사용하도록 하였습니다.

참고

끝!!