엔티티를 삭제하는 기능을 개발하는 과정에서 Referential integrity constraint violation 에러가 발생하며 삭제되지 않는 상황이 생겼고, 이를 해결한 방법을 이야기해보려 합니다. 실제로 발생한 에러 메시지는 아래와 같습니다.
"could not execute statement [Referential integrity constraint violation: \"FKLLBJ31M7Y971AVM7XQG615NIY: PUBLIC.USER_CHAT_ROOM FOREIGN KEY(CHATROOM_ID) REFERENCES PUBLIC.CHAT_ROOM(CHATROOM_ID) (CAST(5 AS BIGINT))\"; SQL statement:\ndelete from chat_room where chatroom_id=? [23503-232]] [delete from chat_room where chatroom_id=?]; SQL [delete from chat_room where chatroom_id=?]; constraint [FKLLBJ31M7Y971AVM7XQG615NIY: PUBLIC.USER_CHAT_ROOM FOREIGN KEY(CHATROOM_ID) REFERENCES PUBLIC.CHAT_ROOM(CHATROOM_ID) (CAST(5 AS BIGINT)); SQL statement:\ndelete from chat_room where chatroom_id=? [23503-232]]",
Referential integrity constraint violation 에러 상황
먼저 에러가 발생한 상황은 Room 엔티티를 삭제하는 과정에서 일어났습니다.
Room 엔티티는 UserChatRoom 중간 테이블 엔티티와 일대다 단방향 연관관계였습니다. 아래 코드는 Room 엔티티와 UserChatRoom 엔티티의 구현 코드입니다.
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ChatRoom extends BaseTimeEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "chatroom_id")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rootclient_id")
private RootClient rootClient;
public static ChatRoom createRoom(String name, RootClient rootClient) {
return ChatRoom.builder()
.name(name)
.rootClient(rootClient)
.build();
}
public void update(String name) {
this.name = name;
}
}
@Entity
@Getter
public class UserChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@Setter
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "chatroom_id")
@Setter
private ChatRoom chatRoom;
}
두 엔티티는 일대다 관계이기 때문에 다쪽인 UserChatRoom 엔티티에서 @JoinColumn(name = "chatroom_id)를 통해 FK(외래키)를 관리합니다. 이때 ChatRoom 엔티티를 데이터베이스에서 삭제하는 과정에서 Referential integrity constraint violation 에러가 발생했습니다. 아래 코드는 ChatRoom 엔티티를 데이터베이스에서 삭제하고 해당 ChatRoom 엔티티가 연관된 UserChatRoom 엔티티를 모두 삭제하는 코드입니다.
@Override
public void deleteRoom(Long roomId) {
ChatRoom chatRoom = getChatRoomOrThrow(roomId);
roomRepository.delete(chatRoom);
deleteUserChatRoom(roomId);
}
deleteRoom 메소드를 호출하면 Referential integrity constraint violation 에러가 발생합니다.
Referential integrity constraint violation 에러를 해석하면 '참조 무결성 제약 조건 위반'으로 각 엔티티끼리는 참조할 수 없는 외래키 값을 가질 수 없어야 한다는 제약조건입니다. 이 말은 ChatRoom 엔티티를 삭제하며 ChatRoom과 관계를 맺고 있는 UserChatRoom 엔티티에서 참조할 수 없는 외래키 즉 ChatRoom을 가지게 되며 참조 무결성 제약 조건을 위반한 것입니다.
만약 에러가 발생하지 않고 ChatRoom 엔티티가 삭제되었다면, UserChatRoom에서는 삭제된 ChatRoom 엔티티를 외래키로 참조하지 못하게 되면서 ChatRoom 엔티티가 고아 상태가 됩니다.
해결 과정
이 에러를 해결하기 위해서 Cascade.REMOVE 설정을 추가하여 ChatRoom 엔티티가 삭제될 때 동시에 UserChatRoom 엔티티를 삭제하는 방법을 생각할 수 있습니다. 하지만 UserChatRoom 중간 테이블 엔티티는 User 엔티티와 Cascade.REMOVE로 설정되어 있어 ChatRoom에도 Cascade 설정을 추가할 수 없었습니다.
사실 deleteRoom 메소드에서의 메소드 호출 순서를 바꾸면 쉽게 해결될 일입니다.
에러가 나는 deleteRoom 메소드를 보면 ChatRoom 엔티티를 먼저 삭제하고, UserChatRoom 엔티티를 삭제합니다. 따라서 ChatRoom 엔티티가 먼저 삭제되며 UserChatRoom에 외래키로 참조하지 못하는 고아 객체가 들어가게 되며 에러가 발생하는 것입니다. 아래 수정한 deleteRoom 메소드처럼 UserChatRoom 엔티티를 먼저 삭제하고 ChatRoom 엔티티를 삭제하면 에러가 해결됩니다.
@Override
public void deleteRoom(Long roomId) {
ChatRoom chatRoom = getChatRoomOrThrow(roomId);
deleteUserChatRoom(roomId);
roomRepository.delete(chatRoom);
}
물론 이 방법만 있는 건 아닙니다. orphanRemoval = true 설정을 ChatRoom 엔티티의 userChatRoom 프로퍼티에 추가하는 방법도 가능합니다. Cascade.REMOVE는 부모 엔티티(ChatRoom)가 삭제될 때 자식 엔티티(UserChatRoom)도 삭제된다는 것을 의미하지만, orphanRemoval은 부모와의 관계가 끊어진 자식 엔티티가 삭제된다는 점에서 다릅니다. 아래 코드로 살펴봅시다.
@Entity
public class ChatRoom extends BaseTimeEntity {
@OneToMany(mappedBy = "chatRoom", orphanRemoval = true)
private List<UserChatRoom> userChatRooms = new ArrayList<>();
...
}
이렇게 ChatRoom 엔티티에 orphanRemoval = true로 설정하면, ChatRoom 엔티티가 먼저 삭제되더라도 고아가 된 UserChatRoom 엔티티까지 같이 삭제됩니다. 하지만 이 방법을 사용하기 위해서는 ChatRoom 엔티티와 UserChatRoom 엔티티를 양방향 관계로 설정해야 하기 때문에 차라리 둘을 단방향으로 두고 deleteRoom 메소드에서 삭제 순서를 변경하는 방식을 선택했습니다.
orphanRemoval과 관련해서는 아래 글을 살펴봅시다.
orphanRemoval (고아 객체)
orphanRemoval고아 객체... 번역이 조금... 그렇지만 말 그대로 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라 부릅니다. 여기서 부모 엔티티와 자식 엔티티는 어떤 기준으로 나눌까요
praaay.tistory.com
아래 글은 저와 굉장히 비슷한 상황에서 해당 에러를 해결하는 글입니다. 참고하기 좋습니다.
https://hungseong.tistory.com/59
Entity 삭제 시 JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation 오류 발생
Spring을 통해 장소와 해당 장소에 대한 이벤트를 CRUD하는 API를 설계하던 중 발생한 문제이다. Entity 연관관계 구성 Place Entity와 Event Entity가 1:N 양방향 연관관계로 구성되어 있다. Entity 설계 - Place @
hungseong.tistory.com
'실시간 채팅 솔루션 개발 > 문제 해결 사례' 카테고리의 다른 글
그럼 모든 JPA 에러는 롤백 처리해야 할까? (트랜잭션 롤백 여부) (0) | 2024.10.23 |
---|---|
JPA가 던지는 체크 에러를 언체크 에러로 변환하면, 트랜잭션이 롤백 될까? (0) | 2024.10.23 |
프록시의 PK만 조회하면, 1+N 쿼리 문제가 터지지 않는다? (1) | 2024.10.21 |
유연함을 위해 Custom Result 타입을 사용하자 (0) | 2024.09.28 |