InfoGrab DocsInfoGrab Docs

다형성 연관

요약

요약: 다형성 연관 대신 항상 별도의 테이블을 사용합니다. Rails에서는 이른바 "다형성 연관"을 정의할 수 있습니다. 이런 구성이 유용해 보일 수 있지만 단점이 많아, 어떤 경우에도 피해야 합니다. 이 구성은 사용할 모델을 문자열 값으로 판별하므로 공간을 많이 낭비합니다.

요약: 다형성 연관 대신 항상 별도의 테이블을 사용합니다.

Rails에서는 이른바 "다형성 연관"을 정의할 수 있습니다. 보통은 테이블에 두 개의 칼럼, 즉 대상 유형 칼럼과 대상 ID 칼럼을 추가하는 방식으로 구현합니다. 예를 들어 이 문서를 작성하는 시점에 members는 다음 칼럼으로 이런 구성을 사용하고 있습니다.

  • source_type: 사용할 모델을 정의하는 문자열이며 Project 또는 Namespace가 됩니다.
  • source_id: source_type을 기준으로 가져올 행의 ID 입니다. 예를 들어 source_type 이 Project 이면 source_id에는 프로젝트 ID가 들어갑니다.

이런 구성이 유용해 보일 수 있지만 단점이 많아, 어떤 경우에도 피해야 합니다.

낭비되는 공간#

이 구성은 사용할 모델을 문자열 값으로 판별하므로 공간을 많이 낭비합니다. 예를 들어 Project와 Namespace의 최대 크기는 9바이트이고, PostgreSQL에서는 문자열마다 1바이트가 더 붙습니다. 행마다 10바이트에 지나지 않더라도, 이런 구성을 쓰는 테이블과 행이 충분히 많아지면 디스크 공간과 (인덱스를 위한) 메모리를 꽤 낭비하게 됩니다.

인덱스#

연관이 두 개의 칼럼으로 나뉘어 있으므로, 쿼리를 효율적으로 수행하려면 복합 인덱스가 필요할 수 있습니다. 복합 인덱스 자체가 잘못된 것은 아니지만, 최적의 성능을 내려면 인덱스 안의 칼럼 순서가 중요해 설정이 까다로울 수 있습니다.

정합성#

다형성 연관의 정말 큰 문제 하나는 외래 키로 데이터베이스 수준의 데이터 정합성을 강제할 수 없다는 점입니다. 데이터베이스 수준에서 정합성을 강제하려면 다형성 연관을 지원하는 외래 키 로직을 직접 작성해야 합니다.

데이터베이스 수준에서 정합성을 강제하는 것은 건강한 환경을 유지하는 데 반드시 필요하며, 이 또한 다형성 연관을 피해야 하는 이유입니다.

쿼리 부담#

다형성 연관을 사용하면 항상 두 칼럼으로 필터링해야 합니다. 예를 들어 다음과 같은 쿼리를 작성하게 됩니다.

SELECT *
FROM members
WHERE source_type = 'Project'
AND source_id = 13083;

두 칼럼에 모두 인덱스가 있으면 PostgreSQL은 이 쿼리를 상당히 효율적으로 수행할 수 있습니다. 다만 쿼리가 복잡해지면 이 인덱스를 효과적으로 사용하지 못할 수 있습니다.

뒤섞인 책임#

함수나 클래스와 마찬가지로 테이블도 하나의 책임, 즉 미리 정의된 칼럼 집합에 데이터를 저장하는 책임만 가져야 합니다. 다형성 연관을 사용하면 (칼럼 구성이 서로 다를 수 있는) 여러 종류의 데이터를 같은 테이블에 저장하게 됩니다.

해결 방법#

다행히 이 문제에는 해결 방법이 있습니다. 같은 테이블에 저장했을 유형마다 별도의 테이블을 사용하는 것입니다. 별도의 테이블을 쓰면 데이터베이스가 제공하는 기능을 모두 활용해 정합성을 보장하고 데이터를 효율적으로 조회할 수 있으며, 애플리케이션 로직을 추가로 작성할 필요도 없습니다.

프로젝트와 그룹의 승인된 구성원과 대기 중인 구성원을 함께 저장하는 members 테이블을 생각해 봅니다. 구성원이 대기 중인지는 requested_at 칼럼에 값이 설정되어 있는지로 판단합니다. 스키마 관점에서 이 구성은 일부 행에만 인덱스와 칼럼을 설정하게 되어 공간을 낭비할 수 있습니다. 이 테이블을 조회할 때도 최적이 아닌 쿼리를 쓰게 됩니다. 예를 들면 다음과 같습니다.

