- 프로젝트를 진행하다가 연관관계가 있는 엔티티를 연쇄적으로 제거하기 위해 사용되는 두 가지 방식에 대해 알게되었고, 여기서 새롭게 알게 된 내용을 간단히 정리해보려고 한다.
주관적인 내용이 많이 포함되어 있을 수 있음
💡 CascadeType.REMOVE와 @OnDelete(action = OnDeleteAction.CASCADE)
- 둘의 가장 큰 차이점은 JPA에 의해 처리되느냐, DDL에 의해 DB 단에서 처리되느냐이다.
- 전자의 방식을 취할 경우, JPA에 의해 외래 키를 찾아가며 참조하는 레코드를 제거해주게 된다.
- 따라서, JPA 상에서는 참조하고 있는 레코드의 개수만큼 DELETE 쿼리가 생성된다.
- 후자의 방식을 취할 경우, 데이터베이스 자체에서
on delete cascade
제약조건이 걸리게 된다. 이를 통해 참조하는 레코드가 모두 제거되는 것이다. - 따라서, JPA 상에서는 한 개의 DELETE 쿼리가 생성되고, 데이터베이스에서 이를 처리해준다.
- 간단한 테스트로 확인해보자.
@Test
void deleteCascadeTest() {
// given
Category category1 = categoryRepository.save(createCategoryWithName("category1"));
Category category2 = categoryRepository.save(createCategory("category2", category1));
Category category3 = categoryRepository.save(createCategory("category3", category2));
Category category4 = categoryRepository.save(createCategoryWithName("category4"));
clear();
// when
categoryRepository.deleteById(category1.getId());
clear();
// then
List<Category> result = categoryRepository.findAll();
assertThat(result.size()).isEqualTo(1);
assertThat(result.get(0).getId()).isEqualTo(category4.getId());
}
- 우리는 위 테스트를 수행하면서 각 방식마다 쿼리가 어떻게 수행되는지 살펴볼 것이다.
- 1번 카테고리를 제거하면, 그의 모든 하위 카테고리인 2번과 3번 카테고리도 연쇄적으로 제거되어야 한다.
- 최종 결과는 1개의 카테고리만 조회될 것이다.
- 위에서 작성했던
Category
엔티티를, 다음과 같이 설정하여 테스트를 수행해보자.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List<Category> children;
- 테스트는 성공하였고, 쿼리 결과는 다음과 같다.
Hibernate: create table category (category_id bigint generated by default as identity, name varchar(30) not null, parent_id bigint, primary key (category_id))
...
Hibernate: alter table category add constraint FK2y94svpmqttx80mshyny85wqr foreign key (parent_id) references category
...
Hibernate: delete from category where category_id=?
Hibernate: delete from category where category_id=?
Hibernate: delete from category where category_id=?
category
테이블에 외래 키 제약 조건 외에는 별다른 제약 조건이 추가되지 않았다.- 상위 카테고리 한 개를 제거하기 위한 DELETE 쿼리와, 그의 하위 카테고리를 제거하기 위해 2개의 DELETE 쿼리가 연쇄적으로 수행된 것을 확인할 수 있다.
- 이번에는
Category
엔티티를 다음과 같이 설정하여 테스트를 수행해보겠다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Category parent;
@OneToMany
에 설정했던cascade=CascadeType.REMOVE
는 제거하고,@ManyToOne
에@OnDelete(action = onDeleteAction.CASCADE)
를 설정해주었다.- 테스트는 성공하였고, 쿼리 결과는 다음과 같다.
Hibernate: create table category (category_id bigint generated by default as identity, name varchar(30) not null, parent_id bigint, primary key (category_id))
...
Hibernate: alter table category add constraint FK2y94svpmqttx80mshyny85wqr foreign key (parent_id) references category on delete cascade
...
Hibernate: delete from category where category_id=?
- alter 문의 뒷 부분을 자세히 보면,
on delete cascade
가 추가되어 있는 것을 확인할 수 있다. - 즉, DDL에 의해 스키마 자체에 해당 제약 조건이 추가된 것이다.
- 또한, delete 쿼리는 한 건만 생성되었지만, 테스트는 성공하였다.
- 즉, JPA가 아닌, DDL로 제약 조건을 설정해둔 데이터베이스에서 참조하는 레코드들을 연쇄적으로 제거해준 것이다.
💡 둘 중 무엇을 선택해야 할까?
- 그래서 이 두 방식 중 어떤 방식을 언제 사용해야 할까?
성능 상의 차이
- 일단 필자가 현재 진행중인 프로젝트에서
@OnDelete
를 사용한 이유는 DELETE 쿼리를 줄이기 위해서이다. @OnDelete
방식에서 JPA는 단일한 DELETE 쿼리만 전송하여 참조하는 레코드들을 연쇄적으로 제거해준다.- 하지만
CascadeType.REMOVE
방식은 JPA에서 외래 키를 통해 참조하는 레코드들을 제거하기 위해 그 개수만큼의 DELETE 쿼리를 전송해야 한다. - 이러한 까닭에 쿼리를 1개만 생성하는
@OnDelete
방식이 성능적으로 이점이 있지 않을까 싶다.
운영 상의 차이
@OnDelete
방식으로 테이블을 생성하여 데이터베이스를 직접 다루다보면,on delete cascade
에 의해 어떠한 레코드의 참조 레코드까지 연쇄적으로 삭제해버리는 실수 할 여지가 있는 반면,CascadeType.REMOVE
방식으로 데이터베이스를 다루다보면, 의존성이 있는 레코드를 제거하기 위해서는 JPA에서 프로그램적으로만 처리할 수 있다보니, 운영자가 의존성이 있는 레코드를 직접 삭제하게 되는 실수의 여지가 비교적 줄어들 수 있다.- 하지만 반대로 생각해보면, 스키마 구조를 변경하는 등 데이터베이스를 조작하는데 번거로움이 생길 수도 있을 것 같다.
코드 상의 차이
Team
과Member
라는 1:N 관계의 엔티티가 있다고 가정해보자.Member
는 하나의Team
에 속할 수 있고,Team
에는 여러 명의Member
가 소속될 수 있다.- 연관관계의 주인은,
Team
의 primary key를 외래 키로 가지고 있는Member
가 될 것이다. @OnDelete
방식을 취하면,Member
에서@ManyToOne
으로 선언한Team
필드에 단순히 해당 어노테이션을 지정해주기만 하면 된다.- 물론
CascadeType.REMOVE
방식을 취해도,Team
에서@OneToMany
으로 선언한Member
필드에 해당 어노테이션을 지정해주기만 하면 된다는 것은 동일하다. - 하지만
@OneToMany
는 JPA로 인하여 만들어지는 양방향 의존 관계이다. - 실제 연관 관계의 주인은
Member
이기 때문에,Team
에서 반드시Member
를 의존하고 있을 이유가 없다. - 즉, JPA를 이용한 양방향 의존 관계를 만들어줘야할 특별한 이유가 없음에도, 단지 연쇄적으로 삭제하기 위해서 새로운 의존 관계를 만들어줘야하는 것이다.
- 양방향 의존 관계를 무분별하게 설정하면, 언제 어떠한 문제가 생길지 모른다.
- 예를 들어, 롬복으로
@ToString
이나@EqualsAndHashCode
등을 사용하는 경우 서로를 계속 참조하며 스택오버플로우가 발생할 수도 있다. - 새로운 의존 관계 생성을 방지하고, 그로 인해 발생할 수 있는 문제를 미연에 방지할 수 있다는 점에서는
@OnDelete
방식이 우위를 점한다고 생각한다. - 물론, 이것도 상황에 따라 다를 수 있다.
📌 Reference
'🥑 Web Technoloy' 카테고리의 다른 글
@OneToMany 단방향 (0) | 2023.07.18 |
---|---|
Vue.js & Spring Boot 연동 및 개발환경 구축 (1) | 2023.06.30 |
@RequestBody vs @ModelAttribute (0) | 2023.06.24 |
더티 체킹(Dirty Checking)이란? (0) | 2023.06.13 |
AJAX란? (0) | 2023.06.13 |