발생한 문제 🚩
대부분 영속성 컨텍스트에서 프록시 객체가 초기화되지 않았던 문제!!
필자의 경우 RequestDTO 를 설계할 때 ManyToOne 관계를 가진 엔티티의 RequestDTO 를 함께 받도록 설계했는데, DTO 를 Entity 로 바꾸는 설계에서
연관관계를 가진 DTO 를 DB에서 꺼낸게 아닌 RequestDTO 를 자신만 Entity 로 바꾸고 FK로 이어진 부모 Entity 는 안 가져왔기 때문에 발생한 것이다.
문제의 세 엔티티는 RequestUserDTO <-> RequestScheduleDTO <-> RequestTodoDTO 가 [User] 1 <-> * [Schedule] 1 <-> * [Todo] 관계로 연결되어있습니다.
// ----------------------- User Entity
public class User {
@Id @GeneratedValue
private Long id;
private String username;
private String password;
private String email;
private String provider;
private String providerId;
@CreationTimestamp
private Timestamp createDate;
@Enumerated(EnumType.STRING)
@Builder.Default
private Role role = Role.USER;
@OneToMany(mappedBy = "user", cascade=CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<Schedule> scheduleList = new ArrayList<>();
public void updatePassword(String password){
this.password = password;
}
public void updateEmail(String email){
this.email = email;
}
public String getRoleKey(){
return this.role.getKey();
}
public void createScheduleList(List<Schedule> scheduleList) {
this.scheduleList = scheduleList;
}
public User(String username, String password){
this.username = username;
this.password = password;
}
}
// ----------------------- Schedule Entity
public class Schedule {
@Id @GeneratedValue
private long id;
@CreationTimestamp
private Timestamp createdDate;
@NonNull
private String title;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="user_id")
private User user;
@OneToMany(mappedBy="schedule", cascade=CascadeType.ALL, orphanRemoval = true)
private List<Todo> todoList;
@Builder.Default
private boolean isPublic = false;
}
// ----------------------- Todo Entity
public class Todo {
@Id @GeneratedValue
private Long id;
@CreationTimestamp
private Timestamp createdDate;
private String title;
private String content;
private boolean isFinished;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_id")
private Schedule schedule;
private String hyperLink;
}
(문제 이해를 돕기 위해 세 Entity 들을 첨부했습니다)
여기서 TodoReuqestDTO 에 ScheduleRequestDTO 를 가지고 있는 형태로 DTO 를 설계했었는데, TodoRequestDTO 를 Entity 로 바꿀 때 ScheduleRequestDTO 또한 schedule 로 바꿔서 Todo 에 전달해야 했습니다.
public class TodoRequestDto {
private String title;
private String content;
private boolean isFinished;
private ScheduleWithTodoRequest scheduleWithTodoRequest;
public Todo toEntity(){
Schedule schedule = scheduleWithTodoRequest.toEntity();
Todo todo = Todo.builder()
.content(content)
.title(title)
// InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException 발생!
// .toEntity() 로 생성된 Schedule 은 영속성 컨텍스트에서 꺼낸 객체가 아니기 때문이다.
// 해결 방법 -> DTO 에서 Entity 로 바꿀 때 연관관계에 있는 객체는 꼭 영속성 컨텍스트 에서 가져오도록 하자
.schedule(schedule)
.build();
return todo;
}
}
위에서 발생한 문제는 Schedule 을 Java 객체로만 바꿨을 뿐 RDB 에서 꺼내온 값이 아니기 때문에 프록시 객체인 상태입니다!
해결하고자 Schedule 을 영속성 컨텍스트에 넣었어도, 부모 엔티티인 User 또한 조회해야 했기 때문에 매우 번거롭습니다. ( @ManyToOne ( cascade = CascadeType.ALL ) 로 부모도 끌어올 순 있긴 합니다.)이런 경우 Schedule 을 무리하게 RDB 에서 조회하여 넣을 순 있겠지만, 계층별 역할이 망가진다 생각하여, 앞으로 RequestDTO 에서 부모나 자식 Entity 가 필요하다면 PK 로 받아서 다른 곳에서 조회하는 것이 좋겠다고 생각을 바꿨습니다.
위의 경우 private ScheduleWithTodoRequest scheduleWithTodoRequest;
부분을 Long scheduleId
로 바꾼 것이 되겠죠.
public class TodoRequestDto {
private String title;
private String content;
private boolean isFinished;
private Long scheduleId;
// scheduleId 를 이용해서 toEntity 를 호출한 메서드에서 Schedule 매개변수로 넣어주기
public Todo toEntity(Schedule schedule){
Todo todo = Todo.builder()
.content(content)
.title(title)
.schedule(schedule)
.build();
return todo;
}
✅여담으로, Schedule 만 필요한데 양방향 연관관계가 걸렸기 때문에 User 까지 조회하여 Schedule 의 User 필드에 넣게되겠죠. 이런 단점 때문에, 비록 일반적인 흐름과는 맞지 않지만 단방향 연관관계를 사람들이 더 선호하는 이유가 되겠습니다.