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으로 변환하는 과정에서
- BookmarkGroup 내부에 있는 user 필드가 User 엔티티이고,
- 이 User는 지연 로딩(LAZY) 되어 있어 Hibernate가 프록시 객체(User$HibernateProxy)로 감싸고 있음,
- 이 프록시 안에 있는 내부 필드(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가 쿼리를 실행해 실제 값을 로딩한다.


로깅해보면 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를 따로 두는 게 유지보수 측면에서 훨씬 안전하다.
'오류' 카테고리의 다른 글
| [SpringBoot] response.getCharacterEncoding() 문자(한글) 깨짐 (0) | 2025.07.05 |
|---|---|
| [SpringBoot] No appenders present in context [default] for logger [org.jboss.logging]. (0) | 2025.07.04 |