1. 문제 설명
Springboot의 @RestController는 Jackson을 사용하여 객체를 JSON으로 자동으로 직렬화 시킵니다.(return 시점에)
이때 직렬화 대상의 Domain이 연관관계가 있고 지연로딩을 사용한다면 다음과 같은 문제가 발생합니다.
예를 들어, 다음의 코드를 실행한다면
@Getter
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long courseId;
@Column(name = "title", length = 50)
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "professor_id")
private Professor professor;
public Course() {}
}
public Course getCourse(@PathVariable("courseId") Long courseId) {
Optional<Course> course = courseService.findOne(courseId);
Course courseEntity = course.get();
return courseEntity;
}
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: com.example.Application.domain.Course["professor"]->com.example.Application.domain.Professor$HibernateProxy$ql6ws0V2["hibernateLazyInitializer"])
이 오류는 Jackson이 Hibernate의 지연 로딩을 구현하는 프록시 객체(Professor$HibernateProxy)를 직렬화하려 할 때 발생합니다. Hibernate는 지연 로딩을 위해 프록시 객체를 생성하며, 이 객체는 hibernateLazyInitializer와 같은 내부 필드를 포함합니다. 그러나 Jackson은 이러한 프록시 객체를 직렬화할 수 없고, 이를 처리할 수 있는 직렬화기가 없기 때문에 No serializer found 오류가 발생합니다.
그러면 프록시 객체를 초기화하는 코드를 넣고 다시 실행하면
public Course getCourse(@PathVariable("courseId") Long courseId) {
Optional<Course> course = courseService.findOne(courseId);
Course courseEntity = course.get();
Hibernate.initialize(courseEntity.getProfessor()); //professor 초기화
return courseEntity;
}
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: com.example.Application.domain.Course["professor"]->com.example.Application.domain.Professor$HibernateProxy$ql6ws0V2["hibernateLazyInitializer"])
똑같이 프록시 객체를 직렬화할 수 없다고 나옵니다.
이는 프록시 객체를 초기화하더라도, 해당 객체는 여전히 프록시 객체로 유지되기 때문입니다. Hibernate.initialize는 단순히 내부 데이터를 로드하여 지연 로딩(lazy loading)을 해결할 뿐, 프록시 객체 자체를 실제 엔티티로 변환하지는 않습니다. 따라서 초기화된 프록시 객체는 여전히 프록시 클래스를 유지하며, 이를 직렬화하려고 할 때 문제가 발생할 수 있습니다.
문제의 원인을 정리하면
- Jackson은 Hibernate의 프록시 객체를 직렬화할 수 없습니다.
- 초기화를 해도 프록시 객체가 그대로 남아 있어 Jackson이 이를 직렬화하려고 시도하면서 문제가 발생합니다.
2. 해결 방법
2.1) 즉시 로딩(Eager Loading)으로 변경
즉시 로딩을 사용하면 연관된 데이터를 한 번의 쿼리로 가져오므로 프록시 객체가 생성되지 않습니다. 그러나 추가 JOIN이 발생하거나 JPQL을 사용할 경우 N+1 문제가 발생할 수 있습니다.
2.2) DTO(Data Transfer Object) 사용
DTO를 만들어서 필요한 데이터만 반환하도록 합니다.
@Getter
public class CourseResponse {
private final Long courseId;
private final String courseTitle;
private final String professorName;
public CourseResponse(Long courseId, String courseTitle, String professorName) {
this.courseId = courseId;
this.courseTitle = courseTitle;
this.professorName = professorName;
}
}
컨트롤러에서 DTO를 반환하도록 수정합니다.
public CourseResponse getCourse(@PathVariable("courseId") Long courseId) {
Optional<Course> course = courseService.findOne(courseId);
Course courseEntity = course.get();
return new CourseResponse(courseEntity.getId(), courseEntity.getTitle(), courseEntity.getProfessor().getName());
}
응답 전용 DTO를 만들면 다음의 장점들이 있습니다.
- 직렬화 문제 해결: 프록시 객체 문제를 해결합니다.
- 민감한 정보 보호: 민감한 정보를 숨길 수 있습니다.
- 최소한의 데이터 반환: 필요한 데이터만 반환하여 성능을 최적화할 수 있습니다.
3. 결론
Jackson은 기본적으로 Hibernate 프록시 객체를 직렬화할 수 없습니다.(Hibernate5Module를 사용하면 프록시 객체를 직렬화 시킬 수는 있습니다.) 이를 해결하기 위해 DTO를 사용하여 필요한 데이터만을 반환하는 것이 좋습니다. 이렇게 하면 지연 로딩 전략을 유지하면서도 외래키와 관련된 내용을 안전하게 직렬화할 수 있습니다.
'컴퓨터 > JAVA' 카테고리의 다른 글
JAVA - 리플렉션(Reflection) (0) | 2025.02.14 |
---|---|
Spring - Log 레벨 이해 및 JAVA의 로그 프레임워크 종류 (0) | 2025.01.16 |
JPA - 연관관계 매핑 2(프록시 객체) (0) | 2024.12.05 |
JPA - 연관관계 매핑 1(외래키 설정) (0) | 2024.11.28 |
JPA - 영속성 컨텍스트(Persistence Context) (0) | 2024.11.23 |