개발(0)/JPA
🚀 JPA 프로젝트에서 DTO(Data Transfer Object)와 어노테이션 활용하여 순환 참조 해결하기
까만밀가루
2025. 2. 4. 20:15
JPA를 공부하면서 순환참조에 대해 제대로 이해가 되지 않았고, 나에게도 이런 문제가 생길까? 했는데 토이 프로젝트 과정 중 떡하니 바로 생긴 순환 참조 ㅎㅎ
이번 글에서는 ProductController에서 발생한 중복 데이터 반환 문제의 원인과 그 해결 방법을 알아본다.
✅ 1️⃣ 문제 원인 분석
📌 (0) 순환 참조 문제란?
양방향 관계에서 JSON 직렬화 시 무한 루프 발생
@Entity
public class Product {
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
private List<Review> reviews;
}
@Entity
public class Review {
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
}
📌 (1) 양방향 연관관계 순환 참조 문제
- JPA에서는 엔티티 간의 양방향 연관관계를 설정하였고, Product와 Review의 관계가 서로 참조
- Product → Review → Product로 순환 참조가 발생합니다.
- 이로 인해 JSON 직렬화 시 무한 루프에 빠지면서 중복 데이터가 무한히 반환되고 있음
📌 (2) 잘못된 Fetch 전략 설정 (EAGER 로딩)
- JPA의 기본 Fetch 전략은 LAZY이지만, EAGER로 설정 시 연관된 모든 데이터가 즉시 로딩
- 이로 인해 불필요한 데이터까지 포함되어 API 응답이 과도하게 커지는 문제가 발생합니다.
🚀 2️⃣ 해결 방법
✅ (1) 순환 참조 방지 설정
Product.java
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference // 순환 참조 방지
private List<Review> reviews;
Review.java
@ManyToOne
@JoinColumn(name = "product_id", referencedColumnName = "id", nullable = false)
@JsonBackReference // 순환 참조 방지
private Product product;
- @JsonManagedReference: 직렬화 시 포함할 데이터로 유지
- @JsonBackReference: 직렬화 시 제외하여 무한 루프 방지
✅ (2) DTO로 데이터 반환하기
- DTO (Data Transfer Object): 데이터베이스의 엔티티(Entity)와 프론트엔드 간에 필요한 데이터만 전송하기 위해 사용되는 객체
- 엔티티 객체를 그대로 반환하면 해당 엔티티와 연관된 모든 데이터가 포함되기 때문에 엔티티 대신 필요한 필드만 포함한 DTO를 사용하여 응답 데이터를 관리
ProductDTO.java
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ProductDTO {
private Long id;
private String name;
private String description;
private double price;
private Integer stock;
public ProductDTO(Product product) {
this.id = product.getId();
this.name = product.getName();
this.description = product.getDescription();
this.price = product.getPrice();
this.stock = product.getStock();
}
}
✅ (3) 컨트롤러 수정
- Controller에서 Product 엔티티에 들어있는 불필요한 데이터 List<Preview>가 포함되지 않도록 함
ProductController.java
@GetMapping
public ResponseEntity<List<ProductDTO>> getAllProducts() {
List<ProductDTO> products = productService.getAllProducts().stream()
.map(ProductDTO::new) // Product → ProductDTO로 변환
.toList();
return ResponseEntity.ok(products);
}
✅ (4) 불필요한 데이터 로딩 방지 (Lazy 로딩 적용)
- 실제로 필요할 때만 데이터를 로딩되도록 Lazy 적
Product.java
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
private List<Review> reviews;
이렇게 하여 순환참조를 해결하였다.
엔티티 관계를 좀 더 읽고 DTO를 잘 활용하도록 해야겠다.