![[JPA] 양방향 참조 시에 발생하는 순환 참조 문제](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCsbC2%2FbtsGZkwaFPU%2FZwBhkYBgbddHef7jcb7J6K%2Fimg.png)
JPA에서는 @OneToMany, @ManyToOne을 사용하여 연관관계를 맺을 수 있습니다. 덕분에 객체 지향적으로 DB를 다룰 수 있게 됩니다. 특히나 CRUD 과정을 간소화할 수 있습니다.
그러나 편한만큼 주의해야 할 점도 다양합니다. 이번 주제에서 다룰 순환참조 또한 JPA를 다루면 꼭 마주하는 문제입니다. 순환참조는 두 엔티티가 양방향 연관관계로 설정되어 있을 때 발생합니다. A가 B를 참조하고, B가 A를 참조하게 되면서 무한히 반복됩니다.
순환 참조가 발생하는 예제와 해결하는 방법에 대해서 알아보겠습니다.
엔티티 양방향 연관관계 설정
User 엔티티
패키지 정보 확인하기
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@NoArgsConstructor
@Getter
@Setter
@Table(name = "USERS")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@Builder
public User(String username) {
this.username = username;
}
}
Team 엔티티
패키지 정보 확인하기
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "team")
private List<User> users = new ArrayList<>();
private String teamName;
@Builder
public Team(String teamName) {
this.teamName = teamName;
}
}
User에서 @ManyToOne으로 Team을 참조하고, Team에서 @OneToMany를 사용하여 User를 참조하고 있습니다. 이를 양방향 참조라고 합니다.
문제의 코드
UserController
addTeam API를 호출하는 과정에서 순환 참조가 발생합니다. 그 이유는 반환타입인 GetUserResponseDto에 문제가 있기 때문인데, 아래에 알아보겠습니다.
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public List<GetUserResponseDto> getUsers() {
return userService.getUsers();
}
@PostMapping
public CreateUserResponseDto createUser(@RequestBody String username) {
return userService.createUser(username);
}
@PostMapping("/{userId}/teams")
public GetUserResponseDto addTeam(@PathVariable("userId") Long userId, @RequestBody
AddTeamRequestDto request) {
return userService.addTeam(userId, request);
}
}
GetUserResponseDto
@Getter
@AllArgsConstructor
public class GetUserResponseDto {
private Long userId;
private String username;
private Team team;
}
DTO에서 Team 타입의 객체를 반환하는 과정에서 문제가 발생합니다. Team과 User는 양방향 참조 관계에 있습니다. RestController를 사용하기 때문에 데이터가 반환될 때 JSON 형태로 변환하게 됩니다. 객체를 JSON 형태로 변환하는 과정에서 직렬화가 일어납니다.
DTO를 JSON으로 변환하는 과정에서 직렬화가 발생하기 때문에, DTO에 포함된 Team 객체가 직렬화됩니다. Team 객체를 직렬화하는 과정에서 참조하고 있는 User를 같이 직렬화하게 됩니다. 이때, User 또한 Team을 참조하고 있기 때문에 Team을 직렬화하려고 시도합니다. 이러한 과정이 반복되면서 Team -> User -> Team -> User... 순환 참조가 발생합니다.
순환 참조 해결하기
직렬화 과정에서 발생하는 순환 참조를 해결하는 방향은 다양하겠지만, 가장 좋은 방법은 DTO 객체를 반환하지 않는 것이라 생각합니다.
순환 참조가 발생한 근본적인 원인이 DTO에서 객체를 반환했기 때문에 이를 제외해주면 바로 해결될 문제입니다.
DTO에서 객체를 반환하는 것이 아니라 객체에 포함된 필드를 직접 반환하면 됩니다.
@Getter
@AllArgsConstructor
public class GetUserResponseDto {
private Long userId;
private String username;
private Long teamId;
private String teamName;
// private Team team;
}