Entity 삭제 시 JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation 오류 발생
JPA

Entity 삭제 시 JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation 오류 발생

Spring을 통해 장소와 해당 장소에 대한 이벤트를 CRUD하는 API를 설계하던 중 발생한 문제이다.

 

Entity 연관관계 구성

Place Entity와 Event Entity가 1:N 양방향 연관관계로 구성되어 있다.

 

Entity 설계

- Place

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Place {

    // 내용과 관련있는 프로퍼티만 설명함.
    
    @Id
    @GeneratedValue
    @Column(name = "place_id")
    private Long id;

    @ToString.Exclude
    @OrderBy("id")
    @OneToMany(mappedBy = "place")
    private final Set<Event> events = new LinkedHashSet<>();
}

Set 타입으로 다수의 Event를 가지고 있다. @OneToMany(mappedBy = "place")를 통해 연관관계의 주인을 Event Entity로 설정하였다.

 

- Event

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Event {

	// 내용과 관련있는 프로퍼티만 설명함.
    
    @Id @GeneratedValue
    @Column(name = "event_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "place_id")
    private Place place;
}

Place를 가지고 있다. @ManyToOne(fetch = FetchType.LAZY)를 통해 LAZY fetch를 수행하게 하였으며, @JoinColumn(name = "place_id")를 통해 FK를 Event 테이블에서 관리한다.

 

발생한 문제

Place Entity에 대한 삭제 기능을 구현하던 중, Place에 대한 삭제 수행 시 다음과 같은 오류가 발생하였다.

 

org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: 
Referential integrity constraint violation: 
"FKPUVIX4LEXRAKGDLT8SI1TBTXV: 
PUBLIC.EVENT FOREIGN KEY(PLACE_ID) REFERENCES PUBLIC.PLACE(PLACE_ID) 
(CAST(3 AS BIGINT))"; SQL statement:
delete from place where place_id=? [23503-200]
	at org.h2.message.DbException.getJdbcSQLException(DbException.java:459) ~[h2-1.4.200.jar:1.4.200]
    ...(생략)

오류 내용을 보면 Referential integrity constraint violation (참조 무결성 제약 조건 위반)이 발생하였음을 알 수 있다.

 

참조 무결성 제약 조건이란 각 릴레이션(관계)은 참조할 수 없는 외래키 값을 가질 수 없어야 한다는 제약조건이다. 

즉, Place를 삭제함으로써 어딘가의 릴레이션에서 참조할 수 없는 외래키 값을 가지게 되었다는 뜻이다.

 

위의 연관관계 구성을 다시 보며 생각해보면, Event Entity는 Place Entity와 연관관계를 맺고 있으며, Event Entity 측에서 Place Entity와의 연관관계에 대한 FK(외래키)를 가지고 있다.

 

그런데 Event Entity와 연관되어 있는 Place가 제거되면

Event Entity 측에서 가지고 있는 FK가 가리키는(참조하는) Place를 더이상 찾을 수 없어 참조 무결성 제약 조건을 위반하게 된다.

 

결과적으로, 부모인 Place가 제거되어 자식인 Event가 혼자 남아 고아(Orphan) 상태가 된 것이다.

 

 

해결 방법

부모가 사라진 자식은 어떻게 해야할까?

본 코드에서는 하나의 장소(Place)에 대한 여러 이벤트(Event)가 존재한다.

그러므로 부모인 Place가 삭제되었다면 Event도 존재할 필요가 없다.

부모(Place)가 삭제될 때 연관된 자식(Event)들을 함께 삭제할 수 있도록 다음과 같은 방식을 사용하여 해결할 수 있었다.

 

1. cascade = CascadeType.REMOVE

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Place {

    // 내용과 관련있는 프로퍼티만 설명함.
    
    @Id
    @GeneratedValue
    @Column(name = "place_id")
    private Long id;

    @ToString.Exclude
    @OrderBy("id")
    @OneToMany(mappedBy = "place", cascade = CascadeType.REMOVE)
    private final Set<Event> events = new LinkedHashSet<>();
}

 

자식인 events에 cascade = CascadeType.REMOVE 를 추가하여 영속성 전이(Cascade)를 REMOVE로 설정한다.

해당 설정을 통해 부모의 영속성 상태가 자식에게 전이됨으로써 부모인 Place가 제거될 때 연관된 자식인 Event도 함께 제거된다.

 

 

 

2. orphanRemoval = true

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Place {

    // 내용과 관련있는 프로퍼티만 설명함.
    
    @Id
    @GeneratedValue
    @Column(name = "place_id")
    private Long id;

    @ToString.Exclude
    @OrderBy("id")
    @OneToMany(mappedBy = "place", orphanRemoval = true)
    private final Set<Event> events = new LinkedHashSet<>();
}

자식인 events에 orphanRemoval = true를 추가하여 고아가 된 자식을 제거할 수 있도록 설정한다.

orphanRemoval 속성을 true로 두게되면 Place가 제거될 때 고아가 된 Event Entity도 함께 제거된다.

 

위 두 방식 모두 해당 연관관계에선 똑같이 동작하므로, 선택해서 사용하면 된다.

 

! 그러나 영속성 전이와 orphanRemoval이 어떤 상황에서도 똑같이 동작하는 것은 아니며, 각자의 역할이 존재한다.

 

- Cascade(영속성 전이)는 부모의 영속성 상태와 자식의 영속성 상태를 동일하게 관리하는 것이며, PERSIST, ALL, REMOVE, MERGE와 같이 다양한 속성이 존재한다.

- orphanRemoval은 고아가 된 객체에 대한 제거 여부를 설정하는 것이다. 참조 객체가 하나일 때만 사용해야 한다. 즉, 해당 부모가 삭제되었을 때 다른 곳에서 해당 객체를 참조하지 않아 고아 상태임이 확실 할 때 안전하게 사용하여야 한다는 것이다.