Spring Boot에서 JPA와 Jackson 라이브러리를 사용할 때 양방향 관계를 갖는 엔티티를 직렬화 하면 무한 루프(StackOverflowError)가 발생할 수 있다.
직렬화(Serialization)란?
직렬화(Serialization): 객체를 저장하거나 전송할 수 있도록 JSON, XML 또는 바이트 스트림으로 변환하는 과정이고 반대를 역직렬화라고 한다.
Spring Boot에서는 컨트롤러에서 객체를 응답으로 반환할 때 Jackson이 자동으로 JSON으로 변환시킨다 이때 JPA 엔티티가 양방향 관계를 가질 경우 직렬화 시 무한 루프가 발생할 수 있다
문제가 발생한 엔티티 구조이다
@Entity
@Getter
@Setter
@Slf4j
public class Lecture {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "lectures") // Timetable이 주인이므로 mappedBy 사용
private List<Timetable> timetables = new ArrayList<>();
@OneToMany(mappedBy = "lecture", cascade = CascadeType.ALL, orphanRemoval = true)
private List<LectureTime> lectureTimes = new ArrayList<>(); // 여러 개의 시간 블록을 가질 수 있음
public Lecture() {
}
}
@Entity
@Getter
@Setter
public class Timetable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
@JsonBackReference
private Member member;
//다대다 lecture와 timetable 중간엔티티가 의미가없어서 manytomany로 간단히 구현 단방향
@ManyToMany
@JoinTable(
name = "timetable_lecture", // 중간 테이블 이름
joinColumns = @JoinColumn(name = "timetable_id"), // Timetable이 주인
inverseJoinColumns = @JoinColumn(name = "lecture_id") // Lecture 연결
)
private List<Lecture> lectures = new ArrayList<>();
private String name;
}
@Entity
@Getter
@Setter
@Table(name = "lecture_times")
public class LectureTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne //자식관계
@JoinColumn(name = "lecture_id", nullable = false) //FK 설정
private Lecture lecture;
}
Lecture 객체를 JSON으로 변환하려고 하면 Lecture가 참조하고 있는 Timetable과 LectureTime도 직렬화가 이루어진다. 이때 Timetable과 LectureTime도 Lecture를 참조하고있어 무한 루프가 발생한다.
DTO로 변환해서 양방향 관계 제거 + 필요한 데이터만 포함 시켜서 해결할 수 있다
DTO 변환을 통한 해결 방법
@Data
public class LectureDTO {
private Long id;
private String title;
private String professor;
private List<LectureTimeDTO> lectureTimes;
public LectureDTO(String title, String professor, List<LectureTimeDTO> collect) {
this.title = title;
this.professor = professor;
this.lectureTimes = collect;
}
}
@Data
public class LectureTimeDTO {
private String dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public LectureTimeDTO(String dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
}
해당 프로젝트에서는 Timetable의 정보는 필요없어서 DTO에 넣지 않고 lectureTime은 DTO로 변경해서 LectureDTO를 생성하고 있다
이때 양방향 관계 제거를 위해 LectureTimeDTO는 Lecture를 포함하고있지 않는다.