이전부터 현업에서는 ERD에 관계는 정의하더라도 실제 물리모델링(즉, 실제 테이블)에는 FK를 걸지 않고 사용하는 경우가 많다는 것을 들어왔습니다. 여러가지 이유가 있지만, 우선은 제가 실제로 불편함을 느껴보지 않아서 기존의 FK를 걸고 사용하는 방식으로 계속 사용했습니다. 하지만 여러 프로젝트를 해보면서 점점 불편함을 느꼈고, 이번 기회에 FK에 대해 정리하고 제가 불편함을 느꼈던 부분과 JPA에서 어떻게 사용할지 간단하게 정리하려 합니다.
FK를 걸어줌으로써 연관관계의 데이터가 존재하는지의 정합성을 강제합니다.
예를 들어, 회원(Member)가 있고 회원은 게시글(Post)을 작성한다고 합시다. 그러면 게시글 테이블에는 작성자인 회원 ID 칼럼이 있을 것이고, 여기에 FK가 걸리게 됩니다. 이렇게 FK가 걸리면 게시글 테이블의 데이터에 회원 ID는 실제로 존재하는 회원인지 정합성을 확인하게 되고, 회원을 삭제할 때도 이 회원이 작성한 게시글이 있을 경우 회원만 삭제하면 게시글의 회원 ID부분에 정합성이 깨지기 때문에 해당 게시글들을 먼저 삭제해주어야 합니다. 기타 이와 같은 정합성들을 DB 차원에서 보장해줍니다.
DB 차원에서 정합성을 확인해주어야 하기 때문에 성능이 느려집니다.
테이블 구조 변경 등에서 고려사항이 많아집니다. 또 구조 변경으로 인한 벌크연산이 발생할 경우 또 데이터 정합성으로 인해 느린 성능을 보일 수 있습니다.
(벌크연산의 발생의 경우 DB에서 잠시 fk 체크 기능을 끄고 벌크연산 수행 후 다시 켜는 방식을 사용할 수 있습니다.)
실행계획은 옵티마이저에 의해서 생성되는데, 옵티마이저가 사용하는 옵티마이징 팩터는 인덱스 구성과 적합하다고 하고 규칙기준 옵티마이저와 비용기준 옵티마이저 모두 외래키와 관련된 옵티마이징 팩터는 없다고 합니다.
이것이 제가 FK를 제거하기로한 가장 큰 원인입니다. 우선은 FK로 인해서 번거로워진 제 테스트 사례를 보겠습니다.
위 이미지는 제가 작성한 Repository 메서드에 대한 테스트입니다. 제가 테스트하기 위해 필요한 엔터티를 만들기 위해 FK가 걸려있는 많은 다른 엔터티를 생성해야 했습니다.
또한 테스트마다 테스트에 필요한 엔터티 뿐만 아니라 연관된 모든 엔터티가 생성되다 보니까 위 이미지와 같이 특정 엔터티에 문제가 생길 경우 연관이 없는 기타 다른 테스트들까지 영향을 받는 문제가 생겼습니다.
제가 보여드린 것과 같이 테스트 메서드마다 엔터티를 생성하지 않고 테스트 전역에서 사용할 수 있는 엔터티를 생성해둔다면 어떨까요? 전역 테스트 엔터티를 사용한 모든 테스트들은 그 전역 테스트 엔터티에 의존이 발생하게 되고 전역 테스트 데이터에 수정사항이 발생할 경우 이 엔터티를 사용한 많은 테스트 들이 깨질 수 있습니다. 그렇기 때문에 테스트 메서드는 그 메서드 자체로 하나의 어플리케이션으로 볼 수 있게 작성되어야 합니다.
FK의 여러 단점들이 있었지만 제가 가장 크게 경험한 것은 테스트에서의 번거로움이었고, 이로 인해서 FK를 없애기로 결심했습니다.
RDB는 확장이 어렵다는 특성이 있습니다. FK의 역할 중 정합성을 확인해주는 것은 어플리케이션에서도 충분히 가능하고, 어플리케이션 서버는 확장이 쉬운 형태로 개발되기 때문에 어플리케이션에서 이 역할을 하며 확장이 어려운 RDB의 부담도 덜어줄 수 있을 것이라 기대했습니다.
제가 실제 어플리케이션에서는 어떻게 사용하면 될지 연습해본 것을 공유합니다. 실제 코드는 제 github을 참고해주시면 감사드리겠습니다.
아래 사용된 FK 및 Index 관련 어노테이션들은 JPA에서 DDL을 생성할 때만 사용되는 것이므로 DB에 테이블 구성이 이미 되어있는 상태에서 JPA auto-ddl을 사용하지 않으면 영향은 없습니다. 하지만 DB를 확인하지 않고 어플리케이션 코드 내에서 DB 스키마 및 제약조건을 확인할 수 있다는 점에서 많이들 어노테이션을 붙이길 권장합니다.
엔터티 설계를 말씀드리겠습니다.
Member(회원)이 있고 Member가 Post(게시글)을 작성한다고 하겠습니다. 여기서 Post에 Member에 대한 FK가 존재하게 되는데 이를 FK가 있는 것과 없는 것으로 구성해보겠습니다.
Member 엔터티 입니다.
FK가 설정된 Post(FkPost) 엔터티 입니다. 기존에 많이들 보실 수 있는 구성일 것입니다. 위와 같이 구성하면 FK가 설정되는 이유는 member 필드에 걸려있는 @JoinColumn
의 foreignKey 설정이 디폴트로 @ForeignKey(PROVIDER_DEFAULT)
로 되어있기 때문입니다.
FK를 설정했기 때문에 JPA에서 생성해주는 DDL에서 이와 같이 FK 설정을 해주는 것을 볼 수 있습니다.
FK가 설정된 상태에서는 FkPost를 생성하기 위해서 FK가 걸려있는 Member까지 반드시 생성해서 DB에 저장해야 합니다.
Member를 테스트를 위해 실제 DB에는 저장하지 않고 임의로 생성해보시면 보이시는 것과 같이 DataIntegrityViolationException
이 발생됩니다.
FK 설정을 제거한 NonFkPost 엔터티 입니다. FkPost과 거의 같은 구성인데, member 필드의 @JoinColumn을 보면 foreignKey
설정을 NO_CONSTRAINT
로 하였습니다. FK를 설정하지 않았기 때문에 FkPost와 달리 FK 제약을 걸어주는 DDL은 생성되지 않습니다.
위 테스트 메서드를 보면 FkPost의 경우 테스트를 위해 실제 DB에는 저장되지 않은 임의의 Member를 통해 Post를 생성하면 예외가 발생했었지만, NonFkPost의 경우 FK가 걸려있지 않아 성공적으로 생성이 되는 것을 알 수 있습니다.
FK 제약을 걸어주면 Index도 자동으로 걸어줄까요? 그것은 DB by DB입니다. MySQL과 같은 특정 DB는 FK를 걸면 FK에 index를 걸어주기도 합니다.
NonFkPost 는 보이시는 것과 같이 연관관계가 있는 필드에 의해 조회되는 메서드가 구성될 수 있고 그러면 보이시는 것과 같이 쿼리가 발생됩니다. 만약 member_id 부분에 인덱스가 걸려있지 않으면 쿼리의 속도가 느려질 수 있습니다. 이런 경우를 위해서 FK를 제거한 후 인덱스가 필요하다면 별도로 인덱스를 생성하거나 혹은 JPA를 통해서 인덱스를 생성해주는 것이 좋을 것 같습니다.
FK가 걸려있어도 비식별관계이기 때문에 다른 엔티티를 생성하지 않아도 문제없지 않나요?