기록

Spring Boot와 JPA를 활용한 커스텀 아이디 생성 전략 구현 본문

Web/Spring

Spring Boot와 JPA를 활용한 커스텀 아이디 생성 전략 구현

youngyin 2023. 10. 31. 20:00

시작하면서

Spring Boot, JPA, Kotlin, H2(개발용 DB)을 활용하여 Notice 테이블의 목록 조회와 삽입을 구현하고자 했다. 그런데 아이디를 어떤 방식으로 생성해야 하는지에 대한 고민이 있었다. 아래는 그 내용을 다룬 포스팅이다.

프로젝트 구성

(1) 공지사항 테이블 (Notice)

(2) 데이터베이스

(3) 엔드 포인드

  • POST /notices 공지사항 생성
  • GET /notices 공지사항 목록 조회

기본 키 생성 전략(IDENTITY, SEQUENCE, TABLE)

데이터베이스에서 기본 키(Primary Key)를 생성하는 방식으로는 여러 가지 전략이 있다. 이중에는 IDENTITY, SEQUENCE, TABLE과 같이 자주 사용된다. 각 전략은 고유한 특징과 장단점을 가지고 있으며, 프로젝트의 요구 사항에 따라 선택해야 한다.

 

1. IDENTITY

데이터베이스가 자동으로 기본 키를 생성한다. 각 데이터베이스에 따라 다른 방식으로 동작한다.

아이디를 채번하기 위해 데이터베이스를 조회하고, 데이터를 삽입하여 두번의 동작이 일어난다. 
2. SEQUENCE

데이터베이스가 시퀀스(또는 오라클에서는 시퀀스 오브젝트)를 사용하여 기본 키를 생성한다.

마찬가지로, 아이디를 채번하기 위해 시퀀스를 채번하고, 데이터를 삽입하여 두번의 동작이 일어난다. 
3. TABLE

데이터베이스에 별도의 테이블을 사용하여 기본 키를 생성한다.

키값을 가지는 테이블을 따로 두어, 테이블에서 값을 아이디값을 조회해서 새로운 아이디를 채번하고, 데이터를 삽입한다.

새로운 아이디 생성 전략이 필요한 이유

여러 테이블에서 아이디를 생성할 때, 동일한 생성 전략을 사용하고, 아이디만으로 어떤 테이블의 데이터인지 확인할 수 있도록 하고 싶었다. 또한, 아이디의 형식을 일관되게 유지하기 위해 접두사 두 자리와 시퀀스 8 자리를 합친 10 자리 아이디를 생성하고자 했습니다.

첫번째, 사용자 함수로 아이디 생성하기

아이디를 생성하는 데 사용자 함수를 활용하는 방법도 있다. 그러나 이 방법은 로컬 환경에서 개발한 뒤 운영 데이터베이스를 변경할 때 코드를 다시 작성해야 하는 문제가 있었다. 이 문제를 최소화하기 위해 JPA를 사용하여 아이디를 생성하고자 했다.

CREATE ALIAS NEXT_CUSTOM_ID AS $$
String nextCustomId(String prefix) {
    String sequenceName = "";
    int nextValue = 0;

    if ("NT".equals(prefix)) {
        sequenceName = "notice_sequence";
    }

    if (!sequenceName.isEmpty()) {
        ResultSet rs = sql("CALL NEXT VALUE FOR " + sequenceName);
        rs.next();
        nextValue = rs.getInt(1);
        return prefix + String.format("%08d", nextValue);
    } else {
        return null;
    }
}
$$;

두번째, JPA 사용하기

(1) Notice 모델 클래스
Notice 모델 클래스를 디자인할 때, 아이디 생성에 필요한 정보를 관리할 수 있도록 어노테이션을 사용해야 한다.

(2) 사용자 정의 IdentifierGenerator

import org.hibernate.HibernateException
import org.hibernate.engine.spi.SharedSessionContractImplementor
import org.hibernate.id.Configurable
import org.hibernate.id.IdentifierGenerator
import org.hibernate.service.ServiceRegistry
import org.hibernate.type.Type
import java.util.*

class CustomIdGenerator : IdentifierGenerator, Configurable {
    // 전달받은 속성값
    private var sequenceName : String? = null
    private var prefix : String? = null

    // 속성값 처리
    override fun configure(type: Type?, parameters: Properties?, serviceRegistry: ServiceRegistry?) {
        sequenceName = parameters?.get("sequenceName") as String?
        prefix = parameters?.get("prefix") as String?
    }

    // id 채번
    override fun generate(session: SharedSessionContractImplementor?, `object`: Any?): Any {
        return try {
            if (session == null) throw HibernateException("Unable to generate Notice ID")

            val connection = session.jdbcConnectionAccess.obtainConnection()
            val stmt = connection.prepareStatement("select next value for $sequenceName")
            val rs = stmt.executeQuery()
            if (rs.next()) {
                prefix + String.format("%08d", rs.getLong(1))
            } else {
                throw HibernateException("Unable to generate Notice ID")
            }
        } catch (ex: Exception) {
            throw HibernateException("Unable to generate Notice ID", ex)
        }
    }
}

결과

아래 사진처럼 접두사와 시퀀스로 10자리 아이디가 채번됨을 확인할 수 있었다.

더 공부할 내용

위의 예제에서는 Notice 테이블만 다루었다. 이후에 다른 테이블이 추가되면서 GenericGenerator 어노테이션에 프리픽스와 시퀀스를 전달할 때 혼동이 생길 수 있다. 따라서 이를 어떻게 묶어서 관리할 수 있는지에 대한 고민이 필요하다.

또한, 시퀀스를 지원하지 않는 데이터베이스에 대한 대응 방안도 고려해야 한다.

Comments