Google Developer Student Clubs 1기/GDSC 백엔드 스터디

[백엔드 스터디] 7, 8주차 - JPA 연관관계 및 영속성 전이

Razelo 2023. 2. 15. 17:59

이번주 주제는 JPA 연관관계 매핑과 영속성 전이입니다. 

 

주제가 비교적 큰 범위로 선정되어 이번 7, 8주차를 기준으로 마무리된다. 


JPA에서는 아래 세 가지가 중요하다. 

 

방향: 단방향, 양방향 (객체 참조)

연관 관계의 주인: 양방향일 때, 연관 관계에서의 관리의 주체 

다중성: 다대일(N:1), 일대다(1:N), 다대다(N:N)

 

데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능함. 그러나 객체의 세계는 참조용 필드가 있는 객체만 다른 객체를 참조 가능함. 

기본적으로 단방향 매핑으로 하고 나중에 꼭 필요할 때만 양방향을 추가해주면 된다. 

 

연관 관계의 주인을 정하는 건 무엇일까?

두 단방향 관계 즉 (a -> b, b -> a) 에 대해서 제어의 권한 즉 외래 키를 비롯한 테이블 레코드를 저장, 수정, 삭제 처리를 할 수 있는 권한을 갖는 실질적인 관계가 무엇인 지 정하는 것이다. 

 

연관 관계의 주인이 아니면 조회만 가능하다. 

연관관계의 주인이 아닌 객체에서 mappedBy 속성을 사용해서 주인을 지정해줘야한다. 

외래 키가 있는 곳을 연관 관계의 주인으로 정하면 된다. 


게시판과 게시글의 예시로 이해하면 편하다. 

 

일대다 단방향 연관 관계 매핑이 필요한 경우에는 그냥 다대일 양방향 연관 관계를 매핑해주는 게 추후에 유지보수하기 좋다. 

일대다 양방향은 실무에서는 사용 금지다. 

 

다대다는 실무에서 쓰지 말자.

중간 테이블이 숨겨져 있어서 나도 모르는 사이 복잡한 조인 쿼리가 날라갈 수 있다. 

다대다로 자동생성된 중간 테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제될 확률이 높다. 

 

 

영속성 전이란 뭘까? 

부모 엔티티를 영속성 상태로 변경하면 자식도 함께 변경될 수 있도록 하는 기능을 영속성 전이 즉 CASCADE라고 부른다. 

 

고아 객체란 뭘까?

부모 엔티티와 관계가 끊어진 자식 엔티티를 고아객체라고 한다. orphanRemoval = true 옵션을 설정해주면 이런 고아객체를 자동으로 삭제할 수 있다. 

 

만약 회사내에 팀이 있고 팀 안에 구성원이 있다면 팀과 멤버는 일대다 관계를 갖는다. 멤버와 팀은 다대일 관계를 갖는다. 

이때 팀과 멤버는 양방향으로 연결된 것이 아니라 단뱡향으로 서로 연결되어있다고 보면 된다. 

 

다대일 관계에서 '다'를 갖는 Member가 외래키 소유가 된다. 

@Entity
public class Member {
  @ManyToOne  //매핑 설정
  @JoinColumn(name = "team_id", nullable = false) //외래키 이름 지정
  private Team team;
}

@JoinColumn을 사용하면 member 테이블에 team_id 컬럼이 생긴다. 근데 이걸 안해주면 jpa 에서 중간 테이블을 만들어버린다. 

 

mappedBy 를 설정하면 양방향 관계를 가질 수 있다. 아래의 경우 Member가 연관 관계의 주인이 되는 셈이다. 이러면 Team은 반드시 Member에 요청을 보내야 외래키 수정/ 삭제가 가능하다. 

@Entity
public class Team {
  @OneToMany(mappedBy = "team")
  @JoinColumn(name = "team_id")
  private List<Member> members = new ArrayList<>();
}

 

다대다 관계 매핑

이건 그냥 안쓰는게 제일 좋다. 

@Entity
public class Member {
  @ManyToMany
  @JoinTable(
    name = "member_product",
    joinColumns = @JoinColumn(name = "member_id"),
    inverseJoinColumns = @JoinColumn(name = "product_id"))
  private Set<Product> products;
}

@Entity
public class Product {
  @ManyToMany(mappedBy = "products")
  private List<Member> members;
}

위처럼 @ManyToMany를 사용하는 경우 중간 테이블에 외래키와 함께 다른 데이터를 넣고 싶을 수도 있는데 이 경우 중간 테이블에는 외래키를 제외한 다른 데이터는 넣지 못한다고 한다. 

 

일대일 관계 매핑

주 테이블과 대상 테이블 중 어느 곳에 외래 키를 둘 지 정하면 된다. 

@Entity
public class Company {
  @OneToOne(mappedBy = "company")
  private Member member;
}

 

 

 

영속성 전이

CASCADE옵션으로 제공된다. 

 

영속성 전이는 연관관계 매핑과 아무런  관련없다.

