JPA에서 양방향 연관 관계(예: @OneToMany, @ManyToOne 등)를 설정하면,
Spring Boot에서 JSON을 직렬화(Serialize)할 때 무한 루프(Infinite Recursion) 문제가 발생할 수 있다.
📌 무한 루프 발생 예시
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders;
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
❌ 문제 발생 상황
1️⃣ User 엔티티를 조회하면 orders 리스트도 같이 조회됨.
2️⃣ Order 엔티티에는 다시 User 객체가 들어있음.
3️⃣ JSON 변환 과정에서, User → Order → User → Order ... 무한 반복됨.
4️⃣ 결국, StackOverflowError(스택 오버플로우 에러) 발생!
{
"id": 1,
"name": "Kim",
"orders": [
{
"id": 101,
"orderNumber": "A123",
"user": { // 🔄 여기서 다시 user 정보가 들어가며 무한 루프!
"id": 1,
"name": "Kim",
"orders": [ ... ]
}
}
]
}
위 JSON을 보면, "orders" 안에 "user"가 다시 들어가고, 그 안에 또 "orders"가 들어가면서 끝없이 반복되는 걸 확인할 수 있다..
1️⃣ 해결 방법: @JsonIgnore 사용하기
가장 간단한 해결책은 @JsonIgnore을 사용해서 불필요한 직렬화를 막는 것..!
✅ 해결 코드
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@ManyToOne
@JoinColumn(name = "user_id")
@JsonIgnore // 🛑 JSON 변환 시 user 필드를 제외
private User user;
}
이렇게 하면 Order가 JSON으로 변환될 때 user 정보는 포함되지 않아서, 무한 루프 문제를 방지할 수 있다! 🎯
💡 @JsonIgnore 적용 후 JSON 결과 예시
{
"id": 1,
"name": "Kim",
"orders": [
{
"id": 101,
"orderNumber": "A123"
}
]
}
🚀 이제 "user" 객체가 더 이상 JSON에 포함되지 않으므로, 무한 루프가 해결됨!
2️⃣ 해결 방법: DTO 변환 사용하기
@JsonIgnore을 적용하면 간단하게 해결되지만,
만약 API에서 Order의 User 정보도 필요하다면 어떻게 해야 할까요?
이럴 때는 DTO 변환 방식이 가장 좋은 해결책이 될 수 있어요.
✅ DTO 변환 방식 적용
1️⃣ UserDTO & OrderDTO 만들기
@Getter @Setter
public class UserDTO {
private Long id;
private String name;
private List<OrderDTO> orders;
public UserDTO(User user) {
this.id = user.getId();
this.name = user.getName();
this.orders = user.getOrders().stream()
.map(OrderDTO::new)
.collect(Collectors.toList());
}
}
@Getter @Setter
public class OrderDTO {
private Long id;
private String orderNumber;
public OrderDTO(Order order) {
this.id = order.getId();
this.orderNumber = order.getOrderNumber();
}
}
2️⃣ Service에서 변환 후 반환
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDTO getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
return new UserDTO(user);
}
}
3️⃣ Controller에서 DTO 반환
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
💡 DTO 변환 후 JSON 결과 예시
{
"id": 1,
"name": "Kim",
"orders": [
{
"id": 101,
"orderNumber": "A123"
}
]
}
🚀 이제 불필요한 user 객체가 JSON에 포함되지 않고, 깔끔하게 반환됨!
3️⃣ 해결 방법: @JsonManagedReference & @JsonBackReference
Spring에서는 @JsonManagedReference와 @JsonBackReference를 사용하여
양방향 매핑의 직렬화를 자동으로 조절할 수도 있다.
✅ 적용 코드
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
@JsonManagedReference // 🔵 직렬화 허용
private List<Order> orders;
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@ManyToOne
@JoinColumn(name = "user_id")
@JsonBackReference // 🔴 역방향 직렬화 제외
private User user;
}
💡 "부모(User) → 자식(Order)" 관계에서는 JSON 직렬화 가능
💡 "자식(Order) → 부모(User)" 관계에서는 직렬화 제외
🟢 적용 후 JSON 결과
{
"id": 1,
"name": "Kim",
"orders": [
{
"id": 101,
"orderNumber": "A123"
}
]
}
🚀 양방향 관계지만, JSON 변환 시 루프가 발생하지 않도록 자동으로 처리됨!
4️⃣ 최종 정리
해결 방법 | 장점 | 단점 |
@JsonIgnore | 간단하고 빠름 | 특정 필드가 JSON에서 아예 제외됨 |
DTO 변환 | API 설계에 유연 | 변환 과정이 추가로 필요 |
@JsonManagedReference / @JsonBackReference | 자동 직렬화 조절 가능 | 복잡한 관계에서는 예상치 못한 동작 가능 |
📌 실무에서 가장 추천되는 방식
✅ @JsonIgnore: 가장 쉬운 방법이지만, 일부 데이터가 JSON에서 사라질 수 있음
✅ DTO 변환(추천!): API 설계가 자유롭고, 직렬화 문제 없이 필요한 데이터만 제공 가능
✅ @JsonManagedReference/@JsonBackReference: 자동 처리는 가능하지만, 관계가 복잡할 경우 예측하기 어려울 수 있음
🚀 결론: 가장 추천하는 해결 방법은?
무한 루프 문제를 해결하는 방법은 여러 가지가 있지만,
"API 설계의 확장성"과 "유지보수 편리함"을 고려하면 DTO 변환 방식이 가장 안정적
- 간단한 해결 👉 @JsonIgnore
- API 확장성 & 유지보수 용이 👉 DTO 변환 (가장 추천)
- 자동 직렬화 조절 필요 👉 @JsonManagedReference / @JsonBackReference
'개념정복💫 > 스프링 Spring 정복' 카테고리의 다른 글
JPA 메서드 네이밍 & 반환 타입 (0) | 2025.02.25 |
---|---|
스프링에서 Bean 주입 시 주의사항 (필드 vs 생성자 주입) (0) | 2025.02.24 |
JPA 일대일(OneToOne) 관계 매핑 (0) | 2025.02.23 |
즉시 로딩(EAGER)과 지연 로딩(LAZY), 언제 어떤 전략을 써야 할까? (1) | 2025.02.23 |
JPA 페치 타입: 즉시 로딩(EAGER)과 지연 로딩(LAZY) (0) | 2025.02.23 |