SELECT *
FROM members
WHERE requested_at IS NULL
AND source_type = 'GroupMember'
AND source_id = 4

이런 테이블은 별도의 테이블로 나누어야 합니다. 예를 들어 이 경우에는 테이블 4개가 됩니다.

  • project_members
  • group_members
  • pending_project_members
  • pending_group_members

이렇게 하면 데이터 조회가 아주 단순해집니다. 예를 들어 그룹의 구성원을 가져오려면 다음과 같이 실행합니다.

SELECT *
FROM group_members
WHERE group_id = 4

그룹에서 대기 중인 구성원을 모두 가져오려면 다음과 같이 실행합니다.

SELECT *
FROM pending_group_members
WHERE group_id = 4

둘 다 가져오려면 UNION을 사용할 수 있습니다. 다만 어떤 칼럼을 SELECT 할지 명시해야 하며, 그러지 않으면 결과 집합이 첫 번째 쿼리의 칼럼을 따릅니다. 예를 들면 다음과 같습니다.

SELECT id, 'Group' AS target_type, group_id AS target_id
FROM group_members

UNION ALL

SELECT id, 'Project' AS target_type, project_id AS target_id
FROM project_members

위 예시는 다소 사소해 보일 수 있지만, 데이터를 합쳐 같은 페이지에 보여 주는 데 아무 제약이 없다는 점을 보여 줍니다. 칼럼을 명시적으로 선택하면 (사용하지도 않는 칼럼까지 전부 선택하는 경우에 비해) 데이터베이스가 데이터를 가져오는 작업이 줄어 쿼리 속도도 빨라집니다.

스키마도 더 단순해집니다. source_type 칼럼을 저장하고 인덱싱할 필요가 없어지고, 외래 키를 쉽게 정의할 수 있으며, IS NULL 조건으로 행을 필터링하지 않아도 됩니다.

정리하면, 별도의 테이블을 사용하면 외래 키를 효과적으로 쓰고, 필요한 곳에만 인덱스를 만들고, 공간을 아끼고, 데이터를 더 효율적으로 조회하고, (예를 들어 서로 다른 디스크에 저장하는 식으로) 테이블을 더 쉽게 확장할 수 있습니다. 부수적인 이점으로 하나의 모델이 여러 종류의 데이터를 처리하지 않아도 되므로 코드도 더 단순해집니다.

다형성 연관

GitLab v19.4
원문 보기

요약

요약: 다형성 연관 대신 항상 별도의 테이블을 사용합니다. Rails에서는 이른바 "다형성 연관"을 정의할 수 있습니다. 이런 구성이 유용해 보일 수 있지만 단점이 많아, 어떤 경우에도 피해야 합니다. 이 구성은 사용할 모델을 문자열 값으로 판별하므로 공간을 많이 낭비합니다.

요약: 다형성 연관 대신 항상 별도의 테이블을 사용합니다.

Rails에서는 이른바 "다형성 연관"을 정의할 수 있습니다. 보통은 테이블에 두 개의 칼럼, 즉 대상 유형 칼럼과 대상 ID 칼럼을 추가하는 방식으로 구현합니다. 예를 들어 이 문서를 작성하는 시점에 members는 다음 칼럼으로 이런 구성을 사용하고 있습니다.

  • source_type: 사용할 모델을 정의하는 문자열이며 Project 또는 Namespace가 됩니다.
  • source_id: source_type을 기준으로 가져올 행의 ID 입니다. 예를 들어 source_type 이 Project 이면 source_id에는 프로젝트 ID가 들어갑니다.

이런 구성이 유용해 보일 수 있지만 단점이 많아, 어떤 경우에도 피해야 합니다.

낭비되는 공간#

이 구성은 사용할 모델을 문자열 값으로 판별하므로 공간을 많이 낭비합니다. 예를 들어 Project와 Namespace의 최대 크기는 9바이트이고, PostgreSQL에서는 문자열마다 1바이트가 더 붙습니다. 행마다 10바이트에 지나지 않더라도, 이런 구성을 쓰는 테이블과 행이 충분히 많아지면 디스크 공간과 (인덱스를 위한) 메모리를 꽤 낭비하게 됩니다.

인덱스#

연관이 두 개의 칼럼으로 나뉘어 있으므로, 쿼리를 효율적으로 수행하려면 복합 인덱스가 필요할 수 있습니다. 복합 인덱스 자체가 잘못된 것은 아니지만, 최적의 성능을 내려면 인덱스 안의 칼럼 순서가 중요해 설정이 까다로울 수 있습니다.

정합성#

