TIL: JPA Cascade와 데이터베이스 ON DELETE CASCADE의 차이점
개념적 이해
JPA에서 cascade
옵션과 데이터베이스의 ON DELETE CASCADE
는 비슷한 목적을 가지고 있지만, 작동하는 레벨과 방식이 완전히 다릅니다. 이러한 차이점으로 인해 개발자가 예상하지 못한 동작이 발생할 수 있습니다.
JPA Cascade (Java 애플리케이션 레벨)
JPA의 cascade는 Java 애플리케이션 내에서 엔티티 간의 연산을 전파하는 메커니즘입니다:
- 작동 위치: 메모리 내 영속성 컨텍스트(Persistence Context)
- 영향 범위: JPA가 관리하는 엔티티 객체에만 적용
- 주요 목적: 엔티티 간의 작업(저장, 업데이트, 삭제 등)을 자동화
- 구현 방식: Hibernate와 같은 JPA 구현체가 JDBC를 통해 필요한 SQL 쿼리를 생성하고 실행
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children;
이 설정은 "parent 엔티티를 삭제할 때, JPA가 관련된 모든 children 엔티티도 함께 삭제하라"는 의미입니다.
데이터베이스 ON DELETE CASCADE (데이터베이스 레벨)
데이터베이스의 ON DELETE CASCADE는 데이터베이스 시스템 자체에 내장된 기능입니다:
- 작동 위치: 데이터베이스 서버
- 영향 범위: 데이터베이스 테이블과 레코드
- 주요 목적: 참조 무결성(Referential Integrity) 유지
- 구현 방식: 데이터베이스 엔진이 외래 키 제약조건을 처리
ALTER TABLE child
ADD CONSTRAINT fk_child_parent
FOREIGN KEY (parent_id) REFERENCES parent(id)
ON DELETE CASCADE;
이 설정은 "parent 테이블의 레코드가 삭제될 때, 데이터베이스가 자동으로 연결된 child 테이블의 레코드도 삭제하라"는 의미입니다.
왜 JPA Cascade가 데이터베이스 ON DELETE CASCADE로 자동 변환되지 않는가?
JPA를 사용하여 엔티티를 정의하고 테이블을 자동 생성할 때, JPA의 cascade 옵션이 데이터베이스의 ON DELETE CASCADE로 자동 변환되지 않는 이유는 다음과 같습니다:
설계 철학의 차이:
- JPA는 객체 지향 프로그래밍과 관계형 데이터베이스 간의 패러다임 불일치를 해소하기 위한 것
- 데이터베이스 외래 키는 데이터 무결성을 보장하기 위한 것
- 이 두 메커니즘은 서로 다른 문제를 해결하기 위해 설계됨
실행 컨텍스트의 차이:
- JPA cascade는 애플리케이션이 실행 중일 때만 작동
- 데이터베이스 CASCADE는 어떤 클라이언트가 데이터베이스에 접근하더라도 항상 작동
- 이러한 차이로 인해 두 기능을 자동으로 연결하는 것은 예기치 않은 결과를 초래할 수 있음
Hibernate의 기본 설계 결정:
- Hibernate(JPA 구현체)는 의도적으로 이 두 기능을 분리하여 개발자가 각각 명시적으로 구성할 수 있도록 설계됨
- 이는 개발자에게 더 많은 제어력을 제공하지만, 동시에 두 설정을 일치시켜야 하는 책임도 부여함
다양한 데이터베이스 방언:
- 각 데이터베이스 시스템마다 CASCADE 옵션 구현 방식이 다를 수 있음
- JPA는 이러한 다양성을 고려하여 최소한의 공통 기능만 자동화하는 접근법을 취함
실제 문제 상황
// JPA 엔티티 설정
@Entity
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children;
}
@Entity
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
위 코드에서 JPA cascade는 설정되어 있지만, 실제 생성된 데이터베이스 스키마에는 ON DELETE CASCADE가 없을 수 있습니다:
CREATE TABLE child (
id BIGINT PRIMARY KEY,
parent_id BIGINT,
CONSTRAINT fk_child_parent FOREIGN KEY (parent_id) REFERENCES parent(id)
-- ON DELETE CASCADE가 없음!
);
이러한 상황에서 발생할 수 있는 문제:
JPA를 통한 삭제: Parent 엔티티를 JPA를 통해 삭제하면, 먼저 Child 엔티티를 삭제한 후 Parent 엔티티를 삭제합니다. 일반적으로 문제 없이 작동합니다.
데이터베이스 직접 삭제: 데이터베이스에서 직접 Parent 레코드를 삭제하려고 하면, 외래 키 제약조건 위반으로 오류가 발생합니다.
다른 애플리케이션에서 삭제: JPA를 사용하지 않는 다른 애플리케이션이 데이터베이스에 접근하여 삭제 작업을 수행할 경우, 예기치 않은 오류가 발생할 수 있습니다.
해결 방법
1. Hibernate @OnDelete 어노테이션 사용
@Entity
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Parent parent;
}
이 어노테이션은 Hibernate가 테이블을 생성할 때 외래 키에 ON DELETE CASCADE 제약조건을 추가하도록 지시합니다.
2. SQL 스크립트를 통한 제약조건 수정
ALTER TABLE child
DROP FOREIGN KEY fk_child_parent,
ADD CONSTRAINT fk_child_parent
FOREIGN KEY (parent_id)
REFERENCES parent (id)
ON DELETE CASCADE;
이 SQL을 애플리케이션 시작 시 실행하거나, 데이터베이스 마이그레이션 도구(Flyway, Liquibase 등)를 사용하여 적용할 수 있습니다.
3. 초기화 스크립트 사용
Spring Boot를 사용하는 경우:
spring.jpa.properties.hibernate.hbm2ddl.import_files=classpath:db/foreign_keys.sql
그리고 foreign_keys.sql
파일에 ALTER TABLE 문을 추가합니다.
4. 명시적인 삭제 순서 관리
서비스 레이어에서 삭제 로직을 직접 제어:
@Transactional
public void deleteParent(Long parentId) {
Parent parent = parentRepository.findById(parentId)
.orElseThrow(() -> new EntityNotFoundException("Parent not found"));
// 먼저 자식 엔티티 삭제
childRepository.deleteByParentId(parentId);
// 그 다음 부모 엔티티 삭제
parentRepository.delete(parent);
}
결론
JPA의 cascade와 데이터베이스의 ON DELETE CASCADE는 서로 보완적이지만 자동으로 연결되지 않는 별개의 메커니즘입니다. 두 기능의 차이점을 이해하고 적절히 설정하는 것이 중요합니다. 일관성 있는 데이터 관리를 위해 두 설정을 일치시키거나, 의도적으로 다르게 설정하는 경우 해당 결정의 이유와 영향을 문서화해야 합니다.
'Project' 카테고리의 다른 글
[FE] 프론트 관련 내용 정리 (0) | 2025.04.29 |
---|---|
[JPA] N+1문제 정의 + 해결 전략 (0) | 2025.02.15 |
[JPA] API 서비스 흐름 알아보기. (0) | 2025.02.02 |
[JPA] Entity 설계하기. (1) | 2025.01.27 |
[프로젝트 Day 5] OAuth2 로그인 (Google) (0) | 2024.11.19 |