TODO 리스트 토이 프로젝트를 해보다가 순환 참조 문제가 발생했다.
순환참조란, 참조하는 대상이 서로 물려 있어서 참조할 수 없게 되는 현상을 말한다.
TodoListInfo.java
@Entity
@Getter
@Builder
@NoArgsConstructor @AllArgsConstructor
public class TodoListInfo {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long code;
@OneToOne
private Member writer;
@OneToMany(mappedBy = "todoListInfo", cascade = CascadeType.ALL)
private List<Todo> todoList = new ArrayList<>();
}
Todo.java
@Entity
@Getter
@Builder
@NoArgsConstructor @AllArgsConstructor
public class Todo {
@Column(name = "todo_id")
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "todo_list_info_code")
private TodoListInfo todoListInfo;
private String description;
private boolean isCompleted;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
private LocalDateTime startTime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
private LocalDateTime endTime;
}
위 두 객체를 보면 TodoListInfo
에서는 List<Todo>
를 통해, Todo
에서는 TodoListInfo
를 통해 서로 참조하고 있다.
만약 JSON 응답으로 이 관계를 그대로 보내게 되면 순환 참조 문제가 발생한다.
예를 들어, TodoListInfo
의 형식 그대로 응답으로 내보냈다고 해보자.
그러면 TodoListInfo
의 List<Todo>
에서 각각의 Todo
는 TodoListInfo
를 참조할 것이다. 이 방식이 계속 반복되게 된다.
이러한 순환 참조를 막기 위한 2가지 방법을 알아보자.
@JsonIgnore
Todo
클래스의 TodoListInfo
필드에 @JsonIgnore 애노테이션을 추가해준다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "todo_list_info_code")
@JsonIgnore
private TodoListInfo todoListInfo;
이 애노테이션을 사용 시 조회할 경우 해당 필드를 조회하지 않는다.
DTO
해당 응답에 따른 DTO를 생성하여 필요한 데이터만 담아서 반환하면 순환 참조 문제를 방지할 수 있다.
ReadTodoListDto.java
@Data
@Builder
public class ReadTodoListDto {
private Long code;
private List<TodoDto> todoDtoList = new ArrayList<>();
private LoginOutputDto writer;
public static ReadTodoListDto of(TodoListInfo todoListInfo) {
return ReadTodoListDto.builder()
.code(todoListInfo.getCode())
.todoDtoList(todoListInfo.getTodoList()
.stream()
.map(todo -> TodoDto.of(todo))
.toList())
.writer(LoginOutputDto.of(todoListInfo.getWriter()))
.build();
}
}
TodoDto.java
@Data
@Builder
public class TodoDto {
private String description;
private boolean isCompleted;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
private LocalDateTime startTime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul")
private LocalDateTime endTime;
public Todo toEntity() {
return Todo.builder()
.description(description)
.isCompleted(isCompleted)
.startTime(startTime)
.endTime(endTime)
.build();
}
public static TodoDto of(Todo todo) {
return TodoDto.builder()
.description(todo.getDescription())
.isCompleted(todo.isCompleted())
.startTime(todo.getStartTime())
.endTime(todo.getEndTime())
.build();
}
}
TodoListInfo
를 조회할 때, DTO를 이용하여 필요한 정보만 담아줌으로써 순환 참조를 방지한다.
TodoDto
에는 TodoListInfo
에 대한 참조가 빠져있다.