티스토리 뷰

기존 spring boot 3.0.13 버전에서 3.1.12 로 버전업을 시도했다. 하위호환을 잘 보장해준 덕에 큰 이슈없이 빌드에 성공했는데 엔티티 조회시 ArrayIndexOutOfBoundsException 이 발생하는걸 확인했다. 해당 엔티티는 크게 복잡하지 않은 보통의 엔티티였는데 왜 문제가 발생한건지 정리하려한다.
 
문제가 발생하는 환경을 간단한 엔티티로 표현하면 이렇다.

@Entity
@Table(name = "car")
public class Car {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "car_id")
    private Long id;

    @Column(name = "car_type")
    private CarType carType;

    public Car() {
    }

    public Car(CarType type) {
        this.carType = type;
    }

    public Long id() {
        return this.id;
    }

    public CarType carType() {
        return this.carType;
    }
}

public enum CarType {
    AUTO, STICK
}

// 위 엔티티에 대한 저장/조회 테스트
@Test                                                       
@Transactional                                              
@Rollback(false)                                            
void 엔티티_저장_후_조회() {                                        
    Car car = new Car(CarType.STICK);                       
    entityManager.persist(car);                             
    entityManager.clear();                                  
                                                            
    Car foundCar = entityManager.find(Car.class, car.id()); 
    assertThat(foundCar).isNotNull();                       
}

// 테스트 실행시 에러로그
Index 49 out of bounds for length 2
java.lang.ArrayIndexOutOfBoundsException: Index 49 out of bounds for length 2
	at org.hibernate.type.descriptor.java.EnumJavaType.fromByte(EnumJavaType.java:202)
	at org.hibernate.type.descriptor.java.EnumJavaType.wrap(EnumJavaType.java:139)
	at org.hibernate.type.descriptor.java.EnumJavaType.wrap(EnumJavaType.java:36)
	...

 
저장엔 문제가 없는데 Car 엔티티를 조회해올때 예외가 발생하는걸 볼 수 있다.
 

# 원인

엔티티 정의코드를 유심히 봤다면 왜 이게 없지? 싶은게 있는데, carType enum 필드에 @Enumerated 이 없다. enum 필드에 해당 애노테이션이 없으면 기본값인 EnumType.ORDINAL 로 동작한다. 실무에서는 EnumType.ORDINAL 로 쓰는 경우가 거의 없기 때문에 기본값이 아니라 매번 EnumType.STRING 으로 매핑을 하게된다. 하지만 어디까지나 EnumType.ORDINAL 도 JPA 표준스펙이기 때문에 EnumType.ORDINAL 로 매핑한다고해서 문제가 발생한다면 그건 JPA 구현체의 버그라고 볼 수 있다. 여기서 한가지 조건이 더 추가되는데 JPA 엔티티에서는 EnumType.ORDINAL 로 매핑하면서, DB 스키마는 varchar 여야한다.
 
EnumType.ORDINAL 은 enum 의 순서로 매핑하기 때문에 DB 에 숫자로 들어가게된다. 그래서 보통 DB 스키마는 숫자타입을 쓰게되는데 숫자타입이 아니라 문자열 타입으로 지정해놨을때 해당 문제가 발생한다. 문제가 발생하는 JPA 구현체인 Hibernate 는 6.1 에서 6.2 로 버전업할때 enum 에 대한 로직수정이 있었는데 예외가 발생하는건 이때 발생한 사이드이펙트로 보인다. 6.2 마이그레이션 가이드를 보면 enum 매핑 로직에 대한 언급을 확인할 수 있다. 참고로 spring boot 는 3.0.x 까진 Hibernate 6.1.x 를, 3.1.0 부터 Hibernate 6.2.x 를 사용한다.
https://github.com/hibernate/hibernate-orm/blob/6.2/migration-guide.adoc#ddl-implicit-datatype-enum
 
근데, 그럼 왜 이렇게 매핑했을까? 아마도 EnumType.STRING 으로 매핑하려했던건데 enum 필드에 매핑 애노테이션을 붙이는걸 실수로 누락했을 것 같다.
 

# 해결

이 부분은 Hibernate 6.2 에서 발생하는 문제니 Hibernate 의 버그로 보고 Hiberante 가 패치를 해줄까? 글을 작성하는 이 시점에 Hibernate 최신버전은 이미 6.2 를 아득히 넘어 6.6.4 까지 왔고 위 문제는 전혀 해결되지 않았다. Hibernate 가 이 문제를 모르는걸까? 이미 23년도에 같은 문제에 대해 논의한 내용을 찾을 수 있었다.
https://discourse.hibernate.org/t/hibernate-6-cannot-persist-enum-as-ordinal-in-varchar-column/7775
 
Hibernate 측에선 EnumType.ORDINAL 로 매핑하면서 왜 DB 컬럼타입을 varchar 로 하냐고 오히려 반문했다. 즉 애초에 그렇게 매핑해도 문제가 없었던건 Hibernate 가 의도한 스펙이 아니었기 때문에 이 문제를 Hibernate 가 버그로 보고 패치를 진행할 의사가 없고, 이미 문제를 인지한 상태에서 6.6.4 까지 진행했다는 것이다. 만약 시간이 더 흘러 다른버전에서 다시 매핑이 정상적으로 진행된다면 그또한 표준스펙이 아니라 그냥 사이드이펙트일 확률이 높다.
 
사실 Hibernate 에서 수정을 해주지 않더라도 원인을 알았으니 해결은 쉽다. enum 의 매핑을 EnumType.ORDINAL 이 아니라 EnumType.STRING 으로 변경하거나, DB 의 컬럼 타입을 숫자로 바꿔주면 된다. 하지만 새로 시작하는 프로젝트라거나 개인 프로젝트가 아닌 이상 위 링크에서 문제를 제기한 사람도 얘기했듯 이미 레거시 코드가 있는 엔터프라이즈 환경에서 저 둘을 변경하는건 생각보다 쉽지 않다. 그래서 기존 코드를 건드리지 않고 별도의 컨버터를 구현하는 방식으로 문제를 해결했다.

@Converter
public class CarTypeOrdinalConverter implements AttributeConverter<CarType, String> {
    private static final CarType[] values = CarType.values();

    @Override
    public String convertToDatabaseColumn(CarType attribute) {
        return String.valueOf(attribute.ordinal());
    }

    @Override
    public CarType convertToEntityAttribute(String dbData) {
        return values[Integer.parseInt(dbData)];
    }
}

// 잘못매핑되어있던 필드에 컨버터 적용
@Convert(converter = CarTypeOrdinalConverter.class)
private CarType carType;

 
혹시라도 잘못매핑한 타입이 2개 이상이라면 추상화를 통해 해결할 수도 있다.

@Converter
abstract class AbstractEnumOrdinalConverter<T extends Enum<?>> implements AttributeConverter<T, String> {
    private final T[] values = reifyEnumType().getEnumConstants();
    
    @Override
    public String convertToDatabaseColumn(T attribute) {
        return String.valueOf(attribute.ordinal());
    }

    @Override
    public T convertToEntityAttribute(String dbData) {
        return values[Integer.parseInt(dbData)];
    }

    protected abstract Class<T> reifyEnumType();
}

public class CarTypeOrdinalConverter extends AbstractEnumOrdinalConverter<CarType> {
    @Override
    protected Class<CarType> reifyEnumType() {
        return CarType.class;
    }
}

 
 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함