본문 바로가기

프로그래밍/Troubleshooting

JPA 순환 참조, API 호출 시 순환 참조가 발생하는 이유

반응형

JPA의 순환 참조란 객체 간의 상호 참조가 무한 반복되는 경우를 말한다.

객체 그래프에서 한 객체가 다른 객체를 참조하고, 그 다른 객체가 다시 처음 객체를 참조하는 상황이고 스택 오버 플로우가 발생할 수 있다.

 

사이드 프로젝트 진행 하던 도중 Service 단 개발을 끝내고 API를 만들다가 스택 오버 플로우가 발생 하였다.

순환 참조를 미쳐 신경쓰지 못하고 있었음.. 순환 참조를 잘 예방해야겠다고 다시 한 번 생각함 

 

Service를 테스트 할 때 까지만 해도 발생하지 않았던 문제인데 REST API를 구현하면서 문제가 발생한 이유는

ResponseBody를 구현할 시 Object 형태의 값을 직렬화를 이용해 JSON 형태로 변환하는데 이 때 순환 참조가 발생하기 때문이다.

 

public class Orders {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Integer quantity;
    private Integer totalPrice;
    private OrderStatus orderStatus;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @OneToMany(mappedBy = "orders")
    private List<OrderProduct> orderProducts = new ArrayList<>();

    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "payment_id")
    private Payment payment;

    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "shipment_id")
    private Shipment shipment;
}
public class Product {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
    private String description;
    private Integer price;
    private Integer quantity;

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;

    @OneToMany(mappedBy = "product")
    private List<OrderProduct> orderProducts = new ArrayList<>();
}
public class OrderProduct {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "orders_id")
    private Orders orders;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    private Integer quantity;
}

 

Orders와 Product를 다대다 관계로 매핑하기 위해 OrderProduct를 만들었고 

 

Orders 조회 DTO인 OrderResponseDto를 만들었다.

 

*  문제가 되었던 DTO

 

순환 참조를 피하도록 DTO를 만들 수 있는데, DTO에서 객체를 그대로 반환하게 만들어서 문제가 발생했다.

 

OrderProduct, Customer, Payment와 Orders가 서로 참조  하고 있기 때문에..

@Getter
public class OrdersResponseDto {
    private Integer quantity;
    private Integer totalPrice;
    private OrderStatus orderStatus;
    private List<OrderProduct> orderProducts = new ArrayList<>();
    private Customer customer;
    private Payment payment;
    private Shipment shipment;

    public OrdersResponseDto(Orders orders) {
        this.quantity = orders.getQuantity();
        this.totalPrice = orders.getTotalPrice();
        this.orderStatus = orders.getOrderStatus();
        this.orderProducts = orders.getOrderProducts();
        this.customer = orders.getCustomer();
        this.payment = orders.getPayment();
        this.shipment = orders.getShipment();
    }
}

 

JPA는 순환 참조를 해결하기 위한 여러가지 방법을 제공 하는데, 대표적인 4가지 방법이다

  • 단방향 관계 설정 : 한 엔티티만 다른 엔티티를 참조하도록 설계
  • FetchType 설정 : FetchType.LAZY를 적용하여 연관된 엔티티를 실제로 사용할 때 까지 로딩을 지연시키기
  • @JsonIgnore, @JsonMangedReference, @JsonBackReference 등의 어노테이션 사용 : JSON 직렬화 시 에 해당 필드를 무시하고나 참조를 관리
  • 직렬화 및 역직렬화 커스터마이징 : 필요에 따라 엔티티의 직렬화 및 역직렬화 과정을 직접 커스터 마이징하여 순환 참조를 피함
  • 순환 참조가 발생하지 않도록 별도의 DTO 사용

 

나는 그중 @JsonIgnore를 사용하여 JSON 직렬화 시 문제가 되는 필드를 무시하도록 수정 했다.

 

@Getter
public class OrdersResponseDto {
    private Integer quantity;
    private Integer totalPrice;
    private OrderStatus orderStatus;
    @JsonIgnore
    private List<OrderProduct> orderProducts = new ArrayList<>();
    @JsonIgnore
    private Customer customer;
    @JsonIgnore
    private Payment payment;
    private Shipment shipment;

    public OrdersResponseDto(Orders orders) {
        this.quantity = orders.getQuantity();
        this.totalPrice = orders.getTotalPrice();
        this.orderStatus = orders.getOrderStatus();
        this.orderProducts = orders.getOrderProducts();
        this.customer = orders.getCustomer();
        this.payment = orders.getPayment();
        this.shipment = orders.getShipment();
    }
}
반응형