본문 바로가기
카테고리 없음

Reflections JPA DDL 검증

by sujupark54 2026. 2. 11.

코딩 ddl

데이터베이스 형상 관리를 위해 Flyway나 Liquibase 같은 검증된 도구를 사용하는 것이 일반적이다. 하지만 모든 상황에서 반드시 무거운 도구가 필요한 것은 아니다. 현재 프로젝트의 구조가 단순하고 검증 대상이 명확하다면 직접 구현하는 선택지도 충분히 의미가 있다.

이 글에서는 DDL.sql과 JPA Entity를 비교해 형상 불일치를 검증하는 작은 도구를 직접 구현한 과정을 정리한다. 핵심은 SQL 파싱보다는 리플렉션을 활용해 JPA Entity 메타 정보를 어떻게 수집하고 검증에 활용할 수 있는지에 있다.


Reflections 기반 JPA 엔티티 스캔

자바 표준 리플렉션 API인 java.lang.reflect만으로도 클래스와 필드 정보를 가져올 수는 있다. 하지만 클래스 패스를 순회하고, 특정 어노테이션이 붙은 클래스만 추출하는 작업은 직접 구현하기엔 번거롭다. 이때 org.reflections 라이브러리를 사용하면 이 과정을 훨씬 간결하게 처리할 수 있다.

이번 구현의 목적은 JPA Entity 클래스를 자동으로 수집하는 것이다. 기준이 되는 조건은 jakarta.persistence.Table 어노테이션이다. 해당 어노테이션이 붙은 클래스만을 엔티티로 간주해 동적으로 수집한다.


final class JpaEntityReader {

    private static final String PACKAGE_NAME = "lkdcode.entity";
    private static final Reflections REFLECTIONS =
        new Reflections(PACKAGE_NAME, Scanners.TypesAnnotated);

    Set<Class<?>> getJpaEntities() {
        return REFLECTIONS.getTypesAnnotatedWith(Table.class);
    }
}

이 방식의 장점은 명확하다. 엔티티 클래스가 추가되거나 삭제되더라도 별도의 등록 작업 없이 자동으로 스캔 대상에 포함된다. 또한 특정 패키지 기준으로 범위를 제한할 수 있어 불필요한 클래스 탐색 비용도 줄일 수 있다.

만약 순서 보장이 필요하다면 @Table 어노테이션의 name 속성을 기준으로 정렬하는 방식도 적용할 수 있다. 이처럼 Reflections는 저수준 리플렉션 API를 직접 다루지 않으면서도 충분히 유연한 확장성을 제공한다.


리플렉션 기반 필드와 어노테이션 추출

엔티티 클래스를 수집했다면 다음 단계는 메타 정보 추출이다. DDL과 비교하기 위해서는 필드 타입, 컬럼 이름, 제약 조건 등 여러 정보를 함께 확인해야 한다.

특히 복합키를 사용하는 엔티티의 경우 @EmbeddedId와 @Embeddable이 적용된 중첩 클래스까지 함께 분석해야 한다. 단순히 clazz.getDeclaredFields()만 호출하면 이 정보는 누락된다.


public List<Field> extractFieldList(Class<?> clazz) {
    return Stream.concat(
        Arrays.stream(clazz.getDeclaredClasses())
            .filter(c -> c.isAnnotationPresent(Embeddable.class))
            .flatMap(c -> Arrays.stream(c.getDeclaredFields())),
        Arrays.stream(clazz.getDeclaredFields())
    ).toList();
}

이 방식은 중첩된 복합키 필드와 엔티티 본문 필드를 하나의 스트림으로 결합한다. 결과적으로 DDL 기준 컬럼 목록과 일대일 비교가 가능한 구조를 만들 수 있다.

필드 자체뿐 아니라 어노테이션 메타 정보도 중요하다. @JoinColumn, @JoinColumns와 같은 연관관계 어노테이션 역시 리플렉션을 통해 손쉽게 추출할 수 있다. 이를 통해 외래 키 제약 조건 검증까지 확장하는 것도 가능하다.

리플렉션은 무겁고 위험하다는 인식이 있지만, 스캔 범위를 명확히 제한하고 캐싱된 메타데이터를 활용한다면 충분히 안정적으로 사용할 수 있다.


DDL Entity 비교 검증 로직

마지막 단계는 DDL.sql과 JPA Entity 메타 정보를 직접 비교하는 검증 로직이다. DDL.sql은 이미 파싱되어 컬럼 타입과 제약 조건을 문자열 혹은 enum 형태로 얻었다고 가정한다.

가장 단순하면서도 중요한 검증은 NULL 제약이다. DDL에서 NOT NULL로 정의된 컬럼이라면 JPA Entity에서도 @Column(nullable = false)와 @NotNull 어노테이션이 함께 존재해야 한다.


public void validateNullConstraint(
    String ddlConstraint, Column column, Field field) {

    if ("NOT NULL".equals(ddlConstraint)) {
        if (column.nullable()) {
            throw new RuntimeException(
                field.getName() + " : @Column(nullable=false) 누락");
        }

        if (!field.isAnnotationPresent(NotNull.class)) {
            throw new RuntimeException(
                field.getName() + " : @NotNull 누락");
        }
    }
}

컬럼 타입 검증 역시 중요하다. 예를 들어 DDL에서 DATETIME으로 정의된 컬럼은 JPA Entity에서 LocalDateTime 타입과 columnDefinition 설정이 일치해야 한다.

이러한 검증을 통해 엔티티는 존재하지만 DDL이 누락된 경우, DDL은 있지만 엔티티가 없는 경우, 혹은 제약 조건이 어긋난 경우를 조기에 감지할 수 있다.

결과적으로 이 작은 도구는 Flyway를 대체하려는 목적이 아니라, 현재 프로젝트 범위에 맞는 가볍고 명확한 검증 장치로 기능한다. 리플렉션을 활용한 형상 검증은 생각보다 단순하며, 필요한 만큼만 구현했을 때 오히려 유지보수성이 높아진다.