[Error] 순환 참조 오류

김지현·2024년 1월 29일
0

Spring Boot 프로젝트

목록 보기
13/20
post-thumbnail

sports echo 프로젝트를 진행하며 남기는 오늘의 트러블 슈팅은 순환 참조 오류이다. 이것은 product와 product image 테이블을 분리하던 중에 발생하였다.

설계

우선 원래의 로직은 product 엔티티 내부에 product 이미지를 string으로 저장하였는데 이 경우 하나의 상품에 하나의 이미지밖에 올리지 못한다. 그래서 여러개의 이미지를 올릴 수 있도록 상품 이미지 테이블을 따로 생성하여 분리하기로 하였다.

다음은 수정한 엔티티의 구조이다.

// 기존 product 엔티티
public class Product extends TimeStamp {
	...
    @Column(name = "product_image", nullable = false)
    private String prodcutImage;
    ...
    }
    
// product image 엔티티 생성
public class Product extends TimeStamp {
	...
    @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private final List<ProductImage> productImageList = new ArrayList<>();
    ...
    }
    
public class ProductImage {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(name = "image_url", nullable = false)
    private String imageUrl;
}

product와 product image를 @OneToMany로 설정하고 product가 prodcutImage를 리스트로 가지도록 하였다.

문제

이슈는 product response dto에서 발생하였다. dto 내부에 prodcut image를 엔티티 그대로 실었는데 다음과 같은 오류와 마주했다.

public class ProductResponseDto {

    ...
    private List<ProdcutImage> imageUrlList;

}

ERROR 34636 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError)] with root cause

JSON으로 변환하려는 객체에서 무한 순환 참조가 발생하여 스택 오버플로우 오류가 발생했다. 엔티티와 dto 클래스에 양뱡향 참조가 있기 때문이다.

코드를 다시 살펴보자. Product response dto 내부에 ProductImage를 실었는데 product image 내부에도 @ManyToOne 으로 매핑한 product가 존재한다. 즉, product -> productImage -> product 의 무한 순환 참조가 발생하는 것이다.
이 모든 것은 response dto에 product image를 넣을 때 dto로 넣어주었어야 하는데 깜빡함 이슈.....로 발생하고 말았다.

해결

해결 방법은 두 가지이다.

  1. productImageDto를 생성하여 product responseDto에 넣어줄 것
  2. productImage의 필요한 정보들의만 responseDto에 넣어줄 것

나는 product Image의 imageUrl만 필요했기 때문에 2번 방법으로 변경하였다. 하여 productResponseDto를 다음과 같이 수정했다.

public class ProductResponseDto {
	...
    private List<String> imageUrlList;
}

mapper - AfterMapping

Product image 엔티티의 imageUrl들을 문자열 그대로 리스트로 저장하는 것이다. 우리 프로젝트는 mapstruct mapper를 사용하므로 해당 필드들을 매핑시켜주어야 한다. 다만 문자열들을 리스트로 만들어주어야 하므로 직접 메서드를 만들어 @AfterMapping 으로 매핑해주어야 한다.

@Mapper
public interface ProductMapper {
	...
   ProductResponseDto toResponseDto(Product product);

    @AfterMapping
    default void toImageUrlList(Product product,
        @MappingTarget ProductResponseDto.ProductResponseDtoBuilder dtoBuilder) {
        dtoBuilder.imageUrlList(
            product.getProductImageList().stream()
                .map(ProductImage::getImageUrl)
                .toList()
        );
    }
    }

위와 같이 toImageUrlList() 메서드를 생성하여 product의 prodcutImageList를 get한 뒤, product image 엔티티의 imageUrl을 get하여 리스트로 만들어 준다. @AfterMapping 애너테이션으로 responseDto의 imageUrlList로 매핑해준다.

결과

이렇게 수정하였더니 다음과 같이 잘 동작하는 것을 볼 수 있다!

0개의 댓글