티스토리 뷰
JPA(Hibernate) 사용시 enum 필드에서 ArrayIndexOutOfBoundsException 가 발생한다면
LichKing 2024. 12. 21. 13:24기존 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;
}
}
'Java > jpa' 카테고리의 다른 글
spring boot 2.x hibernate 5.x 에서 매핑키만 있고 데이터는 없는 경우 전체 엔티티 조회 실패 (0) | 2024.04.30 |
---|---|
Entity Merge 시에는 동일한 객체를 반환하지 않는 이유 (0) | 2023.05.07 |
@DynamicUpdate 는 언제 써야할까 (0) | 2023.01.10 |
non-primary key 를 이용해 연관관계 설정시 Serializable 인터페이스 구현 (1) | 2021.06.16 |
JPA 양방향 연관관계 매핑 (0) | 2020.04.25 |
- Total
- Today
- Yesterday
- TEST
- OOP
- mariadb
- JavaScript Core
- java8
- programming
- 정규표현식
- Kotlin
- EffectiveJava
- db
- frontcode
- Design Pattern
- code
- MySQL
- DesignPattern
- javascript
- frontend개발환경
- go-core
- clean code
- http
- toby
- backend개발환경
- Jackson
- servlet
- java
- Spring
- generics
- Git
- spring cloud
- JPA
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |