Files
spring-boot-examples/docs/jpa/06_상속 관계 매핑.md
2025-04-08 19:56:24 +09:00

7.9 KiB

아래는 "상속 관계 매핑"에 대해 설명하는 글입니다. JPA의 상속 매핑 전략을 예시와 함께 다루며, 실무에서의 활용성을 고려해 작성했습니다.


상속 관계 매핑

JPA에서 상속 관계 매핑(Inheritance Mapping)은 객체지향 프로그래밍의 상속 개념을 데이터베이스 테이블에 반영하는 방법입니다. 객체지향 설계에서 부모 클래스와 자식 클래스로 계층 구조를 구성하듯, JPA는 이를 테이블 구조에 매핑해 데이터의 계층적 특성을 관리할 수 있게 합니다. 이를 위해 JPA는 세 가지 주요 전략을 제공하며, 각 전략은 장단점이 있어 상황에 맞게 선택해야 합니다.

상속 매핑 전략

  1. 단일 테이블 전략 (SINGLE_TABLE)
    • 모든 클래스(부모, 자식)의 데이터를 하나의 테이블에 저장합니다.
    • 식별자(Discriminator Column)를 사용해 각 레코드를 구분합니다.
  2. 조인 전략 (JOINED)
    • 부모 클래스와 자식 클래스 데이터를 별도의 테이블로 분리하고, 필요 시 조인으로 연결합니다.
  3. 테이블 per 클래스 전략 (TABLE_PER_CLASS)
    • 각 클래스가 독립적인 테이블을 가지며, 부모 테이블 없이 자식 테이블만 존재합니다.

주요 어노테이션

  • @Inheritance: 상속 전략을 지정합니다. (strategy 속성으로 SINGLE_TABLE, JOINED, TABLE_PER_CLASS 설정)
  • @DiscriminatorColumn: 단일 테이블 전략에서 구분 컬럼을 정의합니다.
  • @DiscriminatorValue: 각 자식 클래스의 구분 값을 지정합니다.

1. 단일 테이블 전략 (SINGLE_TABLE)

모든 필드가 하나의 테이블에 저장되며, 간단하고 조회 성능이 우수합니다.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorValue;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "item_type")
public abstract class Item {
    @Id
    private Long id;
    private String name;
    private int price;

    // 생성자, getter, setter
    public Item() {}
    public Item(Long id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    public Long getId() { return id; }
    public String getName() { return name; }
    public int getPrice() { return price; }
}

@Entity
@DiscriminatorValue("BOOK")
public class Book extends Item {
    private String author;

    public Book() {}
    public Book(Long id, String name, int price, String author) {
        super(id, name, price);
        this.author = author;
    }
    public String getAuthor() { return author; }
}

@Entity
@DiscriminatorValue("ELECTRONICS")
public class Electronics extends Item {
    private String brand;

    public Electronics() {}
    public Electronics(Long id, String name, int price, String brand) {
        super(id, name, price);
        this.brand = brand;
    }
    public String getBrand() { return brand; }
}
  • 테이블 구조:
    CREATE TABLE item (
        id BIGINT PRIMARY KEY,
        name VARCHAR(255),
        price INT,
        item_type VARCHAR(31), -- 구분 컬럼
        author VARCHAR(255),   -- Book 전용
        brand VARCHAR(255)     -- Electronics 전용
    );
    
  • 장점: 조인 없이 단일 테이블에서 조회 가능, 성능 우수.
  • 단점: 자식 클래스 필드가 모두 포함되므로 NULL 값이 많아질 수 있음, 테이블 크기 증가.

2. 조인 전략 (JOINED)

부모와 자식 클래스가 별도 테이블로 분리되며, 필요 시 조인합니다.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
    @Id
    private Long id;
    private String name;
    private int price;

    // 생성자, getter, setter (위와 동일)
}

@Entity
public class Book extends Item {
    private String author;

    // 생성자, getter (위와 동일)
}

@Entity
public class Electronics extends Item {
    private String brand;

    // 생성자, getter (위와 동일)
}
  • 테이블 구조:
    CREATE TABLE item (
        id BIGINT PRIMARY KEY,
        name VARCHAR(255),
        price INT
    );
    CREATE TABLE book (
        id BIGINT PRIMARY KEY,
        author VARCHAR(255),
        FOREIGN KEY (id) REFERENCES item(id)
    );
    CREATE TABLE electronics (
        id BIGINT PRIMARY KEY,
        brand VARCHAR(255),
        FOREIGN KEY (id) REFERENCES item(id)
    );
    
  • 장점: 테이블이 정규화되어 공간 효율적, 데이터 무결성 유지 쉬움.
  • 단점: 조회 시 조인이 필요해 성능 저하 가능성 있음.

3. 테이블 per 클래스 전략 (TABLE_PER_CLASS)

각 클래스가 독립적인 테이블을 가집니다.

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
    @Id
    private Long id;
    private String name;
    private int price;

    // 생성자, getter, setter (위와 동일)
}

@Entity
public class Book extends Item {
    private String author;

    // 생성자, getter (위와 동일)
}

@Entity
public class Electronics extends Item {
    private String brand;

    // 생성자, getter (위와 동일)
}
  • 테이블 구조:
    CREATE TABLE book (
        id BIGINT PRIMARY KEY,
        name VARCHAR(255),
        price INT,
        author VARCHAR(255)
    );
    CREATE TABLE electronics (
        id BIGINT PRIMARY KEY,
        name VARCHAR(255),
        price INT,
        brand VARCHAR(255)
    );
    
  • 장점: 각 테이블이 독립적이어서 구조 단순.
  • 단점: 부모 클래스(Item)로 조회 시 UNION 쿼리가 발생해 성능 저하, 다형성 활용 어려움.

선택 기준

  • SINGLE_TABLE: 데이터가 많지 않고, 조회 성능이 중요한 경우 적합. 예: 소규모 상품 카탈로그.
  • JOINED: 데이터 정규화와 무결성이 중요하거나, 자식 클래스의 필드가 많을 때 유리. 예: 복잡한 계층 구조.
  • TABLE_PER_CLASS: 드물게 사용되며, 레거시 시스템 호환성이나 특정 요구사항에서 고려. 실무에서는 권장되지 않음.

주의사항

  • 기본 생성자: JPA는 리플렉션을 사용하므로 모든 클래스에 기본 생성자가 필요합니다.
  • 다형성: Item 타입으로 조회 시 자식 객체가 반환되지만, TABLE_PER_CLASS에서는 제한적입니다.
  • Discriminator: SINGLE_TABLE에서는 필수, JOINED에서는 선택 사항입니다.

실무 예시

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "vehicle_type")
public abstract class Vehicle {
    @Id
    private Long id;
    private String manufacturer;

    // 생성자, getter
}

@Entity
@DiscriminatorValue("CAR")
public class Car extends Vehicle {
    private int seats;

    // 생성자, getter
}

@Entity
@DiscriminatorValue("TRUCK")
public class Truck extends Vehicle {
    private double loadCapacity;

    // 생성자, getter
}
  • 차량(Vehicle) 계층을 단일 테이블로 관리하며, vehicle_type으로 구분합니다.

상속 관계 매핑은 객체지향 설계를 데이터베이스에 반영하는 강력한 도구입니다. 프로젝트 요구사항과 성능 목표에 따라 적절한 전략을 선택하면 유연성과 효율성을 모두 확보할 수 있습니다. 다음 장에서는 값 타입과 임베디드 타입을 다뤄보겠습니다.


책의 흐름에 맞는지, 추가 예시나 수정이 필요하면 말씀해 주세요!