테이블 상속 전략 (feat. JPA)

RDBMS에서는 객체지향 언어처럼 테이블 상속을 지원하지는 않는다. 하지만 중복되는 테이블에 대해 상속으로 데이터베이스를 설계하고 싶어진다. 이때 3가지의 대표적인 전략이 있다.

 

Single Table Inheritance

  • 하나의 테이블에 모든 테이터를 저장
  • dtype 으로 구분
  • 장점 : 조인이 필요없어 성능이 빠름
  • 단점 : 대부분 컬럼이 NULL을 허용해야 함, 테이블이 커지고 복잡해짐

 

Concrete Table Inheritance

  • 각 태이블이 완전체
  • 별도의 테이블을 모두 생성
  • 장점 : 테이블 간 독립적이고 NULL 컬럼 문제 없음
  • 단점 : 중복 필드 존재 가능, 다형성 쿼리(여러테이블 조회)어려움

 

Joined Table Inheritance

  • 부모 테이블, 자식 테이블을 나눠서 저장하고 조인하여 조회함
  • 장점 : 정규화가 잘 되고 데이터 중복이 없어짐
  • 단점 : 쿼리에 조인이 필수적이어서 복잡해지고 성능이 떨어질 수 있음

 


 

ORM 에서의 상속 전략

ORM(JPA, Hibernate, …) 에서 위의 3 가지 전략을 가져와 어노테이션으로 지원한다. Java ORM 표준 인터페이스인 JPA를 이용해 자세히 알아보자.

※ ORM (Object-Relational Mapping) : 관계형 데이터를 자동으로 연결해주는 기술

 

1️⃣ SINGLE_TABLE 전략 (단일 테이블 전략)

 

📁 Payment.java

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")// dtype
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "amount", nullable = false)
    private int amount;
}

 

📁 CardPayment.java

@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    @Column(name="card_number", nullable = false)
    private String cardNumber;
}

 

📁 BankTransferPayment.java

@Entity
@DiscriminatorValue("BANK")
public class BankTransferPayment extends Payment {
    @Column(name = "bank_account", nullable = false)
    private String bankAccount;
}

 

📁 TableService.java

@Service
@RequiredArgsConstructor
public class TableService {

    private final PaymentRepository paymentRepository;

    public void tableExample() {
        // 카드 결제 저장
        CardPayment cardPayment = new CardPayment(7500, "9876-5432-1098-7654");
        paymentRepository.save(cardPayment);

        // 은행 이체 결제 저장
        BankTransferPayment bankPayment = new BankTransferPayment(12000, "210-6543-0987");
        paymentRepository.save(bankPayment);

        // 전체 결제 내역 조회
        List<Payment> payments = paymentRepository.findAll();

        for (Payment payment : payments) {
            System.out.println("결제 ID: " + payment.getId() + ", 금액: " + payment.getAmount());

            if (payment instanceof CardPayment card) {
                System.out.println("카드번호: " + card.getCardNumber());
            } else if (payment instanceof BankTransferPayment bank) {
                System.out.println("은행계좌: " + bank.getBankAccount());
            }
        }
    }
}

 

 

  • 모든 결제 정보가 하나의 테이블에 모인다.
  • payment_type 의 값에서 CARD, BANK 로 구분 된다.
  • CARD 일때 bankAccountNULL, BANK일 때 cardNumberNULL값이 된다.
  • 장점 : JOIN 과정이 없기 때문에 조회 성능이 빠름
  • 단점 : NULL 컬럼이 많아져 테이블이 지저분해질 수 있음

 


 

2️⃣ JOINED 전략 (조인 전략)

 

📁 Payment.java

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "payment_type")// dtype
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "amount", nullable = false)
    private int amount;
}

 

📁 CardPayment.java

@Entity
public class CardPayment extends Payment {
    @Column(name="card_number", nullable = false)
    private String cardNumber;
}

 

📁 BankTransferPayment.java

@Entity
public class BankTransferPayment extends Payment {
    @Column(name = "bank_account", nullable = false)
    private String bankAccount;
}

 

 

  • Payment 테이블에 공통 데이터를 넣고 자식 테이블에 추가 데이터를 넣는 방식이다.
  • 각각의 자식은 부모 테이블의 id값을 외래키로 갖게 된다.
  • 조회할 때 항상 JOIN 이 필요하다.
  • 장점 : NULL이 없어 정규화가 잘됨
  • 단점 : JOIN 성능 비용 존재함, 특히 조회가 많은 경우 주의

 

 


 

3️⃣ TABLE_PER_CLASS 전략 (구체 테이블 전략)

 

📁 Payment.java

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "amount", nullable = false)
    private int amount;
}

 

📁 CardPayment.java

@Entity
public class CardPayment extends Payment {
    @Column(name="card_number", nullable = false)
    private String cardNumber;
}

 

📁 BankTransferPayment.java

@Entity
public class BankTransferPayment extends Payment {
    @Column(name = "bank_account", nullable = false)
    private String bankAccount;
}

 

 

  • 각 자식 클래스마다 테이블을 별도로 생성한다.
  • Payment 테이블이 존재하지 않고 amount 컬럼도 중복 저장된다.
  • Payment_SEQ : 자식 테이블이 독립적이라 공통된 시퀀스가 필요해 Hibernate가 내부적으로 흉내를 내서 MySQL에서도 시퀀스를 사용하게 해준다
항목 sequence auto increment
사용방식 별도 객체에서 ID 생성 테이블 컬럼 자체에서 자동 증가
지원 DB Oracle, PostgreSQL, 등 MySQL, MariaDB
유연성 여러 테이블에서 공유 가능, 제어 가능 단순 자동 증가, 제어 어려움
  • 장점 : 테이블마다 독립성 보장
  • 단점 : Payment 로 전체 조회하면 UNION 쿼리가 발생하기 때문에 느림

 


 

📝 요약

 

항목 SINGLE_TABLE JOINED TABLE_PER_CLASS
테이블 구조 하나의 테이블 부모 + 자식 테이블 분리 자식 테이블만 존재
쿼리 방식 단순 SELECT 부모 기준 JOIN 자식 테이블 UNION ALL
타입 구분 방식 Discriminator 컬럼 필수 (@DiscriminatorColumn) 선택 사항 (@DiscriminatorColumn 가능) 필요 없음
장점 빠름, 구조 단순 정규화, 구조 명확 자식 간 완전 독립
단점 NULL 많음, 컬럼 과다 JOIN 성능 비용 조회 느림, 페이징 어려움
사용 예시 게시글(공지/질문), 결제 등 단순 상속 구조 결제(Payment), 직원(정규/계약) 등 구조적 모델링 드물게 사용, 시스템 로그/감사 이력 등 완전 분리 필요

 

'Database' 카테고리의 다른 글

그룹함수(Group Function) - Oracle  (0) 2025.05.21