Spring Boot에서 JPA 직렬화 무한 루프 문제 해결 (DTO 변환)

최형안·2025년 3월 7일

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를 포함하고있지 않는다.

0개의 댓글