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
'Spring > Spring JPA' 카테고리의 다른 글
20201001 [jpa] JPA N+1 문제를 만들어보고 해결해보기. (수정 2020-10-05) (0) | 2020.10.01 |
---|---|
20190919 [jpa] 스프링 테스트 코드 상에서 DB 에 데이터 적재. (0) | 2019.09.19 |
20190710 [jpa] JPA 알아보기 03 : TestEntityManager 살펴보기 (수정 20190817) (0) | 2019.07.10 |
20190602 [jpa] JPA 알아보기 02 : JPA & Hibernate & Spring Data JPA (0) | 2019.06.02 |
20181209 [jpa] JPA 알아보기 01 (수정 20190601) (0) | 2018.12.09 |