본문 바로가기
Framework and Tool/JPA

JPA - 영속성 전이와 고아 객체

by ocwokocw 2021. 7. 17.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 영속성 전이: CASCADE

여태까지 예제를 수행할 때 부모 와 자식 엔티티 연관에서 한 가지 불편한 점이 있었을것이다. Parent 가 Child 엔티티를 참조하는 상황에서 Parent 와 Child 가 모두 영속된 상태여야 연관관계가 맵핑됐었다.

 

JPA 에서는 특정 엔티티를 영속 상태로 만들 때 CASCADE 옵션으로 연관된 엔티티도 함께 영속 상태로 만들어주는 영속성 전이 기능을 제공한다.

 

@Entity
public class Parent{

	@Id @GeneratedValue
	@Column(name = "PARENT_ID")
	private Long id;
	
	private String name;
	
	@OneToMany(mappedBy = "parent")
	private List<Child> children = new ArrayList<>();

................

@Entity
public class Child{

	@Id @GeneratedValue
	@Column(name = "CHILD_ID")
	private Long id;
	
	private String name;
	
	@ManyToOne
	@JoinColumn(name = "PARENT_ID", referencedColumnName = "PARENT_ID")
	private Parent parent;

	public void setParent(Parent parent) {
		
		if(this.parent != null) {
			this.parent.getChildren().remove(this);
		}
		
		this.parent = parent;
		
		parent.getChildren().add(this);
	}

 

위와 같이 1 : N 의 관계를 가지는 Parent 와 Child 엔티티를 작성해주고 다중성이 N 인 Child 엔티티에 연관관계 편의 메소드를 작성해준다. 그리고 Child 엔티티 2 개를 Parent 1 개와 연관시키려면 아래와 같이 코드를 작성한다.

 

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Parent parent = new Parent();
	
	parent.setName("Parent#1");
	em.persist(parent);
	
	Child child = new Child();
	
	child.setName("Child#1");
	child.setParent(parent);
	em.persist(child);
	
	Child child2 = new Child();
	
	child2.setName("Child#2");
	child2.setParent(parent);
	em.persist(child2);
	
	tx.commit();
	em.close();
}

 

JPA 는 엔티티 저장시 연관 엔티티가 모두 영속상태 여야 하므로, Parent 를 먼저 영속 상태로 만들어 주고나서 연관관계를 설정해준다.


- 영속성 전이: 저장

영속성 전이 기능을 활용해보자. Parent 엔티티의 children 연관에서 @OneToMany 의 cascade 속성을 PERSIST 로 지정해준다.

 

@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();

 

아래와 같이 테스트 코드에서도 child 를 영속시켰던 부분을 제거하고, 연관관계를 모두 설정한 뒤 마지막에 1 번만 Parent 엔티티를 영속한다.

 

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Parent parent = new Parent();
	
	parent.setName("Parent#1");
	
	Child child = new Child();
	
	child.setName("Child#1");
	child.setParent(parent);
	
	Child child2 = new Child();
	
	child2.setName("Child#2");
	child2.setParent(parent);
	
	em.persist(parent);
	
	tx.commit();
	em.close();
}

 

DB 에 잘 저장되었는지 확인도 잊지 말고 해주자.

영속성 전이 사용시 주의할 점이 있는데 영속성 전이는 연관관계 맵핑과는 아무런 상관이 없다. 단지 엔티티를 영속화하는 편리함만 제공해줄뿐이다. 예제를 통해 자세히 알아보자.

 

public void setParent(Parent parent) {
	this.parent = parent;
}

............

public static void save(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	Parent parent = new Parent();
	
	parent.setName("Parent#1");
	
	Child child = new Child();
	
	child.setName("Child#1");
	parent.getChildren().add(child);
	
	Child child2 = new Child();
	
	child2.setName("Child#2");
	parent.getChildren().add(child2);
	
	em.persist(parent);
	
	System.out.println("Child's parent: " + child.getParent());
	System.out.println("Child2's parent: " + child2.getParent());
	
	tx.commit();
	em.close();
}

 

save 메소드를 parent 엔티티에서 자식을 추가하도록 코드를 변경해보자. 그리고 Parent 에 CASCADE 를 설정했으니 알아서 연관관계 맵핑도 하겠지 라는 기대를 품고 실행해보면 아래와 같이 데이터가 저장된다.

CHILD 테이블에서 PARENT_ID 가 null 값이다. 즉 CASCADE 옵션은 em.persist 메소드를 자동으로 호출해주는것이지 연관관계 맵핑을 자동으로 해주는것이 아니다. 이를 꼭 명심하고 연관관계 맵핑을 잘 해주도록 하자.


