JPA 엔티티, JSON 변환 에러

  • org.springframework.http.converter.HttpMessageNotWritableException
  • Could not write JSON
  • Infinite recursion (StackOverflowError)
  • nested exception is com.fasterxml.jackson.databind.JsonMappingException

 

원인

  • JPA 연관관계에서 양방향 매핑을 선언한 경우 발생
  • Jackson lib 의 ObjectMapper 객체에 의해 컨트롤러 단에서 JSON 타입을 변환하는 도중에 변환되는 엔티티의 필드가 다른 엔티티를 참조하고 그 엔티티 클래스의 필드가 또 다른 엔티티를 참조하고 ... 무한루프

 

문제코드

 

Album Entity

  • private List<AlbumComment> albumCommentList = new ArrayList<>();
@Entity
@Table(name = "ALBUM")
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
@ToString(exclude = {"albumPhotoList", "albumCommentList"})
public class Album extends TimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "TITLE")
    private String title;

    @Column(name = "CONTENT")
    private String content;

    @OneToMany(mappedBy = "album", fetch = FetchType.LAZY)
    private List<AlbumPhoto> albumPhotoList = new ArrayList<>();
    
    @OneToMany(mappedBy = "album", fetch = FetchType.LAZY)
    private List<AlbumComment> albumCommentList = new ArrayList<>();
    
    생략 ...
    
}

 

 AlbumComment Entity

  • private Album album
@Entity
@Table(name = "ALBUM_COMMENT")
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
@ToString(exclude = {"album"})
public class AlbumComment extends TimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "COMMENT")
    private String comment;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ALBUM_ID")
    private Album album;
    
    생략 ...
    
}

 

Album 엔티티와 AlbumComment 엔티티는 서로 양방향 관계이다. (@ManyToOne 과 @OneToMany)

컨트롤러에서는 아래와 같이 반환하고 있다.

 

AlbumCommentController

@CustomApiRestController
@Slf4j
@RequiredArgsConstructor
public class AlbumCommentController {

    private final AlbumService albumService;
    private final AlbumCommentService albumCommentService;

    @PostMapping("/album/{albumId}/comment")
    public ResponseEntity<ResponseAlbumCommentDto> createAlbumComment(@PathVariable Long albumId,
                                                                      @Valid @RequestBody RequestAlbumCommentDto requestAlbumCommentDto,
                                                                      BindingResult bindingResult) throws ResourceNotFoundException {

        Album album = findById(albumId);

        if (bindingResult.hasErrors()) {
            throw new InvalidRequestException(bindingResult);
        }

        ResponseAlbumCommentDto res = albumCommentService.createAlbumComment(album, requestAlbumCommentDto);

        return ResponseEntity.ok().body(res);
    }
    
    생략 ...
    
}

위와 같이 RestController 어노테이션을 붙인 컨트롤러에서 값을 반환하면 객체를 JSON 타입으로 ObjectMapper 가 변환시켜준다. 여기서 JSON 타입에 대한 무한루프문제가 발생하고 스택오버플로우가 뜬다. 이렇기 때문에 해당 문제를 해결하려면 양방향 매핑을 맺은 필드에 대해서 두 개의 어노테이션을 붙여야 한다.

 

  • @JsonManagedReference
    • 참조가 되는 앞부분을 의미하며, 정상적으로 직렬화를 수행한다.
    • Collection Type 에 적용된다.
  • @JsonBackReference
    • 참조가 되는 뒷부분을 의미하며, 직렬화를 수행하지 않는다.

컨트롤러 단의 코드를 보면 나는 AlbumComment 에 대한 Dto 클래스를 만들어서 반환하고 있다. 그렇다면 앨범의 댓글에는 앨범이 있는데(양방향이니깐) 앨범은 직렬화를 수행하고 앨범의 내용안에 있는 앨범 댓글은 직렬화를 수행하지 않을 것이다.

 

그렇다면 앞선 Album Entity 와 AlbumComment Entity 에 아래와 같은 내용을 붙여주어야 한다.

 

Album Entity

@JsonManagedReference
@OneToMany(mappedBy = "album", fetch = FetchType.LAZY)
private List<AlbumComment> albumCommentList = new ArrayList<>();

 

AlbumComment Entity

@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ALBUM_ID")
private Album album;

 

위와 같이 수행하면 정상적으로 문제를 해결할 수 있다. 추가적으로 아래의 링크를 참고하면 더 많은 내용을 확인할 수 있다.

 

reference

https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion

Posted by doubler
,