JPA 양방향 관계를 설정하고 테스트하다가 Controller 단에서 아래 예외가 발생하였다.
찾아보니 JSON 직렬화 하는 과정에서 무한 참조로인해 생긴 예외로 원인과 해결방안에 대해 정리해보려한다.
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
..
java.lang.StackOverflowError: null
.. ~[jackson-databind-2.15.3.jar:2.15.3]
JPA 양방향 순환 참조
어떤 상황이었는지 간단하게 시나리오를 구현해보자. User 와 Notice 엔티티가 있고 1:N 양방향 관계이다.
- User
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
@OneToMany(mappedBy = "users", fetch = FetchType.LAZY)
private List<Notice> notices = new ArrayList<>();
.. // Builder 등 생략
}
- Notice
public class Notice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@ManyToOne
@JoinColumn(name = "user_id")
private User users;
.. // Builder 등 생략
}
Service 와 Repository 를 테스트하면 원하는 시나리오를 얻을 수 없는데, 그 이유는 Controller 단 에서 Entity 를 response 값으로 응답할 때 JSON 직렬화하는 과정에서 문제가 발생하기 때문이다.
- Controller
@PostMapping("/notice/{userId}")
public ResponseEntity<?> saveNotice(@PathVariable("userId") Long id, @RequestParam Notice notice) {
User user = userServiceImpl.findByUserId(id);
Notice savedNotice = noticeServiceImpl.saveNotice(notice);
user.addNotice(savedNotice);
return ResponseEntity.ok().body(savedNotice);
}
JSON 직렬화 과정은 객체를 JSON 형식의 문자열로 변환하는 과정을 의미한다.
위의 User Entity 와 Notice Entity 가 서로를 참조하고 있는 상황에서 Notice Entity 를 JSON 직렬화 하게 되면 Notice Entity 에서 User Entity 를 참조하고 User Entity 에서 Notice Entity 를 참조하는 과정을 반복하게 거치게 된다. ( Notice -> User -> Notice -> User -> ... )
즉, 이들 간의 순환 참조가 발생하여 JSON 직렬화 과정에서 무한 루프로 빠지게 되는 것이다.
순환 참조 해결 방법
순환 참조를 해결하기 위한 방법은 아래와 같다.
1. DTO ( Data Transfer Object ) 사용하기
- 참조하는 객체가 아니라 필요한 데이터만 정의한 DTO 객체를 사용한다.
- Controller 단에서 Entity 가 아닌 DTO 를 응답함으로 순환 참조를 문제를 해결한다.
@Getter
public class NoticeDto {
private String title;
private String content;
private Long userId;
}
2. 직렬화 Annotation 사용
- @JsonManagedReference 와 @JsonBackReference 를 사용하여 참조하는 Entity 를 직렬화에서 제외한다.
- 주인 측에 @JsonManagedReference 를 사용하고, 반대 쪽에 @JsonBackReference 를 사용한다.
- 이를 적용하였을 때 User 객체를 직렬화할 때는 Notice 객체가 포함되지만, Notice 객체를 직렬화할 때는 User 객체가 포함되지 않는다.
public class Notice {
..
@JsonBackReference
@ManyToOne
@JoinColumn(name = "user_id")
private User users;
}
public class User {
..
@JsonManagedReference
@OneToMany(mappedBy = "users")
private List<Notice> notices = new ArrayList<>();
}
- 실행 결과
3. Entity 관계 재정의
- 양방향으로 설정되어 발생한 문제이기 때문에 단방향으로 재정의하여 문제를 해결할 수 있다.
정리
나는 DTO 를 사용하여 순환 참조 문제를 해결하였다.
예를 들어 회원 데이터와 다른 Entity 의 데이터를 같이 응답해야하는 경우나 Entity 에 응답에 필요하지 않은 필드가 많은 경우 비효율적이라는 생각이 들었다. 또한, 민감한 데이터를 숨기고 필요한 데이터만 전달할 수 있어 보안적인 측면에서도 이점이 있다.
참고
https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion
'Web' 카테고리의 다른 글
[Spring] Soft Delete with JPA (0) | 2024.01.14 |
---|---|
[Spring] QueryDsl 로 동적 쿼리 짜기 with JPA (0) | 2024.01.07 |
[Spring] 예외 코드 Enum 으로 관리하기 (0) | 2023.12.17 |
[Spring] actuator 를 통한 shutdown endpoint 생성 (0) | 2023.12.10 |
[Spring] RestTemplate 과 WebClient (0) | 2023.12.03 |