InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException 문제

DongHyun Kim·2023년 7월 6일
0

백엔드

목록 보기
15/16

발생한 문제 🚩

대부분 영속성 컨텍스트에서 프록시 객체가 초기화되지 않았던 문제!!

필자의 경우 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 필드에 넣게되겠죠. 이런 단점 때문에, 비록 일반적인 흐름과는 맞지 않지만 단방향 연관관계를 사람들이 더 선호하는 이유가 되겠습니다.

profile
do programming yourself

0개의 댓글