- 영속성 전이: 삭제

CASCADE 가 저장에만 적용되는것은 아니다. CASCADE 를 사용하지 않으면 앞에서 저장한 child 와 parent 를 삭제하려면 아래와 같이 3 개의 엔티티를 모두 삭제해줘야 한다.

 

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Child findChild1 = em.find(Child.class, 2L);
	Child findChild2 = em.find(Child.class, 3L);
	
	Parent parent = em.find(Parent.class, 1L);

	em.remove(findChild1);
	em.remove(findChild2);
	em.remove(parent);
	
	tx.commit();
	em.close();
}

 

아래와 같이 Parent 엔티티의 children 필드에 CASCADE REMOVE 속성을 추가해주고 parent 엔티티만 삭제해보자.

 

@OneToMany(mappedBy = "parent"
	, cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<Child> children = new ArrayList<>();

..........

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

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

	em.remove(parent);
	
	tx.commit();
	em.close();
}

 

그리고 수행된 SQL 을 보면 Parent 만 삭제했음에도 불구하고 Child 까지 삭제되는것은 확인할 수 있다.

 

Hibernate: 
    /* delete com.example.demo.test1.Child */ delete 
        from
            child 
        where
            child_id=?
Hibernate: 
    /* delete com.example.demo.test1.Child */ delete 
        from
            child 
        where
            child_id=?
Hibernate: 
    /* delete com.example.demo.test1.Parent */ delete 
        from
            parent 
        where
            parent_id=?

 

CASCADE 는 PERSIST 와 REMOVE 뿐만 아니라 다양한 옵션을 가지고 있다. CASCADE 의 적용시점은 remove 나 persist 를 실행할 때 바로 수행되지 않고 플러시를 호출할 때 전이가 발생한다.


- 고아 객체

JPA 는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공해준다. 이를 고아 객체(ORPHAN) 제거라고 한다. 이 기능을 이용하면 Parent 엔티티에 Child 엔티티 참조만 제거하면 Child 엔티티가 자동으로 삭제되게 할 수 있다.

 

@OneToMany(mappedBy = "parent"
	, cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
	, orphanRemoval = true)
private List<Child> children = new ArrayList<>();

 

이 기능을 사용하려면 orphanRemoval 속성을 true 로 설정해주면 된다.

 

public static void find(EntityManager em) {
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();

	Parent parent = em.find(Parent.class, 1L);
	parent.getChildren().remove(0);
	
	tx.commit();
	em.close();
}

 

위와 같이 Parent 에서 첫번째 child 에 대한 참조만 삭제하고 엔티티매니저의 remove 를 호출하지 않았는데도 아래 SQL 이 수행된다.

 

Hibernate: 
    /* delete com.example.demo.test1.Child */ delete 
        from
            child 
        where
            child_id=?

 

만약 Collection 의 clear 메소드를 호출하면 모든 자식 엔티티를 제거한다.

 

고아 객체 제거는 마치 가비지컬렉션 처럼 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 즉 한곳에서 참조하는 @OneToMany 나 @OneToOne 일때에만 사용해야 한다. 그래서 해당 다중성에만 속성을 제공한다.

 

고아 객체 제거를 이용하면 부모가 삭제될 때 자식을 모두 삭제 한다. 실제로 CASCADE 의 REMOVE 옵션을 없애도 Parent 엔티티를 삭제해면 아래처럼 자식을 삭제하는 SQL 이 동작하는것을 확인할 수 있다.

 

Hibernate: 
    /* delete com.example.demo.test1.Child */ delete 
        from
            child 
        where
            child_id=?
Hibernate: 
    /* delete com.example.demo.test1.Child */ delete 
        from
            child 
        where
            child_id=?
Hibernate: 
    /* delete com.example.demo.test1.Parent */ delete 
        from
            parent 
        where
            parent_id=?

- 영속성 전이와 고아 객체

JPA 는 엔티티 매니저의 persist 와 remove 메소드를 통해서 영속성을 관리한다. 그래서 부모 엔티티에 CASCADE ALL 과 고아 객체 제거를 true 로 설정하면 이는 곧 자식의 생명주기를 관리한다는 얘기가 된다.

'Framework and Tool > JPA' 카테고리의 다른 글

JPA - 값 타입 - 임베디드 타입  (0) 2021.07.20
JPA - 고급맵핑 - 요구사항 분석과 맵핑4  (0) 2021.07.18
JPA - 지연 로딩  (0) 2021.07.16
JPA - 즉시 로딩과 지연 로딩  (0) 2021.07.15
JPA - 프록시  (0) 2021.07.14

댓글