서론
스터디허브 프로젝트에서 무분별하게 FK를 설정한 탓에 문제가 발생했다.
아래는 스터디허브의 회원, 신청, 스터디의 ERD이다.
apply 테이블은 users, study 테이블의 식별자를 외래키로 참조한다.
apply 테이블이 users 테이블의 데이터를 참조할 때 users 테이블의 데이터를 지우려고 하면 외래키 제약조건에 의해 에러가 발생할 것으로 예상된다.
테스트를 돌려서 확인해보자.
문제
UserEntity와 StudyEntity의 식별자로 ApplyEntity를 조회하는 기능의 단위 테스트를 작성했다.
@Test
void 유저식별자와_스터디식별자로_스터디요청_엔티티_조회() {
// given
UserEntity user = userRepository.save(UserEntityFixture.DONGWOO.UserEntity_생성());
StudyEntity study = studyRepository.save(StudyEntityFixture.INU.studyEntity_생성());
ApplyEntity apply = makeApply(user, study);
applyRepository.save(apply);
applyRepository.flush();
userRepository.delete(user);
userRepository.flush();
// when
ApplyEntity result = applyRepository.findByUserIdAndStudyId(user.getId(), study.getId());
// then
assertAll(
() -> assertEquals(result.getUserId(), user.getId()),
() -> assertEquals(result.getStudyId(), apply.getId())
);
}
apply가 user를 참조하고 있는 상태에서 테스트 코드를 실행하니 외래키 제약조건을 위반했다는 에러가 발생했다.
ERROR 20104 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Referential integrity constraint violation: "CONSTRAINT_3B98: PUBLIC.APPLY FOREIGN KEY(USER_ID) REFERENCES PUBLIC.USERS(USER_ID) (CAST(1 AS BIGINT))"; SQL statement:
MySQL에서 테이블의 외래키 제약조건은 RESTRICT 로 선언되어있다.
RESTRICT 제약조건은 참조하는 테이블에 데이터가 남아 있으면 참조되는 테이블의 데이터를 삭제하거나 수정할 수 없다.
참조하는 테이블은 apply, 참조되는 테이블은 user 테이블인데, 참조되는 테이블의 데이터인 user 데이터를 삭제하면 제약조건에 위배되기 때문에 에러가 발생하는 것이다.
해결
두가지 해결법이 존재한다.
- ApplyEntity와 UserEntity를 FK 매핑을 하는 방식에서 id 매핑으로 변경.
- CascadeType.REMOVE를 이용해 부모 엔티티인 UserEntity 삭제 시 자식 엔티티인 ApplyEntity도 같이 삭제한다.
FK -> ID
외래키 참조 방식에서 UserEntity의 Id만 가지고 있는 방식으로 변경하면 ApplyEntity -> UserEntity로의 참조가 없어지기 때문에 외래키 제약 조건에 위배되지 않게 된다.
ApplyEntity와 UserEntity 간의 연관관계를 끊고id 매핑 방식으로 엔티티를 수정하고 다시 테스트를 돌려보겠다.
// 연관관계 끊기 이전
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity userEntity;
// 연관관계 끊고 id 매핑
@Column(name = "user_id")
private Long userId;
외래키 제약조건에 위배되지 않고 정상적으로 테스트가 동작했다.
CascadeType.REMOVE
CascadeType.REMOVE와 orphanRemoval = true를 함께 사용하면 아래와 같은 효과를 가진다.
- UserEntity가 삭제될 경우 해당 엔티티의 식별자를 외래키로 가지고있는 ApplyEntity가 삭제된다.
- UserEntity와 ApplyEntity의 연관관계가 끊어졌을 경우 끊어진 ApplyEntity를 고아로 취급해 DB에서 삭제한다.
UserEntity에 Cascade 속성과 orphanRemoval 속성을 아래와 같이 추가해줬다.
@OneToMany(mappedBy = "userEntity", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ApplyEntity> applyEntities = new ArrayList<>();
Cascade 속성을 추가해줬으니 UserEntity를 save 하면서 applyEntity 또한 자동으로 save 하도록 테스트 코드를 수정했다.
@Test
void 유저식별자와_스터디식별자로_스터디요청_엔티티_조회() {
// given
UserEntity user = UserEntityFixture.DONGWOO.UserEntity_생성();
StudyEntity study = studyRepository.save(StudyEntityFixture.INU.studyEntity_생성());
ApplyEntity apply = makeApply(user, study);
List<ApplyEntity> list = List.of(apply);
user.updateApply(list);
userRepository.save(user);
userRepository.flush();
userRepository.delete(user);
userRepository.flush();
// when
ApplyEntity result = applyRepository.findByUserEntity(user);
// then
assertAll(
() -> assertEquals(result.getUserEntity().getId(), user.getId()),
() -> assertEquals(result.getStudyId(), apply.getId())
);
}
테스트 결과 NullPointerException이 발생하고, delete 쿼리문이 날아간 뒤에 select 쿼리까지 날아갔다.
NullPointerException은 유저가 삭제된 뒤 applyRepository.findByUserEntity에서 삭제된 유저로 조회하려 해서 발생했다.
결과적으로 userEntity를 삭제하기 전 Cascade 속성을 통해 ApplyEntity를 먼저 삭제했고 UserEntity를 삭제했기 때문에 외래키 제약 조건에 위배되지 않은 것이다.
결론
스터디허브의 비즈니스 로직을 생각해 보았을 때 회원은 신청한 스터디에 관계없이 자유롭게 탈퇴가 가능해야한다. 하지만 연관관계 매핑을 통해 스터디 신청 테이블에서 외래키를 가지고 있는 방법으로 회원 탈퇴를 할 경우 외래키 제약조건에 의해 에러가 터진다.
이를 해결하기 위해 식별자 값만 가지고 있는 id 매핑으로 해결할 수 있고, cascadeType 속성으로 회원 탈퇴 시 ApplyEntity도 자동으로 삭제해줄 수도 있다.
각자 장단점이 있지만 데이터 관련 작업은 실수가 발생해선 안되는 중요한 작업이기 때문에 예상하지 못한 쿼리문이 나갈 수 있는 cascadeType 속성은 지양하기로 결정했다.
불편함을 감수해서라도 보수적으로 다가가는 것이 좋을 것 같다는 생각이 있다.
그렇기에 id 매핑을 사용하고 데이터 삭제 시 아래와 같이 직접 로직을 만들어 쿼리문이 어떻게 날아가는지 파악하며 삭제하는 것이 좋을 것 같다.
https://www.youtube.com/watch?v=vgNHW_nb2mg
https://github.com/study-hub-inu/study-hub-server
GitHub - study-hub-inu/study-hub-server
Contribute to study-hub-inu/study-hub-server development by creating an account on GitHub.
github.com
'개발 > Database' 카테고리의 다른 글
MySQL InnoDB B-Tree Index 탐색 과정 분석 (0) | 2024.04.11 |
---|---|
Hash 조인, NL 조인 (2) | 2024.02.16 |
[StudyHub] 성능 관점에서 확인해본 조회 쿼리문 (0) | 2024.02.11 |
[StudyHub] 다중 칼럼 인덱스를 이용한 조회 성능 개선 (0) | 2024.01.12 |
DB 인덱싱 성능 테스트 (2) | 2023.12.04 |