InfoGrab DocsInfoGrab Docs

다형성 연관

요약

요약: 다형성 연관 대신 항상 별도 테이블을 사용하세요. Rails에서는 소위 "다형성 연관(polymorphic associations)"을 정의하는 것이 가능합니다. source_type: 사용할 모델을 정의하는 문자열로, Project 또는 Namespace가 될 수 있습니다.

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

Rails에서는 소위 "다형성 연관(polymorphic associations)"을 정의하는 것이 가능합니다. 이는 보통 테이블에 두 개의 칼럼, 즉 타깃 타입 칼럼과 타깃 ID 칼럼을 추가함으로써 동작합니다. 예를 들어, 이 글을 작성하는 시점에 members 테이블에는 다음과 같은 칼럼이 있습니다:

  • source_type: 사용할 모델을 정의하는 문자열로, Project 또는 Namespace가 될 수 있습니다.

  • source_id: source_type을 기반으로 조회할 행의 ID입니다. 예를 들어, source_typeProject이면 source_id에는 프로젝트 ID가 포함됩니다.

이러한 설정이 유용해 보일 수 있지만, 많은 단점이 있으므로 반드시 피해야 합니다.

낭비되는 공간#

이 설정은 사용할 모델을 결정하기 위해 문자열 값에 의존하기 때문에 많은 공간을 낭비합니다. 예를 들어, ProjectNamespace의 경우 최대 크기는 9바이트이며, PostgreSQL을 사용할 때 문자열마다 1바이트가 추가됩니다. 행당 10바이트에 불과해 보일 수 있지만, 이러한 설정을 사용하는 테이블과 행이 충분히 많아지면 디스크 공간과 메모리(인덱스를 위한)를 상당히 낭비하게 됩니다.

인덱스#

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

일관성#

다형성 연관의 매우 큰 문제점 중 하나는 외래 키를 사용하여 데이터베이스(DB) 수준에서 데이터 일관성을 강제할 수 없다는 것입니다. DB 수준에서 일관성을 강제하려면 다형성 연관을 지원하는 자체 외래 키 로직을 작성해야 합니다.

DB 수준에서 일관성을 강제하는 것은 건강한 환경을 유지하는 데 절대적으로 중요하며, 따라서 이것이 다형성 연관을 피해야 하는 또 다른 이유입니다.

쿼리 오버헤드#

다형성 연관을 사용할 때는 항상 두 칼럼 모두를 사용하여 필터링해야 합니다. 예를 들어, 다음과 같은 쿼리를 작성하게 될 수 있습니다:

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

위의 예시는 다소 단순하지만, 데이터를 병합하여 동일한 페이지에 표시하는 것을 막을 방법이 없다는 것을 보여줍니다. 칼럼을 명시적으로 선택하면 DB가 데이터를 가져오는 데 해야 하는 작업이 줄어들기 때문에 (사용하지 않는 칼럼을 포함한 모든 칼럼을 선택하는 것과 비교하여) 쿼리 속도를 높일 수도 있습니다.

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

요약하자면: 별도 테이블을 사용하면 외래 키를 효과적으로 사용하고, 필요한 곳에만 인덱스를 생성하고, 공간을 절약하고, 데이터를 더 효율적으로 쿼리하고, 이러한 테이블을 더 쉽게 확장할 수 있습니다(예: 별도 디스크에 저장). 이로 인한 좋은 부수 효과는 단일 모델이 서로 다른 종류의 데이터를 처리하는 책임을 지지 않아도 되므로 코드도 더 간단해질 수 있다는 것입니다.

다형성 연관

GitLab v19.1
원문 보기
요약

요약: 다형성 연관 대신 항상 별도 테이블을 사용하세요. Rails에서는 소위 "다형성 연관(polymorphic associations)"을 정의하는 것이 가능합니다. source_type: 사용할 모델을 정의하는 문자열로, Project 또는 Namespace가 될 수 있습니다.

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

Rails에서는 소위 "다형성 연관(polymorphic associations)"을 정의하는 것이 가능합니다. 이는 보통 테이블에 두 개의 칼럼, 즉 타깃 타입 칼럼과 타깃 ID 칼럼을 추가함으로써 동작합니다. 예를 들어, 이 글을 작성하는 시점에 members 테이블에는 다음과 같은 칼럼이 있습니다:

  • source_type: 사용할 모델을 정의하는 문자열로, Project 또는 Namespace가 될 수 있습니다.

  • source_id: source_type을 기반으로 조회할 행의 ID입니다. 예를 들어, source_typeProject이면 source_id에는 프로젝트 ID가 포함됩니다.

이러한 설정이 유용해 보일 수 있지만, 많은 단점이 있으므로 반드시 피해야 합니다.

낭비되는 공간#

이 설정은 사용할 모델을 결정하기 위해 문자열 값에 의존하기 때문에 많은 공간을 낭비합니다. 예를 들어, ProjectNamespace의 경우 최대 크기는 9바이트이며, PostgreSQL을 사용할 때 문자열마다 1바이트가 추가됩니다. 행당 10바이트에 불과해 보일 수 있지만, 이러한 설정을 사용하는 테이블과 행이 충분히 많아지면 디스크 공간과 메모리(인덱스를 위한)를 상당히 낭비하게 됩니다.

인덱스#

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

일관성#

다형성 연관의 매우 큰 문제점 중 하나는 외래 키를 사용하여 데이터베이스(DB) 수준에서 데이터 일관성을 강제할 수 없다는 것입니다. DB 수준에서 일관성을 강제하려면 다형성 연관을 지원하는 자체 외래 키 로직을 작성해야 합니다.

DB 수준에서 일관성을 강제하는 것은 건강한 환경을 유지하는 데 절대적으로 중요하며, 따라서 이것이 다형성 연관을 피해야 하는 또 다른 이유입니다.

쿼리 오버헤드#

다형성 연관을 사용할 때는 항상 두 칼럼 모두를 사용하여 필터링해야 합니다. 예를 들어, 다음과 같은 쿼리를 작성하게 될 수 있습니다:

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

위의 예시는 다소 단순하지만, 데이터를 병합하여 동일한 페이지에 표시하는 것을 막을 방법이 없다는 것을 보여줍니다. 칼럼을 명시적으로 선택하면 DB가 데이터를 가져오는 데 해야 하는 작업이 줄어들기 때문에 (사용하지 않는 칼럼을 포함한 모든 칼럼을 선택하는 것과 비교하여) 쿼리 속도를 높일 수도 있습니다.

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

요약하자면: 별도 테이블을 사용하면 외래 키를 효과적으로 사용하고, 필요한 곳에만 인덱스를 생성하고, 공간을 절약하고, 데이터를 더 효율적으로 쿼리하고, 이러한 테이블을 더 쉽게 확장할 수 있습니다(예: 별도 디스크에 저장). 이로 인한 좋은 부수 효과는 단일 모델이 서로 다른 종류의 데이터를 처리하는 책임을 지지 않아도 되므로 코드도 더 간단해질 수 있다는 것입니다.