안녕하세요 오랜만입니다 ㅎㅎ
1년차 포트폴리오를 채우자마자 바로 블로그 관리에 너무 소홀해져 버렸네요.
앞선 두 편의 글에서 DDL-DSL 라이브러리의 개발 배경과 CREATE TABLE 구현에 대해 다루었는데요.
이번에 제가 만든 DDL-DSL 라이브러리가 적용된 타 팀의 프로젝트가 첫 프로덕션 출시를 앞두고 있습니다.
때문에, 프로덕션에 나가기전 부랴부랴 마이그레이션 기능을 개발중에 있습니다.
해서 먼저, 이번 편에서는 마이그레이션시 꼭 필요한 '테이블 구조(칼럼 포함)를 변경하고 삭제하는 기능의 구현'에 대해 다루어보겠습니다.
1. ALTER TABLE 구현
1.1 기본 설계
데이터베이스의 테이블 구조를 변경하는 작업은 매우 신중하게 이루어져야 합니다.
특히 여러 데이터베이스를 지원해야 하는 상황에서는 각 데이터베이스의 특성을 고려해야 합니다.
DDL-DSL은 이를 위해 다음과 같은 구조로 설계되었습니다.
sealed class AlterOperation {
data class AddColumn<T : ColumnType<P, C>, P, C : ConfigMarker>(
val column: ColumnDefinition<T, P, C>
) : AlterOperation()
data class DropColumn(
val columnName: String
) : AlterOperation()
data class ModifyColumn<T : ColumnType<P, C>, P, C : ConfigMarker>(
val newColumn: ColumnDefinition<T, P, C>,
val existTable: TableDefinition? = null,
) : AlterOperation()
data class RenameColumn(
val oldColumnName: String,
val newColumnName: String
) : AlterOperation()
// ... 기타 연산들
}
이러한 설계는 각각의 ALTER 작업을 명확하게 구분하고, 타입 안전성을 보장합니다.
1.2 데이터베이스별 구현의 차이
SQLite의 특수성
SQLite는 다른 데이터베이스와 달리 ALTER TABLE의 기능이 매우 제한적입니다.
특히 컬럼 수정이나 삭제와 같은 작업은 다음과 같은 단계를 거쳐야 합니다:
- 새로운 테이블 생성
- 데이터 복사
- 기존 테이블 삭제
- 새 테이블 이름 변경
이를 위해 SQLite 전용 구현을 다음과 같이 작성했습니다.
(이는 젯브레인즈의 DataBase 관리 툴인 DataGrip에서 Sqlite 컬럼을 모디파이 할 때에 생성되는 SQL문을 참고하여 제작했습니다.)
override fun modifyColumn(
operation: AlterOperation.ModifyColumn<*, *, *>,
dialect: SqlDialect,
alterTableName: String?,
): String {
requireNotNull(operation.existTable) { "Exist table must not be null in Sqlite dialect" }
// ... 검증 로직
val modifiedTable = operation.existTable.modifyColumn(operation.newColumn)
val tempTableName = "__temp_table_for_${operation.existTable.tableName}_modify_${operation.newColumn.name}"
return buildString {
append(modifiedTable.changeTableName(tempTableName).toSql(dialect))
append("\n")
append("insert into $tempTableName($columnNames)\n")
append("select $columnNames\n")
append("from $originalTableName;\n\n")
append("DROP TABLE $originalTableName;\n")
append("ALTER TABLE $tempTableName RENAME TO $originalTableName;\n")
}
}
MS SQL Server의 특징
반면 MS SQL Server는 보다 풍부한 ALTER TABLE 기능을 제공합니다:
override fun alterTable(
tableName: String,
operations: List<AlterOperation>,
dialect: SqlDialect,
): String {
return buildString {
if (config.useTransactionalDDL) {
append("BEGIN TRANSACTION;\n")
}
operations.forEach { operation ->
when (operation) {
is AlterOperation.AddColumn<*, *, *> ->
append("ALTER TABLE $tableName ${addColumn(operation, dialect)};\n")
is AlterOperation.ModifyColumn<*, *, *> ->
append("ALTER TABLE $tableName ${modifyColumn(operation, dialect)};\n")
// ... 기타 연산들
}
}
if (config.useTransactionalDDL) {
append("COMMIT;")
}
}
}
1.3 사용자 인터페이스
복잡한 내부 구현과 별개로, 사용자 인터페이스는 가능한 단순하고 직관적으로 설계했습니다:
val alterTable = TableAlteration.create("users") {
addColumn("email", ColumnType.String(255)) nullable true
renameColumn("old_name", "new_name")
modifyColumn("age", ColumnType.Integer) nullable false
dropColumn("temporary_column")
}
2. DROP TABLE 구현
테이블 삭제 기능은 비교적 단순하지만, 각 데이터베이스의 특성을 고려해야 했습니다.
2.1 기본 설계
class TableDeletion private constructor(
private val tableName: String
) : SqlConvertable {
companion object {
fun delete(tableName: String): TableDeletion = TableDeletion(tableName)
}
override fun toSql(dialect: SqlDialect): String {
return dialect.tableDropConfig.dropTable(tableName)
}
}
2.2 데이터베이스별 구현
SQLite:
override fun dropTable(tableName: String): String {
return "DROP TABLE IF EXISTS $tableName;"
}
MS SQL Server:
override fun dropTable(tableName: String): String {
return """
IF OBJECT_ID('$tableName', 'U') IS NOT NULL
BEGIN
DROP TABLE $tableName;
END
""".trimIndent()
}
2.3 사용자 인터페이스
val dropTable = TableDeletion.create("users")
println(dropTable.toSql(SqliteDialect()))
3. 안전성 보장
DDL 작업은 데이터베이스 구조를 직접 변경하는 중요한 작업이므로, 다음과 같은 안전장치를 구현했습니다:
- 타입 안전성
- 필수 파라미터 검증
- 식별자 길이 및 형식 검증
- NOT NULL 컬럼 추가 시 기본값 필수 확인
- 존재하지 않는 컬럼 수정/삭제 방지
private fun validateIdentifier(identifier: String) {
if (identifier.length > 128) {
throw IllegalArgumentException("Identifier exceeds maximum length")
}
if (!identifier.matches(Regex("^[a-zA-Z_][a-zA-Z0-9_]*$"))) {
throw IllegalArgumentException("Invalid identifier format")
}
}
결론
ALTER와 DROP 기능의 구현을 통해 DDL-DSL은 더욱 완성된 형태의 라이브러리가 되었습니다.
특히 각 데이터베이스의 특성을 고려한 구현과 안전성 보장 메커니즘은 실제 프로덕션 환경에서의 활용도를 크게 높여주었습니다.
무엇보다 멀티플랫폼의 솔루션의 경우, 플랫폼마다 RDBMS가 다를 때, 사용하는 RDBMS별로 따로 SQL을 작성해야 하는 문제를 개선해 주었습니다.
다음 편에서는 이 서두에 말씀드렸던 대로 마이그레이션 기능 개발에 대해 마저 다루도록 하겠습니다.
긴 글 읽어주셔서 감사합니다!