Tiny Star

오류

[SpringBoot] Entity 객체 반환 오류 (SerializationFeature.FAIL_ON_EMPTY_BEANS)

하얀지 2025. 7. 8. 22:22

SerializationFeature.FAIL_ON_EMPTY_BEANS 오류

aused by: cohttp://m.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.ArrayList[0]->io.github.newsgptback.news.domain.bookmarkGroup.BookmarkGroup["user"]->io.github.newsgptback.news.domain.user.User$HibernateProxy["hibernateLazyInitializer"])

 

객체를 JSON 으로 변환하는 과정에서 발생하는 문제다.

에러 문구를 잘 살펴보면 `java.util.ArrayList[0] -> BookmarkGroup["user"] -> User$HibernateProxy["hibernateLazyInitializer"]` 이부분에서 에러났음을 알 수 있다.

 

즉, BookmarkGroup 객체를 JSON으로 변환하는 과정에서

  1. BookmarkGroup 내부에 있는 user 필드가 User 엔티티이고,
  2. 이 User는 지연 로딩(LAZY) 되어 있어 Hibernate가 프록시 객체(User$HibernateProxy)로 감싸고 있음,
  3. 이 프록시 안에 있는 내부 필드(hibernateLazyInitializer)를 Jackson이 직렬화할 수 없어 에러 발생.

 

@Entity
public class BookmarkGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    ...

문제가 되는 컬럼은 `user_id` 로 `FetchType.LAZY` 설정이 되어있다.

`BookmarkGroup`을 조회할 때 `User`는 즉시 DB에서 조회되지 않고, `getUser()`를 호출하는 시점에 프록시 객체를 통해 실제 쿼리가 실행하게 한다. 이렇게 함으로써 불필요한 join 쿼리를 방지할 수 있기 때문에 대부분 `FetchType.LAZY` 로 설정한다.

 

 

프록시 객체

 

프록시 객체는 "가짜 객체"라고 보면 된다. 실제로는 join 으로 가져와야하지만, 위에서 말했다시피 join 비용을 줄이기 위해 실제로 조회를 하지 않는다. `BookmarkGroupRepository.findAll()`로 불러오면 `bookmarkGroup.getUser()`는 `User`의 프록시 객체를 반환한다.
이 객체는 겉보기엔 `User`지만, 실제 필드들은 아직 초기화되지 않았으며, DB에서 값을 로드하지 않는다. `user.getId()` 같은 메서드를 호출하는 순간 Hibernate가 쿼리를 실행해 실제 값을 로딩한다.

 

logging
HibernateProxy

로깅해보면 HibernateProxy 객체로 생성된 것을 확인할 수 있다.

이 프록시 객체는 내부적으로 Hibernate 전용 구조를 포함하고 있기 때문에, 이를 JSON 직렬화하려 할 경우 Jackson이 직렬화할 수 없어 예외가 발생한다.

 

 

직렬화

 

직렬화는 Java 객체를 JSON, XML, 바이트 등으로 변환하는 것이다.

현재 상황에서의 직렬화는 Hibernate 프록시 객체를 포함한 Java 객체를 JSON으로 변환하는 것이다. (ex. Entity를 ResponseEntity.body() 에 담아서 내보낼 때)

 

 

해결방법

@JsonIgnore 와 Hibernate5JakartaModule() 을 적용하는 방법이 있다.

 

@JsonIgnore

`FetchType.LAZY` 인 필드에 `@JsonIgnore` 을 붙이기만 하면 된다. 직렬화에서 해당 필드를 제외하겠다는 의미이다.

장점은 필드 단위로 명확하게 관리할 수 있다는 것이고, 단점은 필요할 때마다 해당 어노테이션을 하나하나 적용해야한다는 것이다.

 

Hibernate5JakartaModule()

ObjertMapper의 Module에 추가해주면 된다.

`Hibernate5JakartaModule` (혹은 `Hibernate5Module`)은 Jackson에게 "프록시 객체를 무시하거나, 필요하면 초기화해서 직렬화하라" 는 것을 알려주는 모듈이다.

설정 동작
.configure(Feature.FORCE_LAZY_LOADING, false) Lazy 객체는 직렬화에서 제외함 (기본값)
.configure(Feature.FORCE_LAZY_LOADING, true) Lazy 객체도 강제로 초기화해서 직렬화함 → 쿼리 발생
Hibernate5JakartaModule module = new Hibernate5JakartaModule();
module.configure(Hibernate5JakartaModule.Feature.FORCE_LAZY_LOADING, true);

objectMapper.registerModule(module);

위와 같이 설정하고 직렬화 대상에 LAZY 필드가 포함되면, 해당 필드는 강제로 초기화되며 쿼리가 실행된다.

 

기본 설정(`FORCE_LAZY_LOADING = false`) 은 초기화되지 않은 프록시 객체(Lazy 필드) 를 Jackson이 자동으로 무시한다. 다만 이는 `@JsonIgnore` 처럼 무조건 제외하는 것이 아니라, 초기화 여부에 따라 직렬화할지 말지를 동적으로 판단하는 방식이다.

 

그럼 @JsonIgnore와 Hibernate 모듈 false는 동일한가?

직렬화한 값만 따지자면 사실상 user 값이 없는 건 동일하다.

`@JsonIgnore` 가 붙은 필드는 아예 직렬화 시도조차 하지 않고

`Hibernate 모듈`은 직렬화 시도는 하되 실패 시 null 값이 반환된다는 차이점이 있다고 보면 될 것 같다.

[@JsonIgnore] 
{"id":1,"name":"기본"}
[HibernateModule false / 초기화X]
{"id":1,"user":null,"name":"기본"}
[HibernateModule false / 초기화O] 
{"id":1,"user":{"id":1,"email":"user@test.com","password":"***"} ,"name":"기본"}
[HibernateModule true  / 초기화X] 
{"id":1, "user":{"id":1,"email":"user@test.com","password":"***"},"name":"기본"}
[HibernateModule true  / 초기화O]
{"id":1,"user":{"id":1,"email":"user@test.com","password":"***"},"name":"기본"}

* `Hibernate.initialize(bookmarkGroup.getUser());` 또는 `bookmark.getUser().getUsername()` 를 통해 직렬화 진행

 

무엇을 사용해야하나?

가장 바른 방법은 Entity 가 아닌 Response DTO를 생성해서 내보내는게 가장 좋은 방법이다. 왜냐하면 Entity 에는 password, create_dt 등 중요 정보/불필요한 정보가 담겨져 있기 때문에 아무리 편해도 관리를 위해 별도 DTO를 관리하는 것이 좋다. 

"나는 DTO를 늘리고 싶지 않다!"는 생각이 있다면, `@JsonIgnore` 또는 `Hibernate5Module`을 상황에 맞게 적용하면 되지만, 장기적으로는 응답 DTO를 따로 두는 게 유지보수 측면에서 훨씬 안전하다.

top