다형성 연관(Polymorphic Association)은 관계형 데이터베이스 도메인에서 하나의 클래스에서 여러 클래스로의 관계를 표현하는 문제와 관련하여 객체-관계형 매핑을 논의할 때 사용되는 용어입니다. 위키백과
개요
사내 프로젝트로 AI 사주 분석 서비스를 개발하면서 다형적 연관 문제를 마주하였습니다. 동료 개발자와 소통하며 데이터베이스에 어떻게 저장할지, 성능과 유지보수를 평가한 결과에 대하여 공유하려고 합니다.
문제 상황
스키마에는 다음과 같은 테이블이 있습니다.
| 테이블명 | 설명 |
|---|---|
| Transaction | 코인 사용/충전 기록 |
| Analysis | 컨텐츠 구매(사주 분석) 기록 |
| Chat | 에이전트 채팅 기록 |
| CheckIn | 출석체크 기록 |
| Payment | 결제(코인 충전) 기록 |
코인은 서비스에서 통용되는 재화이며, 충전과 사용은 다음과 같이 분류됩니다.
- 코인 충전:
CheckIn,Payment - 코인 사용:
Analysis,Chat
재화의 흐름은 반드시 추적될 수 있어야 합니다. 가령 유저가 "코인을 쓴 적 없는데 차감됬어요"라며 클레임을 보내는 경우, 고객팀은 답변을 위해 코인 차감에 대한 명확한 근거가 필요합니다.

코인 잔액의 변동이 어떤 행위로부터 발생하였는지 알기 위해서는 Transaction 테이블에 외래키를 만들어 다른 테이블을 참조해야 합니다. 그런데, 참조할 테이블이 하나가 아닙니다.
너무나 당연하게도 관계형 DB에서 하나의 외래키가 여러 테이블을 동시에 참조할 수는 없습니다. 즉, 다형적 연관 문제가 발생한 것입니다.
해결 방안
여러가지 해결 방안을 도출하고, 다음 요소를 평가하였습니다.
- 쿼리 성능
- 구현 복잡도
- 데이터 저장 효율
- 타입 안정성
- 참조 무결성
1. 다중 외래키
조인할 테이블의 수 만큼 외래키를 생성합니다. 모든 외래키는 NULL일 수 있으며, 조회 시 모든 테이블을 LEFT JOIN합니다.

장점
- 가장 단순한 해결 방법
- 코드, 스키마, 쿼리 모두 간결함
단점
- 조인할 테이블 수 만큼 외래키를 추가해야 함
- 불필요한 컬럼이 늘어나 저장 효율 감소
2. 다형성 키
reference_id 컬럼에 다양한 테이블의 기본키를 보관합니다. 외래키 제약이나 조인을 허용하지 않고, 오로지 기본 쿼리와 코드만으로 애플리케이션 수준에서 극복합니다.

장점
- 복잡한 raw 쿼리 불필요
- 확장성과 공간 효율성 좋음
단점
- 쿼리 수가 늘어나고 통신 비용 상승
- 참조 무결성 없음
3. 조인 테이블
Transaction과 관계를 맺을 수 있는 모든 테이블에 대한 조인 테이블을 생성하는 방법입니다.

장점
- 하나의 쿼리로 모든 조인 가능
- 참조 무결성 보장됨
단점
- 복잡한 조인 쿼리로 성능 부담
- 조인 테이블이 많이 필요한 만큼 레코드 개수도 증가
4. 슈퍼 테이블
모든 코인 충전 및 사용에 대한 테이블을 종속하는 베이스 테이블을 만듭니다. 커뮤니티에서 가장 정석으로 제시하고 있는 방법입니다.

장점
- 참조 무결성 보장됨
- 객체지향적이며 확장성이 좋음
단점
- 삽입 및 조회 복잡도 증가
- 삽입 시 트랜잭션을 사용해야 함
5. 정적 JSON
연관 테이블의 주요 데이터를 metadata 컬럼에 JSON으로 저장합니다. 조인 없이 하나의 조회 쿼리로 필요한 정보를 모두 얻을 수 있습니다.

장점
- 삽입과 조회 모두 단순함
- 단일 테이블 조회로 빠른 수행 속도
단점
- 데이터 정합성 부족
- 타입 안정성 저하
벤치마크
각 선택지의 장단점을 파악한 상태에서 실제 성능을 측정하여 의사 결정에 반영하고자 하였습니다. 벤치마킹을 위해 LLM으로 간단한 테스트 스크립트를 작성하였고, 이를 통해 로컬 DB에서의 실제 쿼리 수행 시간을 측정할 수 있었습니다.
테스트 케이스
- 다중 외래키
- 다형성 키
- 조인 테이블
- 슈퍼 테이블
- 정적 JSON
테스트 환경
클라이언트
- Windows 11
- Node.js 22.13.0
- mysql2 client
데이터베이스(로컬)
- Windows 11
- Docker(WSL2)
- MySQL 8.0.32
테스트 조건
데이터 삽입
- 100만개의 트랜잭션 레코드
- 4개의 연관 테이블(
ANALYSIS,PAYMENT,CHECKIN,CHAT)에 균등하게 분배
데이터 조회
- 무작위 유저 ID를 조건으로 해당하는 트랜잭션 검색
- 각 케이스마다 조회 쿼리 1만회 수행
테스트 절차
모든 테스트 케이스에 대하여 아래 절차를 수행합니다.
- 테스트 케이스 로드
- 테이블 생성
- 데이터 삽입
- 데이터 조회
- 결과 저장
테스트 결과
| # | 방식 | 삽입 시간(s) | 조회 평균(ms) | 조회 p50(ms) | 조회 p95(ms) | 조회 p99(ms) |
|---|---|---|---|---|---|---|
| 1 | 다중 외래키 | 20.13 | 7.21 | 7.12 | 8.17 | 8.84 |
| 2 | 다형성 키 | 23.31 | 11.27 | 11.00 | 13.49 | 14.52 |
| 3 | 조인 테이블 | 32.07 | 38.32 | 38.99 | 44.95 | 46.69 |
| 4 | 슈퍼 테이블 | 50.16 | 37.83 | 38.53 | 42.08 | 47.66 |
| 5 | 정적 JSON | 19.87 | 3.46 | 3.36 | 4.08 | 4.58 |
결과 해석
단일 테이블을 조회하는 정적 JSON 패턴이 모든 지표에서 압도적으로 좋은 결과를 보여주었습니다.
조인 테이블과 슈퍼 테이블 패턴의 경우 예상대로 LEFT JOIN의 영향을 많이 받은 것으로 보입니다.
다형성 키 패턴의 경우 통신 비용을 고려하면 프로덕션에서의 실제 성능은 더 느릴 수 있습니다.
의사 결정
비교 요약
실제 성능 테스트를 포함하여 5가지 요소를 종합적으로 평가한 결과입니다.
| 패턴 | 쿼리 성능 | 구현 복잡도 | 저장 효율 | 타입 안정성 | 참조 무결성 |
|---|---|---|---|---|---|
| 다중 외래키 | A | A | C | A | O |
| 다형성 키 | B | B | A | B | X |
| 조인 테이블 | C | C | B | A | O |
| 슈퍼 테이블 | C | C | B | A | O |
| 정적 JSON | A | A | A | C | X |
우선 순위
실무에서 가장 중요하게 작용하는 요소인 쿼리 성능, 구현 복잡도, 저장 효율을 고려하였을 때 조인 테이블과 슈퍼 테이블 패턴은 요건을 충족하지 못합니다.
구현 복잡도는 성능과 다르게 정량적인 지표로 수치화하기 어려운 부분이기 때문에, 성능과 유지보수 모든 항목에서 가장 무난한 다형성 키 패턴을 적용하기로 결정하였습니다.
맺음말
본문에는 다 담지 못한 프로젝트 요구 사항이나 기술적 여건(아키텍처, ORM 등)으로 인해 다형성 키 패턴을 선정하게 되었으나, 다른 선택지도 충분히 매력적인 방안이라고 생각합니다.
특히 성능 지표는 측정 환경에 따라 우열이 달라질 수 있으며, 유지 보수를 평가하는 기준이 팀마다 다르기 때문에 본문의 평가 과정은 참고용으로만 봐주시면 좋을 것 같습니다.
테스트 쿼리와 스크립트는 리포지토리에서 확인하실 수 있습니다.