데이터베이스에서 다형성 연관관계 재구현: 레일즈가 제공하는 방식보다 엄격한 대안

Reimplementing polymorphic associations in the database

작성자
발행일
2025년 05월 12일

핵심 요약

  • 1 레일즈의 다형성 연관관계는 편리하지만, 관계형 데이터베이스 원칙에 위배되어 대규모 시스템에서 성능 및 복잡성 문제를 야기합니다.
  • 2 대안으로 단순한 CHECK 제약조건을 사용하거나, 슈퍼타입 테이블을 도입하여 참조 무결성을 강화하는 방법이 있습니다.
  • 3 슈퍼타입 테이블, CHECK 제약조건, 트리거를 활용한 데이터베이스 레벨의 다형성 구현은 견고한 데이터 무결성을 제공하지만, 구현 복잡성이 증가합니다.

도입

레일즈(Rails)의 다형성 연관관계(Polymorphic Associations)는 적은 코드로 복잡성을 압축하는 강력한 기능이지만, 편리함 뒤에는 비용이 따릅니다. 모델 이름 변경 시 `type` 컬럼 업데이트 누락과 같은 사소한 문제부터, 관계형 데이터베이스의 작동 방식에 위배되어 대규모 시스템에서 성능 및 복잡성 문제를 야기할 수 있다는 지적까지 존재합니다. 본 글은 이러한 다형성 연관관계의 한계를 인식하고, 데이터베이스 수준에서 더 엄격하고 견고한 대안을 구현하는 방법을 탐구합니다.

레일즈의 다형성 연관관계는 데이터 무결성을 약화시키고 확장 시 문제를 일으킬 수 있습니다. 이에 대한 대안은 다음과 같습니다.

레일즈 다형성 연관관계의 문제점

레일즈의 다형성 연관관계는 _type_id 컬럼을 사용하여 여러 모델과 연결되는데, 이는 관계형 데이터베이스의 참조 무결성을 직접적으로 보장하지 못합니다. GitLab 개발자 문서, SQL Antipatterns 저자 등 다수의 전문가들은 이러한 방식이 대규모 시스템에서 성능 저하와 복잡성을 초래할 수 있다고 경고합니다.

단순한 대안

  • 개별 테이블 사용: GitLab 개발자 문서에서는 각 타입마다 별도의 테이블을 사용할 것을 권장합니다.

  • CHECK 제약조건 활용: PostgreSQL에서는 공통 컬럼 세트에서 약간만 벗어나는 소수의 간단한 타입에 대해 각 서브타입에 CHECK 제약조건이 있는 테이블을 사용하는 것이 적합합니다. 예를 들어, poly_type에 따라 특정 컬럼의 NULL 여부를 강제하는 방식입니다. sql CREATE TABLE poly ( poly_id serial PRIMARY KEY , poly_type "char" NOT NULL REFERENCES poly_type , common_col text , a_col1 text , a_col2 text , b_col3 text , CONSTRAINT poly_allowed_cols_type_a CHECK (poly_type = 'a' OR (a_col1, a_col2) IS NULL) , CONSTRAINT poly_allowed_cols_type_b CHECK (poly_type = 'b' OR (b_col3) IS NULL) );

데이터베이스에서 다형성 연관관계 재구현 (슈퍼타입 테이블)

더 엄격한 대안은 슈퍼타입(Supertype) 테이블을 도입하는 것입니다. 예를 들어, standup_updatescomments에 모두 반응(reactions)을 추가하고 싶다면, 이 둘의 슈퍼타입인 reactionables 테이블을 생성합니다.

참조 무결성 보장

reactionables 테이블은 idtype을 가지며, 각 서브타입 테이블(standup_updates, comments)은 reactionable_id를 통해 reactionables를 참조합니다. 이때, 서브타입이 실제 reactionables 테이블에 존재하는지 확인하기 위해 사용자 정의 함수와 CHECK 제약조건을 사용합니다.

  • check_reactionable_exists 함수: target_idtarget_type을 받아 reactionables 테이블에 해당 레코드가 있는지 확인하는 함수를 생성합니다. ruby class AddCheckConstraintFunction < ActiveRecord::Migration[8.0] def up execute <<-SQL CREATE OR REPLACE FUNCTION check_reactionable_exists(target_id bigint, target_type CHAR(1)) RETURNS int AS ' SELECT COALESCE( (SELECT 1 FROM reactionables WHERE reactionables.id = target_id AND reactionables.type = target_type), 0 ); ' LANGUAGE SQL; SQL end # ... (down method) end

  • CHECK 제약조건 추가: 각 서브타입 테이블에 reactionable_id와 해당 서브타입의 고유 식별자(예: ‘S’ for standup_updates, ‘C’ for comments)를 사용하여 위 함수를 호출하는 CHECK 제약조건을 추가합니다. ruby class AddCheckConstraintToStandupUpdates < ActiveRecord::Migration[8.0] def up execute <<-SQL ALTER TABLE standup_updates ADD CONSTRAINT supertype_check CHECK(check_reactionable_exists(standup_updates.reactionable_id, 'S') = 1); SQL end # ... (down method) end

고아 레코드 방지

슈퍼타입 레코드가 서브타입 레코드보다 먼저 삭제되지 않도록 BEFORE DELETE 트리거를 사용하여 연쇄 삭제를 구현합니다. 이는 서브타입 레코드 삭제 시 관련 슈퍼타입 reactionable 레코드도 함께 삭제되도록 하여 고아 레코드를 방지합니다.

```sql CREATE FUNCTION cascade_delete_reactionable() RETURNS trigger AS \(BEGIN DELETE FROM reactionables WHERE reactionables.id=OLD.id; RETURN OLD; END;\) LANGUAGE plpgsql;

CREATE TRIGGER cascade_delete_standup_updates BEFORE DELETE ON standup_updates FOR EACH ROW EXECUTE PROCEDURE cascade_delete_reactionable(); ```

결론

데이터베이스 수준에서 다형성 연관관계를 재구현하는 것은 레일즈의 기본 다형성 기능보다 훨씬 더 번거롭고 복잡합니다. 하지만 대규모 시스템에서는 이러한 추가적인 노력이 강력한 참조 무결성을 보장하고, 고아 레코드를 방지하며, 잠재적인 성능 및 복잡성 문제를 해결하는 데 매우 중요할 수 있습니다. 비록 구현 비용은 높지만, 시스템의 견고성과 장기적인 유지보수 측면에서 볼 때 그 가치가 충분히 있습니다. 이 접근 방식은 Won Rhim의 아이디어에서 시작되었으며, 레일즈의 편리함과 데이터베이스의 엄격함 사이의 균형점을 찾는 중요한 논의를 제공합니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!