사건의 발단
어느 날, 잘 되던 API 에서 이런 오류가 나옵니다.
Estimate 라는 엔티티를 들고 있는 Tender 라는 데이터를 저장하려는데 아래 오류 발생. (estimate 도 tender 를 참조하고 있는 상황)
Call to TraversableResolver.isReachable() threw an exception
로그를 까보다보니
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role:
com.healing.어쩌구.. 등이 나옵니다. 왠지 hibernate proxy object 와 연관이 있어보입니다.
javax.validation.ValidationException: HV000041: Call to TraversableResolver.isReachable() threw an exception.
at org.hibernate.validator.internal.engine.ValidatorImpl.isReachable(ValidatorImpl.java:1621)
at org.hibernate.validator.internal.engine.ValidatorImpl.isValidationRequired(ValidatorImpl.java:1597)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:609)
...중략
at org.hibernate.internal.SessionImpl.save(SessionImpl.java:707)
at org.hibernate.internal.SessionImpl.save(SessionImpl.java:702)
at com.healing.beauty.dao.BaseDao.saveNew(BaseDao.java:114)
at com.healing.beauty.service.EstimateService.add(EstimateService.java:90)
...중략
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.healing.beauty.domain.Tender.estimates, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:576)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:215)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:555)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:143)
at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:447)
at com.healing.beauty.domain.**Tender**.hashCode(Tender.java:28)
at com.healing.beauty.domain.**Estimate**.hashCode(Estimate.java:16)
at org.hibernate.validator.internal.engine.resolver.CachingTraversableResolverForSingleValidation$TraversableHolder.buildHashCode(CachingTraversableResolverForSingleValidation.java:143)
at org.hibernate.validator.internal.engine.resolver.CachingTraversableResolverForSingleValidation$TraversableHolder.<init>(CachingTraversableResolverForSingleValidation.java:104)
at org.hibernate.validator.internal.engine.resolver.CachingTraversableResolverForSingleValidation$TraversableHolder.<init>(CachingTraversableResolverForSingleValidation.java:86)
at org.hibernate.validator.internal.engine.resolver.CachingTraversableResolverForSingleValidation.isReachable(CachingTraversableResolverForSingleValidation.java:31)
at org.hibernate.validator.internal.engine.**Va*lidatorImpl.isReachable***(ValidatorImpl.java:1612)
... 103 common frames omitted
아래 메시지까지 보니 hibernate
에서 엔티티를 저장하는 과정에서 validate
관련 프로세스가 있는데 이때 entity 의 hashCode
가 쓰이는듯하고 hashCode()
에서 Tender 와 Estimate (이제는 안쓰이는 강남언니 도메인들) 가 쓰이는데 현재 2개의 엔티티는 서로를 참조하고 있다보니 오류가 나오는것으로 보입니다.
그래도 기존까지 잘 쓰였었는데 ( @JsonIgnore
나 @JackBackReference
등을 이용해서 내려줄때 순환참조 되지 않게 막을 수 있다) 왜 이런 이슈가 나올까 했더니..
새로 추가한 Lombok
이 문제였습니다.
원인파악
Lombok
으로 바꾸면서 hashCode(), toString() 자동 생성됨
롬복에서 @Data
어노테이션을 쓰면 자동으로 toString()
과 equals()
hashCode()
등의 메소드를 생성해서 존재하는 모든 프로퍼티들을 기반으로 메소드를 작성합니다.
Lombok
자동생성 toString
method는 해당 클래스의 모든 필드와 값을 String
값으로 표현해주게 됩니다. 이 때, 이 toString
같은 녀석은 @JsonIgnore
계열의 어노테이션과 상관없이 수행되는 메소드( serialization
과 무관)이기 때문에 Proxy object
를 그대로 스티링화의 대상으로 인식해서 수행합니다. 그러니 **initialize 안된 Proxy object**
를 쓰려다가 오류가 나고 있었습니다. (물론 이 말은 어딘가 로깅등에서 toString
을 쓰고 있었던것으로 보인다)
해결방법
-
ToString 과 Equals 등을 꼭 사용하고 싶다면 → 개별 exclude
-
제외할 필드 (프록시로 다른 엔티티와 연결된) 를
@ToString
을 따로 명시적으로 써서exclude
시킵니다. 꼭 해당 필드도 투스트링이나 해시코드등에 사용해야 한다면 해당 메소드를override
@Entity @Data @EqualsAndHashCode(exclude="foo") @ToString(exclude="foo") public class Bar { @ManyToOne private Foo foo; }
-
-
게터세터만 생성되면 오케이 →
@Getter
,@Setter
만 사용- toString 등 을 자동시전 해버리는
@Data
는 조금 위험할 수 있으니,@Getter
,@Setter
어노테이션만을 선택적으로 사용합니다.
@Data
→ All together now: A shortcut for @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non-final fields, and @RequiredArgsConstructor@Entity @Getter @Setter public class Bar { @ManyToOne private Foo foo; }
- toString 등 을 자동시전 해버리는
결론
hibernate
로 만들어진 entity
는 DB 와 커넥션이 이어져 있는 Persistent
상태로 존재하게 되는데, 이 때 이미 데이터를 다 가져와서 매핑되어 제대로 만들어진 객체가 있는 반면 lazy
로 연결된 entity
들은 proxy
형태로 남아있다가 initialize 되어야만 사용이 가능합다. 그런데 만약 initializing 이 안된 상태에서 transaction
이 종료돼면 해당 데이터는 그냥 프록시의 껍데기만 남게 되고 이 과정에서 다양한 문제들이 발생할 수 있게 됩니다.
껍데기채로 리스폰스에 담겨서 client 가 이해하지 못하는 밸류가 전달되거나, 위에 처럼 껍데기만 있는데 사용하려다가 lazy 오류가 난다거나... hibernate 버전이 올라가면서 이와 같은 오류를 핸들링하거나 방지하는 것들이 생기는데, 예를 들면 아래처럼 init 안된 object 를 serialize 할때는 자동으로 null 로 바꿔서 오류가 안나게 해주는 모듈도 존재합니다. 해당 모듈을 아래처럼 ****추가해주어서 jackson converter 로 통신하는 것을 도와줄 수 있습니다
public class HibernateAwareObjectMapper extends ObjectMapper {
private static final long serialVersionUID = 2293078232819971861L;
// objectMapper를 hibernate4module 사용
public HibernateAwareObjectMapper() {
**Hibernate4Module** hm = new Hibernate4Module();
// 추가적으로 @transient 필드를 serialize 하지 않는 설정을 방지
hm.disable(Feature.USE_TRANSIENT_ANNOTATION);
**registerModule**(hm);
}
}
<mvc:annotation-driven>
<mvc:message-converters>
<bean
class="org.springframework.http.converter.json.**MappingJackson2HttpMessageConverter**">
<property name="objectMapper">
<bean primary="true" class="com.healing.beauty.common.**HibernateAwareObjectMapper**" />
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
이런것과 관련된 고생을 너무 많이 해서 여러 포스팅을 적어두었는데 추후에 정리해봐야겠습니다.
역시 조심해서 잘 쓰시기를.. 늘 느끼는것이지만 매우 편리하지만 꽤 위험한 ORM 의 세계
참고
Does Lombok @Data override the existing toString and hashCode methods?