단지 엔티티를 영속화할 떄 연관된 엔티티도 함께 영속화하는 편리함을 제공함

양방향 연관관계를 꼭 설정해주고 영속 상태로  만들어야함. 

 

아래 예시를 보자. 

@Setter
@Getter
@Entity
public class Parent {

    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();
}
@Getter
@Setter
@Entity
public class Child {

    @Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Parent parent;
}

영속성 전이가 없다면 아래처럼 저장해야 한다. 

Parent parent = new Parent();
em.persist(parent);//부모 영속화



Child child1 = new Child();

child1.setParent(parent);
parent.getChildList().add(child1);
em.persist(child1);//자식 1 영속화



Child child2 = new Child();

child2.setParent(parent);
parent.getChildList().add(child2);
em.persist(child2);//자식 2 영속화

 

jpa에서 엔티티를 저장할 때 연관된 엔티티가 모두 영속 상태여야 한다. 그래서 위 코드를 보면 꽤 번거롭다. 부모도 영속 상태로 만들어야하고 자식도 영속 상태로 만들어야한다. 

 

그런데 영속성 전이를 사용한 케이스를 살펴보자. 

@Setter
@Getter
@Entity
public class Parent {

    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> childList = new ArrayList<>();
}
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();

child1.setParent(parent);  // 생략되면 저장은 되지만 child의 FK에 null이 저장된다.
child2.setParent(parent);

parent.getChildList().add(child1);  // 생략되면 저장되지 않는다.
parent.getChildList().add(child2);

em.persist(parent);

위 코드를 보면 parent만 persist해줘도 된다. 

 

 

영속성 전이 -> 삭제 (REMOVE)

부모와 자식을 제거하려면 다음과 같이 해야한다. 

Parent fParent = em.find(Parent.class, 1L);
Child fChild1 = em.find(Child.class, 2L);
Child fChild2 = em.find(Child.class, 3L);

em.remove(fChild1);
em.remove(fChild2);
em.remove(fParent);

그런데 영속성 전이에서 REMOVE 를 사용한다면 ?

@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)

아래와 같이 한번에 자식 엔티티까지 모두 지울 수 있다. 

Parent fParent = em.find(Parent.class, 1L);

em.remove(fParent);

 

이때 영속성 전이가 설정되어있지 않으면 외래키 무결성 제약조건 위반 오류가 발생한다고 한다. 

 

ALL, PERSIST, MERGE, REMOVE, REFRESH라는 영속성 전이가 있다. DETACH도 2.0이후로 추가되었다. 

 

PERSIST와 REMOVE는 em.persist()나 em.remove가 아니라 플러시를 호출할 떄 발생한다고 한다. 

그런데 예외는 @GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하면 save() or persist()하는 시점에 flush()가 자동호출되기 때문에 이건 바로 영속성 전이가 발생한다. 

 

영속성 전이는 관리하는 부모가 단 하나일 경우 쓰는 게 좋다. 

 

 

고아 객체?

JPA에서 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다고 한다. 이게 고아 객체 제거라고 부른다. 이걸 사용하면 부모 엔티티 컬렌션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다고 한다. 

@Setter
@Getter
@Entity
public class Parent {

    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();
}
Parent parent = new Parent();
em.persist(parent);

Child child1 = new Child();
child1.setParent(parent);
parent.getChildList().add(child1);


Child child2 = new Child();
child2.setParent(parent);
parent.getChildList().add(child2);
em.flush();
em.clear();

Parent fParent = em.find(Parent.class, 1L);

fParent.getChildList().remove(0);

위 코드를 실행하면 delete 쿼리가 날라간다고 한다. 

 

 

고아 객체 제거시 주의

고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않아서 고아 객체로 보고 삭제하는 기능이다.

그래서 참고하는 곳이 하나일 떄 사용해야 한다. 

특정 엔티티가 개인소유하는 엔티티에만 이 기능을 적용하는 게 맞다. 

그래서 orphanRemoval은 @OneToOne과 @OneToMany에 사용하는 게 맞다고 한다. 

 

부모를 제거하면 자식은 고아가 된다. 그래서 고아 객체 제거 기능을 쓰면 부모를 제거할 때 자식도 함께 제거된다. 즉

CascadeType.REMOVE처럼 동작한다고 한다. 

 

CascadeType.ALl + orphanRemoval = true

영속성 전이와 고아 객체 제거를 함께 쓸 수도 있다. 

보통 엔티티는 em.persist()로 본인을 영속화하고 em.remove()를 통해 본인을 제거한다. 즉 엔티티 스스로가 본인의 생명 주기를 관리한다. 

하지만 이 두 옵션을 사용하면 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다. 

자식을 저장하려면 부모에 등록하면 되고 자식을 삭제하려면 부모에서 제거만 하면 된다. 


 

아래 링크에서 많은 도움을 받았습니다. 

 

감사합니다. 

 

https://jeong-pro.tistory.com/231

https://www.youtube.com/watch?v=7JAoNNhvsjw 

https://ttl-blog.tistory.com/139

 

 

 

 

 

 

반응형