Skip to main content

Jinx 네이밍 전략

Jinx의 네이밍 전략은 일관성을 최우선으로 하여 데이터베이스 객체들의 명명 규칙을 명문화하고 체계적으로 관리합니다. 이 문서는 실제 구현된 네이밍 규칙과 충돌 방지 메커니즘을 상세히 설명합니다.

목차

  1. 네이밍 전략 개요
  2. 기본 명명 규칙
  3. 정규화 규칙
  4. 길이 제한 및 해시 절단
  5. ColumnKey 기반 충돌 방지
  6. 설정 및 커스터마이징

네이밍 전략 개요

핵심 원칙

  1. 일관성: 모든 DB 객체는 동일한 규칙으로 명명
  2. 예측 가능성: 규칙을 알면 객체명을 예측 가능
  3. 충돌 방지: 동일한 이름이 생성되지 않도록 보장
  4. 길이 제한: DB별 식별자 길이 제한 준수
  5. 가독성: 개발자가 이해하기 쉬운 명명

아키텍처

public interface Naming {
String joinTableName(String leftTable, String rightTable);
String foreignKeyColumnName(String ownerName, String referencedPkColumnName);
String pkName(String tableName, List<String> columns);
String fkName(String fromTable, List<String> fromColumns, String toTable, List<String> toColumns);
String uqName(String tableName, List<String> columns);
String ixName(String tableName, List<String> columns);
// ... 기타 제약조건 명명 메서드들
}

기본 구현체: DefaultNaming

  • 설정 가능한 최대 길이 제한
  • 해시 기반 절단 메커니즘
  • 정규화 및 충돌 방지

기본 명명 규칙

1. 외래키 (Foreign Key) 이름

규칙: fk_<childTable>__<childCols>__<parentTable>

// DefaultNaming.fkName()
public String fkName(String childTable, List<String> childCols, String parentTable, List<String> parentCols) {
String base = "fk_"
+ norm(childTable)
+ "__"
+ joinNormalizedColumns(childCols) // 정렬된 컬럼들
+ "__"
+ norm(parentTable);
return clampWithHash(base);
}

예시:

-- @ManyToOne User user;
-- Order 테이블의 user_id 컬럼이 User 테이블의 id를 참조
fk_order__user_id__user

-- 복합 FK의 경우 (컬럼들은 알파벳순 정렬)
-- OrderItem 테이블의 order_id, product_id가 복합키인 경우
fk_orderitem__order_id_product_id__order

2. 인덱스 이름

규칙: ix_<table>__<cols>

public String ixName(String table, List<String> cols) {
return buildNameWithColumns("ix_", table, cols);
}

private String buildNameWithColumns(String prefix, String table, List<String> cols) {
String base = prefix + norm(table) + "__" + joinNormalizedColumns(cols);
return clampWithHash(base);
}

예시:

-- 단일 컬럼 인덱스
ix_user__email

-- 복합 인덱스 (컬럼들은 알파벳순 정렬)
ix_order__customer_id_order_date

3. 조인 테이블 이름 (@ManyToMany)

규칙: jt_<A>__<B> (알파벳순 엔티티명 결합)

public String joinTableName(String leftTable, String rightTable) {
String a = norm(leftTable);
String b = norm(rightTable);
// 알파벳순으로 정렬하여 일관성 보장
String base = (a.compareTo(b) <= 0) ? a + "__" + b : b + "__" + a;
return clampWithHash("jt_" + base);
}

예시:

-- @ManyToMany List<Role> roles; (User ↔ Role)
-- 입력 순서와 무관하게 알파벳순으로 정렬
jt_role__user

-- 조인 테이블의 컬럼들
role_id, user_id -- 각각 role, user 테이블의 PK 참조

4. 컬렉션 테이블 이름 (@ElementCollection)

규칙: <owningTable>_<attrName>

// 실제 구현은 ElementCollectionHandler에서 처리
// 일반적인 패턴: ownerEntity.getTableName() + "_" + attributeName

예시:

-- @ElementCollection List<String> tags;
user_tags

-- @ElementCollection Set<Address> addresses;
user_addresses

5. 외래키 컬럼 이름

규칙: <ownerName>_<referencedPkColumnName>

public String foreignKeyColumnName(String ownerName, String referencedPkColumnName) {
return norm(ownerName) + "_" + norm(referencedPkColumnName);
}

사용 패턴:

  • 일반 엔티티의 FK: fieldName + referencedPK
  • 조인테이블의 FK: entityTableName + referencedPK

예시:

-- @ManyToOne User user;
user_id

-- @ManyToOne Customer customer; (Customer의 PK가 customer_id인 경우)
customer_customer_id

-- 조인 테이블에서
user_id, role_id -- 각각 user, role 테이블명 기반

6. 기타 제약조건

// Primary Key
public String pkName(String table, List<String> cols) {
return buildNameWithColumns("pk_", table, cols);
}

// Unique 제약
public String uqName(String table, List<String> cols) {
return buildNameWithColumns("uq_", table, cols);
}

// Check 제약
public String ckName(String tableName, List<String> columns) {
return buildNameWithColumns("ck_", tableName, columns);
}

// Not Null 제약
public String nnName(String tableName, List<String> columns) {
return buildNameWithColumns("nn_", tableName, columns);
}

예시:

pk_user__id                    -- 단일 PK
pk_orderitem__order_id_product_id -- 복합 PK
uq_user__email -- UNIQUE 제약
ck_user__age_status -- CHECK 제약

정규화 규칙

모든 테이블명, 컬럼명은 다음 정규화 규칙을 거쳐 표준화됩니다:

정규화 알고리즘

private String norm(String s) {
if (s == null) return "null";
String x = s.replaceAll("[^A-Za-z0-9_]", "_"); // 비허용문자 -> '_'
x = x.replaceAll("_+", "_"); // 연속 '_' -> 단일 '_'
x = x.toLowerCase(); // 소문자화
if (x.isEmpty() || x.chars().allMatch(ch -> ch == '_')) {
return "x"; // 빈 문자열이나 '_'만 있으면 'x'
}
return x;
}

정규화 예시

입력출력설명
"UserTable""usertable"소문자화
"User-Table""user_table"특수문자 → '_'
"User Table!""user_table"공백, 특수문자 → '', 연속 '' 제거
"___""x"의미없는 문자열 → 'x'
"""x"빈 문자열 → 'x'
null"null"null 처리

컬럼 정렬 규칙

복수 컬럼이 포함된 제약조건 이름은 알파벳순 정렬 후 조합:

private String joinNormalizedColumns(List<String> cols) {
if (cols == null || cols.isEmpty()) return "";
List<String> normalized = cols.stream()
.map(this::norm)
.collect(Collectors.toCollection(ArrayList::new));
Collections.sort(normalized, String.CASE_INSENSITIVE_ORDER); // 정렬
return String.join("_", normalized);
}

정렬의 이점:

  • 컬럼 순서에 무관하게 동일한 제약조건명 생성
  • 중복 제약조건 방지
  • 예측 가능한 명명

길이 제한 및 해시 절단

길이 제한 설정

// ProcessingContext에서 설정 로드
private int parseMaxLength(Map<String, String> config) {
String maxLenOpt = config.get(JinxOptions.Naming.MAX_LENGTH_KEY);
int maxLength = JinxOptions.Naming.MAX_LENGTH_DEFAULT; // 기본값: 보통 63

if (maxLenOpt != null) {
try {
maxLength = Integer.parseInt(maxLenOpt);
} catch (NumberFormatException e) {
// 경고 후 기본값 사용
}
}
return maxLength;
}

설정 방법:

  • 컴파일 옵션: -Ajinx.naming.maxLength=63
  • 설정 파일: jinx.properties에서 jinx.naming.maxLength=63
  • 환경변수: JINX_NAMING_MAX_LENGTH=63

해시 절단 메커니즘

길이가 제한을 초과하면 자동으로 절단 및 해시 추가:

private String clampWithHash(String name) {
if (name.length() <= maxLength) return name;

String hash = Integer.toHexString(name.hashCode());
int keep = Math.max(1, maxLength - (hash.length() + 1)); // '_' 포함 계산
return name.substring(0, keep) + "_" + hash;
}

절단 예시 (maxLength=20 가정):

원본 이름절단된 이름설명
fk_very_long_table_name__column__targetfk_very_lon_1a2b3c4d앞부분 + '_' + 해시
ix_user__emailix_user__email제한 내이므로 그대로

해시 절단의 이점:

  • 유일성 보장: 해시 충돌 확률 극히 낮음
  • 결정적: 동일한 입력은 항상 동일한 결과
  • 공간 효율: 최대 길이 제한 준수
  • 추적 가능: 해시를 통해 원본 추적 가능

ColumnKey 기반 충돌 방지

ColumnKey 개념

ColumnKey는 테이블과 컬럼의 조합을 고유하게 식별하는 키입니다:

public final class ColumnKey implements Comparable<ColumnKey> {
private final String canonical; // 정규화된 키 (DB 비교용)
private final String display; // 원본 키 (표시용)

// 기본 생성 (소문자 정규화)
public static ColumnKey of(String tableName, String columnName) {
String displayKey = tableName + "::" + columnName;
String canonicalKey = tableName.toLowerCase() + "::" + columnName.toLowerCase();
return new ColumnKey(canonicalKey, displayKey);
}

// 정규화 정책 지정 생성
public static ColumnKey of(String tableName, String columnName, CaseNormalizer normalizer) {
String displayKey = tableName + "::" + columnName;
String canonicalKey = normalizer.normalize(tableName) + "::" + normalizer.normalize(columnName);
return new ColumnKey(canonicalKey, displayKey);
}
}

ColumnKey 형식

패턴: <tableName>::<columnName>

예시:

ColumnKey.of("User", "email")        // "User::email" (display)
// "user::email" (canonical)

ColumnKey.of("OrderItem", "productId") // "OrderItem::productId" (display)
// "orderitem::productid" (canonical)

대소문자 정규화 정책

다양한 데이터베이스의 대소문자 처리 정책을 지원:

// CaseNormalizer 인터페이스
public interface CaseNormalizer {
String normalize(String identifier);
}

// 구현체 예시
public class LowerCaseNormalizer implements CaseNormalizer {
public String normalize(String identifier) {
return identifier == null ? "" : identifier.toLowerCase(Locale.ROOT);
}
}

public class UpperCaseNormalizer implements CaseNormalizer {
public String normalize(String identifier) {
return identifier == null ? "" : identifier.toUpperCase(Locale.ROOT);
}
}

충돌 방지 메커니즘

  1. 정규화된 비교: canonical 키로 동일성 판단
  2. 원본 보존: display 키로 가독성 유지
  3. 정렬 지원: Comparable 구현으로 일관된 순서
  4. Map 키 사용: equals()hashCode() 오버라이드
// RelationshipModel에서 ColumnKey 활용 예시
public List<ColumnKey> getColumnsKeys(CaseNormalizer normalizer) {
return columns.stream()
.map(col -> ColumnKey.of(tableName, col, normalizer))
.toList();
}

설정 및 커스터마이징

기본 설정

# jinx.properties
jinx.naming.maxLength=63

컴파일 타임 설정

# Maven
mvn compile -Djinx.naming.maxLength=128

# Gradle
gradle compileJava -Ajinx.naming.maxLength=128

프로그래매틱 설정

// ProcessingContext 초기화 시
public ProcessingContext(ProcessingEnvironment processingEnv, SchemaModel schemaModel) {
// 설정 로드 (프로파일 지원)
Map<String, String> config = loadConfiguration(processingEnv);
int maxLength = parseMaxLength(config);

this.naming = new DefaultNaming(maxLength);
}

커스텀 Naming 구현

public class CustomNaming implements Naming {
private final int maxLength;

public CustomNaming(int maxLength) {
this.maxLength = maxLength;
}

@Override
public String fkName(String fromTable, List<String> fromColumns, String toTable, List<String> toColumns) {
// 커스텀 FK 명명 로직
return "custom_fk_" + fromTable + "_to_" + toTable;
}

// 다른 메서드들도 커스터마이징...
}

네이밍 규칙 요약표

객체 유형접두사패턴예시
Foreign Keyfk_fk_<child>__<cols>__<parent>fk_order__user_id__user
Indexix_ix_<table>__<cols>ix_user__email
Join Tablejt_jt_<A>__<B> (알파벳순)jt_role__user
Collection Table-<owner>_<attr>user_addresses
Primary Keypk_pk_<table>__<cols>pk_user__id
Uniqueuq_uq_<table>__<cols>uq_user__email
Checkck_ck_<table>__<cols>ck_user__age
FK Column-<owner>_<refPK>user_id

정규화 및 충돌 방지 특징

  1. 일관된 정규화: 모든 식별자는 소문자 + 특수문자 제거
  2. 컬럼 정렬: 복수 컬럼은 알파벳순 정렬로 일관성 보장
  3. 길이 제한: 설정 가능한 최대 길이 + 해시 절단
  4. ColumnKey: 테이블::컬럼 형태의 고유 식별자
  5. 충돌 방지: 해시 기반 유일성 보장

이러한 체계적인 네이밍 전략을 통해 Jinx는 대규모 프로젝트에서도 일관되고 예측 가능한 데이터베이스 객체 명명을 보장합니다.