자바의 객체 지향 프로그래밍 아키텍처에서 클래스와 데이터베이스 테이블은 일대일 대응 관계를 갖는 경우가 많습니다. 수많은 클래스에 대해 매번 수동으로 테이블 생성 스크립트를 작성하는 것은 비효율적인 반복 작업입니다. 자바의 강력한 기능인 리플렉션(Reflection)과 어노테이션(Annotation)을 활용하면 클래스 구조를 런타임에 분석하여 MySQL 테이블 생성 쿼리(DDL)를 자동으로 생성하는 도구를 구현할 수 있습니다.
먼저 테이블과 컬럼의 메타데이터를 정의하기 위한 커스범 어노테이션을 작성합니다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 테이블 명칭을 지정하기 위한 어노테이션
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EntityMapping {
String tableName();
}
// 컬럼 상세 속성을 정의하기 위한 어노테이션
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ColumnMapping {
String columnName() default "";
String columnType() default "VARCHAR";
int length() default 255;
boolean nullable() default true;
}
// 기본 키(Primary Key) 식별을 위한 어노테이션
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Identifier {
boolean autoIncrement() default true;
}
이제 리플렉션을 사용하여 클래스 정보를 읽고 SQL 문을 빌드하는 핵심 로직을 구현합니다. 이 엔진은 클래스의 필드를 순회하며 어노테이션 정보를 바탕으로 각 로우의 타입과 제약 조건을 문자열로 조합합니다.
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SchemaGenerator {
public static String generateCreateTableSql(Class<?> clazz) {
if (!clazz.isAnnotationPresent(EntityMapping.class)) {
throw new IllegalArgumentException("EntityMapping 어노테이션이 누락되었습니다.");
}
String tableName = clazz.getAnnotation(EntityMapping.class).tableName();
StringBuilder sql = new StringBuilder("CREATE TABLE " + tableName + " (\n");
Field[] fields = clazz.getDeclaredFields();
List<String> columnDefinitions = new ArrayList<>();
List<String> primaryKeys = new ArrayList<>();
for (Field field : fields) {
if (!field.isAnnotationPresent(ColumnMapping.class)) continue;
ColumnMapping column = field.getAnnotation(ColumnMapping.class);
Identifier id = field.getAnnotation(Identifier.class);
String name = column.columnName().isEmpty() ? field.getName() : column.columnName();
String type = column.columnType();
String lengthStr = (type.equalsIgnoreCase("VARCHAR") || type.equalsIgnoreCase("CHAR"))
? "(" + column.length() + ")" : "";
StringBuilder columnDef = new StringBuilder();
columnDef.append(" ").append(name).append(" ").append(type).append(lengthStr);
if (!column.nullable()) {
columnDef.append(" NOT NULL");
}
if (id != null) {
if (id.autoIncrement()) {
columnDef.append(" AUTO_INCREMENT");
}
primaryKeys.add(name);
}
columnDefinitions.add(columnDef.toString());
}
sql.append(String.join(",\n", columnDefinitions));
if (!primaryKeys.isEmpty()) {
sql.append(",\n PRIMARY KEY (")
.append(String.join(", ", primaryKeys))
.append(")");
}
sql.append("\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
return sql.toString();
}
}
구현된 생성기가 정상적으로 작동하는지 확인하기 위해 샘플 엔티티 클래스를 정의하고 테스트를 진행합니다.
@EntityMapping(tableName = "member_account")
public class Member {
@Identifier
@ColumnMapping(columnName = "member_id", columnType = "BIGINT", nullable = false)
private long id;
@ColumnMapping(columnName = "username", length = 50, nullable = false)
private String username;
@ColumnMapping(columnName = "email", length = 100)
private String email;
@ColumnMapping(columnName = "age", columnType = "INT")
private int age;
// Getter, Setter 생략
}
public class Main {
public static void main(String[] args) {
String sql = SchemaGenerator.generateCreateTableSql(Member.class);
System.out.println(sql);
}
}
위 코드를 실행하면 자바 필드 타입과 어노테이션 설정값이 결합된 다음과 같은 SQL 문이 출력됩니다.
CREATE TABLE member_account (
member_id BIGINT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NULL,
age INT NULL,
PRIMARY KEY (member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
이러한 방식은 JDBC와 결합하여 애플리케이션 구동 시점에 테이블 존재 여부를 체크하고 자동으로 생성하는 기능을 구현하는 데 활용될 수 있습니다. 또한 인덱스 설정이나 외정 키 제약 조건 등을 추가하는 방향으로 확장하여 더 완성도 높은 ORM 엔진의 기초를 다질 수 있습니다.