lv2. JPA Cascade
JPA의 관계 매핑과 Cascade(영속성 전이)에 대해 잘 알고 있다면 문제될 것이 없는 문제입니다.


1. JPA 관계 매핑

JPA에서 객체 간의 관계를 설정할 때는 @OneToMany, @ManyToOne, @OneToOne, @ManyToMany 등의 어노테이션을 사용합니다.

@OneToMany & @ManyToOne

@OneToMany: 하나의 Todo가 여러 개의 Manager를 가질 수 있음
@ManyToOne: 여러 개의 Manager가 하나의 Todo에 속함

// Manager.java (ManyToOne 관계)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "todo_id", nullable = false)
private Todo todo;
// Todo.java (OneToMany 관계)
@OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Manager> managers = new ArrayList<>();

Cascade(영속성 전이)

Cascade란?

JPA에서 부모 엔티티가 저장, 삭제될 때 자식 엔티티에도 같은 동작이 전파되도록 하는 기능입니다.
즉, Todo를 저장할 때 Manager도 자동으로 저장되게 만들 수 있습니다.

Cascade 옵션

orphanRemoval = true

orphanRemoval = true 설정을 추가하면 부모가 관계를 끊으면 자식 엔티티도 삭제됨
즉, Todo에서 managers 리스트에서 요소를 제거하면 자동으로 Manager 데이터도 삭제됨


연관관계 편의 메서드

  • Todo가 Manager를 생성할 때 자동으로 관계를 맺어주려면, 편의 메서드가 필요함
  • 이전 코드에서는 List에 new Manager(user, this)를 직접 추가했었다.
    하지만, JPA 연관관계를 위해 addManager() 메서드를 사용하면 더 깔끔하기 때문에 리팩토링 해줄 것입니다.
public void addManager(Manager manager) {
    managers.add(manager);
    manager.setTodo(this);
}

이런식으로 하면 더 좋다는 말 (메서드 확인하면 됩니다)


여기서 궁금해해야합니다.

왜 CascadeType.ALl로 해야하는지 ?

1. CascadeType.ALL을 사용하지 않으면 어떤 문제가 발생하는 것이지 ?

지금 Manager 엔티티를 Todo와 연관시키고 있는데, CascadeType.ALL을 설정하지 않으면 할 일을 저장할 때 Manager도 함께 저장되지 않습니다.

@OneToMany(mappedBy = "todo", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Manager> managers = new ArrayList<>();
  • cascade = CascadeType.ALL을 사용하면 Todo가 저장될 때, Manager도 자동으로 저장됩니다.
  • 만약 CascadeType.ALL을 사용하지 않는다면 Manager를 수동으로 따로 저장해야 하는 번거로움이 생기게 됩니다,.

2. CascadeType.ALL의 역할

CascadeType.ALL을 설정하면 부모(Todo)에 대한 모든 영속성 변화가 자식(Manager)에게 전파됩니다.


지금 보면 persist와 remove만 사용해도 가능하지만, 앞으로 todo를 더 수정하게 되거나, 수정할 계획이 있따면(가능성) 일괄적으로 ALL 설정하는 것이 제일 안전한 방법입니다. (나머지는 현재 요구 사항에서 필요 x)

쉽게 예시로 설명하면,

Todo todo = new Todo("지금은 Spring 공부중", "JPA 연관관계 학습", "맑음", user);
todoRepository.save(todo);  // 자동으로 Manager도 함께 저장

todoRepository.delete(todo);  // 자동으로 Manager도 함께 삭제

이렇게 todo 저장할 때 manager도 같이 저장되고, todo삭제시에 manager도 같이 자동으로 삭제돠ㅣㅂ니다.

orphanRemoval

둘은 서로 다른 역할을 하고, orphanRemoval = true만 설정한다고 해서 Manager가 자동 저장되지 않습니다. 따라서, 자동으로 할 일을 만든 사용자가 담당자로 등록 되도록 하려면 ALL 옵션이 반드시 필요합니다.
아래 표 참고하시면 됩니다.


코드를 정독하지 않고 봤을 때엔,
서비스에서 매니저를 건들지도 않은데 persist로 걸면 jpa랑 자기 리스트에 그 한마디로 양방향을 걸면 원래 꺼가 있고 그거에 대한 리스트에 양방향이 있고 양방향에 객체를 넣고 세이브를 하면 그거를 같이 저장안해도 알아서 jpa가 저장함. 이거를 생성자에서 해주고 있음.
new manage생성자에서 todo가 있고 유저가 있음. 일정에 대해서 관리자가 여러명일 수 있음.
여행을 가면 여행 일정에 대해서 관리자가 여려명일 수 있음. 이거를 자동으로.
생성자도 유저인데 생성자인 사람, 생성 한 사람도 관리할 수 있잖아.
그래서 생성자에서 비슷한 유저끼리 해주고, 해당유저를 매니저에다 추가해준거다.
끄러면 jpa에서 알아서 저장을 매니저에 해줌. 이게 양방향.

그래서 생성자때문에 cascade = CascadeType.PERSIST 하면 저장이 된다.
라는 생각의 전환(?)이 되었었습니다.


설명을 덧 붙여서 이해를 하자면,

JPA에서 cascade = CascadeType.PERSIST가 왜 저장을 도와주는가?

  • 양방향 연관관계 (@OneToMany, @ManyToOne)가 걸려 있으면, 부모 엔티티의 List<자식> 필드에 데이터를 추가하는 순간 JPA가 이를 인식함.
  • 부모 엔티티(Task)가 persist 될 때, 자식 엔티티(Assignee)도 자동으로 persist 됨.
  • 생성자에서 자식을 추가하면, 해당 부모 객체의 List<자식> 필드가 업데이트되며, JPA는 이를 자동으로 감지하여 저장을 수행함.

콰드로 예시를 든다면?

Task (부모) 엔티티

@Entity
public class Task {

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

    @ManyToOne
    private User creator; // 생성한 유저

    @OneToMany(mappedBy = "task", cascade = CascadeType.PERSIST)
    private List<Assignee> assignees = new ArrayList<>();

    //생성자에서 creator를 Assignee로 자동 추가
    public Task(String title, User creator) {
        this.title = title;
        this.creator = creator;
        this.addAssignee(new Assignee(this, creator)); //creator를 자동으로 Assignee로 등록
    }

    public void addAssignee(Assignee assignee) {
        assignees.add(assignee);
        assignee.setTask(this);  // 양방향 관계 유지
    }
}

Task가 생성될 때 생성한 유저(creator)가 자동으로 Assignee로 등록됨.
cascade = CascadeType.PERSIST 덕분에, Task 저장 시 Assignee도 자동 저장됨.

Assignee (자식) 엔티티

@Entity
public class Assignee {

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

    @ManyToOne
    private Task task;

    @ManyToOne
    private User user;

    public Assignee(Task task, User user) {
        this.task = task;
        this.user = user;
    }

    public void setTask(Task task) {
        this.task = task;
    }
}

Assignee는 어떤 Task와 어떤 User가 연결되는지를 저장하는 엔티티.

Service 코드 실행

public Task createTask(String title, User creator) {
    Task task = new Task(title, creator); // 생성자에서 Assignee 자동 추가
    return taskRepository.save(task); // Task 저장 시 Assignee도 자동 저장됨
}
  • Task를 생성하면, 생성자에서 Assignee를 자동으로 추가함.
  • JPA가 Task의 List<Assignee>를 감지하고, cascade = CascadeType.PERSIST 덕분에 Assignee도 자동 저장됨.

결론

"왜 생성자 때문에 cascade = CascadeType.PERSIST 하면 저장이 되는거지 ? ㅇㅂㅇ?"

  1. new Task(title, creator)를 호출하면, 생성자에서 new Assignee(this, creator)를 자동으로 추가.
  2. 이 Assignee는 Task의 assignees 리스트에 추가됨.
  3. taskRepository.save(task); 호출 시, JPA는 Task의 @OneToMany 관계를 확인.
  4. cascade = CascadeType.PERSIST이므로, 연관된 Assignee도 함께 persist()됨.
  5. 결과적으로 Task만 save() 했는데, Assignee도 자동 저장되는 것임.

정리

  • 양방향 연관관계에서, cascade = CascadeType.PERSIST를 설정하면 부모 엔티티가 저장될 때 자식도 함께 저장된다.
  • 생성자에서 Assignee를 Task의 리스트에 추가하면, JPA가 이를 감지하고 자동으로 저장해준다.
  • 즉, save()에서 Assignee를 명시적으로 저장하지 않아도 되는 이유가 이것때문이었던 것!!!!
profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN