본문 바로가기

📝 ErrorNote

[ SpringBoot / JPA ] 무한 참조 문제 해결 : :java.lang.IllegalStateException: Cannot call sendError() after the response has been committed

에러 내용 

  • java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
  • Failure while trying to resolve exception [org.springframework.http.converter.HttpMessageNotWritableException]

 

이미지 별로 좋아요 정보를 가져오는 로직에서 사용자가 올린 게시물에 좋아요를 누르지 않았을 때는 무한 참조가 발생하지 않았었는데 

1번 사용자가 2번 사용자의 이미지 게시물을 좋아요 누르자 마자 새로 로딩하면 무한참조가 발생하는 문제가 발생했다. 

 

이를 해결하기 위해 postman으로 데이터를 찍어보았다. 

 

다음과 같이 좋아요 정보인 likes 배열이 비어있을 때는 에러가 발생하지 않는다. 

하지만 다른 사용자가 해당 게시물에 좋아요를 누르고, 다시 이미지들을 불러오는 쿼리를 날려보면 

 

 

다음과 같이 데이터 포맷이 모두 깨져서 나오는 것을 발견할 수 있다. 

 

원인

    @GetMapping("/api/image")
    public ResponseEntity<?> imageStory(@AuthenticationPrincipal PrincipalDetails principalDetails,
                                        @PageableDefault(size = 3) Pageable pageable) {

        Page<Image> images = imageService.imageStory(principalDetails.getUser().getId(), pageable);
        return new ResponseEntity<>(new CMRespDto<>(1, "이미지 스토리 불러오기 성공", images), HttpStatus.OK);
    }
  • 컨트롤러에서 데이터를 리턴할 때 images entity 를 내보냈는데 이때 연관관계가 엉켜서 서로 무한 참조가 발생했다. 
  • 엔티티 자체를 리턴하게 되면서 entity 에 있는 모든 필드에 대해서 getter 가 호출된다고 한다. 그래서 getter 가 호출되고, 이를 json 형식으로 파싱하기 위한 HttpMessageConvertor 가 작동을 하는데 이때 무한 참조로 인해서 파싱하지 못하는 문제가 발생하는 것이다. 
  • [ 새롭게 알게된 사실 참고하기 ]
public class Image {
	```(생략)```
    
    @JsonIgnoreProperties({"images"}) // user 정보가 갖고 있는 images 는 가져올 필요가 없음을 의미
    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.EAGER)  // image 를 select 하면 user 정보를 같이 들고옴.
    private User user; // 연관관계의 경우 FK가 데이터 베이스에 저장됨.

    // 이미지 좋아요
    @JsonIgnoreProperties({"image"}) // image 를 리턴할 때 likes 를 리턴하게 되는데 그 때 likes 에 있는 image 리턴 막기
    @OneToMany(mappedBy = "image")
    private List<Likes> likes;
	
	```(생략)```
}

 

이미지 엔티티를 보면

  • 이미지를 올린 사람을 기록하기 위해 User 엔티티와 ManyToOne 관계이다. 
  • 이미지에 좋아요를 파악하기 위해 Likes 엔티티와 OneToMany 관계이다. 
  • 하지만 이때, user, likes 를 가져올 때 user가 갖고 있는 images 와 likes 가 갖고 있는 image는 갖고 오지 않도록 @JsonIgnoreProperties 를 붙여주었다. 따라서 여기서 문제가 생긴 것은 아님. 
  • @JsonIgnoreProperties : 직렬화 대상에서 제외하겠다는 의미의 어노테이션 

유저 엔티티를 보면 

public class User {

	``` (생략) ```

    // 연관관계의 주인이 아니므로 테이블에 컬럼 생성하지 않도록 설정
    // User 를 Select 할 때 해당 User id 로 등록된 image 모두 들고오도록
    // Lazy = User 를 Select 할 때 해당 User id 로 등록된 image 가져오지 않도록 - 대신 getImages()함수의 image 들이 호출될 때 가져옴,
    // Eager = User 를 Select 할 때 해당 User id로 등록된 image 들을 전부 Join 해서 가져오도록
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @JsonIgnoreProperties({"user"}) // image 내부에 있는 user 를 무시하고 파싱하도록 설정
    private List<Image> images;  // 양방향 매핑 컬럼
	
    ``` (생략) ```


}

 

  • 사용자가 올린 이미지를 불러오기 위해 Image 엔티티와 OneToMany 관계이다. 
  • 여기서도 images 를 가져올 대  image가 갖고 있는 user 의 정보를 갖고오지 않도록 설정해주었다. 

 

public class Likes {  // N , N

	``` (생략) ``` 
    
    @JoinColumn(name = "imageId")
    @ManyToOne
    private Image image; // 1

    @JoinColumn(name = "userId")
    @ManyToOne
    private User user;  // 1

	``` (생략) ``` 
}

 

문제는 여기서 발생했다. 

  • Image 에서 likes 정보를 가져올 때 oneToMany 관계인 likes 를 살펴보면 likes 에서도 user 의 정보를 가져온다.  user 의 정보를 가져올 때 또 이 User 와 oneToMany 관계인 images 를 불러오기 때문에 이때 무한참조 문제가 발생하는 것이다. 
  • 따라서 여기에도 @JsonIgnoreProperties 를 붙여서 user 가 갖고 있는 image 정보를 가져올 필요가 없다는 것을 명시해준다. 

 

해결방법

public class Likes {  // N , N

    @JoinColumn(name = "imageId")
    @ManyToOne
    private Image image; // 1


    @JsonIgnoreProperties({"images"}) // 추가
    @JoinColumn(name = "userId")
    @ManyToOne
    private User user;  // 1

}

 

이렇게 하면 문제가 해결된다. 

 

likes 를 출력할 때 images 를 무시하도록 하자

images 의 데이터가 출력되지 않고, 무한 참조 문제도 해결되었다. 

 

[✏️ 새롭게 알게 된 사실]

JPA 에서는 지연로딩이 기본 설정이기 때문에, 연관된 엔티티를 조회할 때 프록시 객체가 반환된다. 이 프록시 객체는 실제 엔티티가 필요한 시점에 초기화가 되는데 직렬화 작업을 수행하면서 엔티티의 getter 메서드가 호출되어 프록시 객체를 초기화하려고 시도한다. 

 

프록시 객체를 초기화하려고 시도하는 과정에서 무한 참조가 발생할 수 있다.