다형성 연관의 정말 큰 문제 하나는 외래 키로 데이터베이스 수준의 데이터 정합성을 강제할 수 없다는 점입니다. 데이터베이스 수준에서 정합성을 강제하려면 다형성 연관을 지원하는 외래 키 로직을 직접 작성해야 합니다.

데이터베이스 수준에서 정합성을 강제하는 것은 건강한 환경을 유지하는 데 반드시 필요하며, 이 또한 다형성 연관을 피해야 하는 이유입니다.

쿼리 부담#

다형성 연관을 사용하면 항상 두 칼럼으로 필터링해야 합니다. 예를 들어 다음과 같은 쿼리를 작성하게 됩니다.

SELECT *
FROM members
WHERE source_type = 'Project'
AND source_id = 13083;

두 칼럼에 모두 인덱스가 있으면 PostgreSQL은 이 쿼리를 상당히 효율적으로 수행할 수 있습니다. 다만 쿼리가 복잡해지면 이 인덱스를 효과적으로 사용하지 못할 수 있습니다.

뒤섞인 책임#

함수나 클래스와 마찬가지로 테이블도 하나의 책임, 즉 미리 정의된 칼럼 집합에 데이터를 저장하는 책임만 가져야 합니다. 다형성 연관을 사용하면 (칼럼 구성이 서로 다를 수 있는) 여러 종류의 데이터를 같은 테이블에 저장하게 됩니다.

해결 방법#

다행히 이 문제에는 해결 방법이 있습니다. 같은 테이블에 저장했을 유형마다 별도의 테이블을 사용하는 것입니다. 별도의 테이블을 쓰면 데이터베이스가 제공하는 기능을 모두 활용해 정합성을 보장하고 데이터를 효율적으로 조회할 수 있으며, 애플리케이션 로직을 추가로 작성할 필요도 없습니다.

프로젝트와 그룹의 승인된 구성원과 대기 중인 구성원을 함께 저장하는 members 테이블을 생각해 봅니다. 구성원이 대기 중인지는 requested_at 칼럼에 값이 설정되어 있는지로 판단합니다. 스키마 관점에서 이 구성은 일부 행에만 인덱스와 칼럼을 설정하게 되어 공간을 낭비할 수 있습니다. 이 테이블을 조회할 때도 최적이 아닌 쿼리를 쓰게 됩니다. 예를 들면 다음과 같습니다.

SELECT *
FROM members
WHERE requested_at IS NULL
AND source_type = 'GroupMember'
AND source_id = 4

이런 테이블은 별도의 테이블로 나누어야 합니다. 예를 들어 이 경우에는 테이블 4개가 됩니다.

  • project_members
  • group_members
  • pending_project_members
  • pending_group_members

이렇게 하면 데이터 조회가 아주 단순해집니다. 예를 들어 그룹의 구성원을 가져오려면 다음과 같이 실행합니다.

SELECT *
FROM group_members
WHERE group_id = 4

그룹에서 대기 중인 구성원을 모두 가져오려면 다음과 같이 실행합니다.

SELECT *
FROM pending_group_members
WHERE group_id = 4

둘 다 가져오려면 UNION을 사용할 수 있습니다. 다만 어떤 칼럼을 SELECT 할지 명시해야 하며, 그러지 않으면 결과 집합이 첫 번째 쿼리의 칼럼을 따릅니다. 예를 들면 다음과 같습니다.

SELECT id, 'Group' AS target_type, group_id AS target_id
FROM group_members

UNION ALL

SELECT id, 'Project' AS target_type, project_id AS target_id
FROM project_members

위 예시는 다소 사소해 보일 수 있지만, 데이터를 합쳐 같은 페이지에 보여 주는 데 아무 제약이 없다는 점을 보여 줍니다. 칼럼을 명시적으로 선택하면 (사용하지도 않는 칼럼까지 전부 선택하는 경우에 비해) 데이터베이스가 데이터를 가져오는 작업이 줄어 쿼리 속도도 빨라집니다.

스키마도 더 단순해집니다. source_type 칼럼을 저장하고 인덱싱할 필요가 없어지고, 외래 키를 쉽게 정의할 수 있으며, IS NULL 조건으로 행을 필터링하지 않아도 됩니다.

정리하면, 별도의 테이블을 사용하면 외래 키를 효과적으로 쓰고, 필요한 곳에만 인덱스를 만들고, 공간을 아끼고, 데이터를 더 효율적으로 조회하고, (예를 들어 서로 다른 디스크에 저장하는 식으로) 테이블을 더 쉽게 확장할 수 있습니다. 부수적인 이점으로 하나의 모델이 여러 종류의 데이터를 처리하지 않아도 되므로 코드도 더 단순해집니다.