Database/JPA / / 2024. 9. 13. 16:06

JPA 고급 상속 매핑 전략: 단일 테이블, 조인 테이블, 테이블당 클래스 비교와 활용

JPA에서 상속관계 매핑은 객체 지향 프로그래밍의 상속 개념을 데이터베이스 테이블에 매핑하는 방법을 말해. JPA는 상속 관계를 데이터베이스에 매핑할 때 세 가지 전략을 제공해: 단일 테이블 전략(Single Table Strategy), 조인드 테이블 전략(Joined Table Strategy), 그리고 테이블당 클래스 전략(Table per Class Strategy). 이 세 가지 각각에 대해 자세하고 쉽게 설명해줄게.

 

1. 단일 테이블 전략 (Single Table Strategy)

 

단일 테이블 전략은 부모 클래스와 자식 클래스의 모든 속성을 하나의 테이블에 저장하는 방식이야. 즉, 상속 구조에 있는 모든 엔티티가 하나의 테이블에 들어가고, 각 행에는 어떤 클래스에서 온 데이터인지 구분하는 컬럼이 포함돼.

 

특징:

 

장점: 성능이 좋아. 하나의 테이블에서 데이터를 조회하고 관리하기 때문에 조인(join) 연산이 필요 없어.

단점: 모든 속성을 하나의 테이블에 저장하므로 테이블이 매우 넓어질 수 있고, 공간 낭비가 발생할 수 있어. 특히 상속 관계에서 부모와 자식이 사용하는 속성이 다를 때, 자식 클래스에 필요 없는 필드가 NULL로 많이 생길 수 있어.

 

예시 코드:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public class Vehicle {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Car extends Vehicle {
    private int doors;
}

@Entity
public class Bike extends Vehicle {
    private boolean hasPedals;
}

 

여기서는 Vehicle 테이블에 Car와 Bike의 모든 속성이 함께 저장되고, DTYPE 컬럼으로 각각 Car와 Bike를 구분해.

 

2. 조인드 테이블 전략 (Joined Table Strategy)

 

조인드 테이블 전략은 부모 클래스의 속성을 부모 테이블에 저장하고, 자식 클래스의 속성은 각 자식 테이블에 따로 저장하는 방식이야. 상속 관계에서 자식 클래스의 데이터를 조회할 때는 부모 테이블과 자식 테이블을 조인해야 해.

 

특징:

 

장점: 중복된 데이터가 없어서 테이블 구조가 깔끔하고, 정규화가 잘 되어 있어. 자식 테이블에는 해당 자식 클래스에서 필요한 속성만 저장돼.

단점: 조인 연산이 많이 발생할 수 있어 조회 성능이 상대적으로 떨어질 수 있어.

 

예시 코드:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Vehicle {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Car extends Vehicle {
    private int doors;
}

@Entity
public class Bike extends Vehicle {
    private boolean hasPedals;
}

 

이 경우, Vehicle 테이블에는 name 속성만 저장되고, CarBike 테이블에는 각각 doorshasPedals 속성이 저장돼. 데이터를 조회할 때는 Vehicle 테이블과 Car 또는 Bike 테이블을 조인해야 해.

 

3. 테이블당 클래스 전략 (Table per Class Strategy)

 

이 전략은 부모 클래스와 자식 클래스 각각의 테이블을 따로 만들고, 모든 속성을 각 테이블에 저장하는 방식이야. 즉, 자식 클래스마다 독립적인 테이블이 생성되고, 부모 클래스의 속성도 함께 포함돼.

 

특징:

 

장점: 각 자식 클래스가 독립된 테이블을 가지기 때문에 구조가 단순해지고, 조인 없이 데이터를 조회할 수 있어.

단점: 부모 클래스의 속성이 자식 테이블에 중복돼 저장되기 때문에, 데이터 중복이 발생할 수 있어. 또한, 여러 자식 클래스에서 데이터를 조회할 때는 여러 테이블을 한번에 조회하는 복잡한 쿼리가 필요할 수 있어.

 

예시 코드:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Car extends Vehicle {
    private int doors;
}

@Entity
public class Bike extends Vehicle {
    private boolean hasPedals;
}

 

이 경우 Car 테이블과 Bike 테이블은 각각 부모 클래스의 속성인 name까지 포함한 독립적인 테이블로 생성돼.

 

정리

 

단일 테이블 전략: 하나의 테이블에 모든 데이터를 저장, 조회 성능이 좋지만 NULL이 많아질 수 있음.

조인드 테이블 전략: 각 클래스별로 테이블을 나눠 정규화가 잘 되어 있지만, 조인 연산이 많아 성능이 떨어질 수 있음.

테이블당 클래스 전략: 각 자식 클래스가 독립된 테이블을 가짐으로써 구조가 단순하지만, 데이터 중복이 발생할 수 있음.

 

각 전략은 사용하는 상황에 따라 장단점이 달라. 성능을 중시하는 경우 단일 테이블 전략이 유리하고, 데이터 정규화나 유지보수를 중시하는 경우 조인드 테이블 전략이 더 적합할 수 있어.

 

Tip💡

정규화(Normalization)는 데이터베이스 설계 과정에서 데이터를 구조화하여 중복을 줄이고, 일관성 있게 저장하기 위한 프로세스야. 주로 데이터베이스 테이블을 나누고 관계를 정의함으로써 데이터 무결성을 높이고, 데이터 저장 공간을 효율적으로 사용하게 돼.

 

정규화는 여러 단계로 나뉘며, 각각의 단계는 특정한 문제를 해결하는 데 초점을 맞춰:

 

1. 제1정규형(1NF):

각 테이블의 컬럼이 원자값(더 이상 나눌 수 없는 값)을 가져야 해. 즉, 한 컬럼에 여러 값을 저장하지 않도록 하고, 각 필드가 단일 값만을 가져야 한다는 원칙이야.

2. 제2정규형(2NF):

제1정규형을 만족하면서, 기본 키가 아닌 모든 속성이 기본 키 전체에 완전 함수 종속(기본 키의 일부가 아닌 전체에 의존하는 것)해야 해. 부분적 종속성을 제거하는 단계야.

3. 제3정규형(3NF):

제2정규형을 만족하면서, 기본 키에 종속되지 않은 컬럼 간의 이행적 종속성(어떤 컬럼이 기본 키를 통해 다른 컬럼에 의존하는 것)을 제거하는 것이야.

 

정규화를 통해 얻을 수 있는 장점:

 

데이터 중복 제거: 불필요한 데이터 중복을 줄여 저장 공간을 절약해.

데이터 무결성 유지: 데이터의 일관성을 유지하고 오류 발생 가능성을 줄여.

효율적인 데이터 수정: 데이터 수정 시 불필요한 작업을 줄일 수 있어.

 

정규화는 데이터베이스 성능을 향상시키지만, 너무 많이 분할하면 성능이 저하될 수 있으므로, 필요에 따라 정규화를 적용해야 해.

 

@DynamicUpdate@DynamicInsert는 JPA/Hibernate에서 성능 최적화를 위해 사용하는 어노테이션들이야. 각각의 어노테이션은 엔티티가 데이터베이스에 업데이트되거나 삽입될 때, 불필요한 SQL 쿼리를 줄이는 데 도움을 줘.

 

1. @DynamicUpdate

 

@DynamicUpdate는 엔티티의 업데이트 시 변경된 필드만 SQL 쿼리에 포함되도록 설정해. 기본적으로 JPA는 엔티티의 변경을 감지하면, 해당 엔티티의 모든 필드를 업데이트하는 SQL을 생성해. 하지만 변경되지 않은 필드까지 업데이트하는 것은 불필요한 작업이 될 수 있어.

 

@DynamicUpdate를 사용하면, 실제로 변경된 필드들만 업데이트 쿼리에 포함돼, 성능이 향상될 수 있어.

 

예시:

@Entity
@DynamicUpdate
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    // getters and setters
}

 

만약 name 필드만 변경되었다면, 기본적으로는 모든 필드를 업데이트하는 SQL이 생성되지만, @DynamicUpdate를 사용하면 name 필드만 업데이트하는 SQL이 생성돼.

 

2. @DynamicInsert

 

@DynamicInsert는 엔티티가 처음 삽입될 때, null이 아닌 필드만 SQL 쿼리에 포함되도록 설정해. 기본적으로 JPA는 엔티티 삽입 시 모든 필드를 포함한 INSERT 쿼리를 생성해. 하지만 값이 없는 필드까지도 포함하는 것은 비효율적일 수 있어.

 

@DynamicInsert는 값이 설정된 필드만 INSERT 쿼리에 포함시킴으로써, 더 효율적인 SQL이 생성되도록 도와줘.

 

예시:

@Entity
@DynamicInsert
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;

    private String address;

    // getters and setters
}

 

만약 nameemail만 설정되었고, address는 null인 상태로 삽입하려고 하면, @DynamicInsert를 사용하면 nameemailINSERT 쿼리에 포함돼.

 

결론

 

@DynamicUpdate: 엔티티 업데이트 시 변경된 필드만 포함하여 쿼리를 최적화.

@DynamicInsert: 엔티티 삽입 시 값이 있는 필드만 포함하여 쿼리를 최적화.

 

이 두 어노테이션을 사용하면 데이터베이스와의 상호작용을 더 효율적으로 만들 수 있어, 특히 대규모 트랜잭션에서 성능 최적화에 큰 도움이 돼.

 

1. @Inheritance(strategy=InheritanceType.JOINED)

 

@Inheritance 어노테이션은 JPA에서 엔티티 상속을 사용할 때, 부모-자식 간의 상속 관계를 어떻게 데이터베이스에 매핑할지 설정하는 데 사용돼. JPA에서는 상속 매핑 전략을 제공하는데, 그 중 JOINED 전략은 상속된 엔티티들을 각기 별도의 테이블에 저장하면서, 부모 엔티티와 자식 엔티티가 조인(Join) 되는 방식으로 데이터를 조회하게 돼.

 

InheritanceType.JOINED: 이 전략은 부모 엔티티의 공통 속성은 부모 테이블에 저장하고, 자식 엔티티의 고유 속성은 자식 테이블에 저장해. 데이터를 조회할 때는 부모 테이블과 자식 테이블을 조인하여 데이터를 가져오는 방식이야. 각 엔티티가 독립된 테이블을 가지기 때문에 데이터 구조가 명확해지지만, 성능은 조인 비용이 발생할 수 있어.

 

예시:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Vehicle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String type;
}

@Entity
public class Car extends Vehicle {
    private int numberOfDoors;
}

 

위 예시에서는 Vehicle 테이블과 Car 테이블이 따로 생성되고, Car 데이터를 가져올 때는 Vehicle 테이블과 조인해서 데이터를 가져와.

 

2. @DiscriminatorColumn(name=“DTYPE”)

 

@DiscriminatorColumn은 상속된 엔티티들 사이에서 엔티티 타입을 구분하는 데 사용하는 칼럼을 정의하는 어노테이션이야. 이 칼럼은 상속된 엔티티가 어떤 하위 엔티티인지를 명시적으로 구분해 주는 역할을 해.

 

name="DTYPE": DiscriminatorColumn에서 name 속성은 해당 칼럼의 이름을 지정해. 예를 들어, DTYPE이라는 이름의 칼럼에 각 하위 클래스의 식별 값이 저장돼, 이를 통해 엔티티가 어떤 타입인지 구분할 수 있어.

 

예시:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public class Vehicle {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String type;
}

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

 

위 예시에서는 Vehicle 테이블에 DTYPE이라는 칼럼이 추가되고, Car 엔티티가 저장될 때는 DTYPE 칼럼에 “CAR”라는 값이 들어가서 이 레코드가 Car 타입임을 구분해.

 

결론

 

@Inheritance(strategy=InheritanceType.JOINED): 부모와 자식 엔티티가 각기 다른 테이블에 저장되며, 데이터를 조회할 때는 테이블을 조인하여 데이터를 가져오는 상속 전략.

@DiscriminatorColumn(name="DTYPE"): 상속된 엔티티 사이에서 타입을 구분하기 위한 컬럼을 정의해, 엔티티가 어떤 하위 클래스인지를 명시적으로 표시.

 

TIp💡

IDENTITY 전략은 데이터베이스에서 자동으로 증가하는 ID 값을 사용하여 엔티티의 기본 키를 생성하는 방식이야. 이 방식은 각 테이블마다 독립적으로 키를 관리하므로, 테이블에 한 번에 하나씩의 레코드가 삽입될 때 즉시 키 값이 할당돼. 하지만 이 방식은 상속 전략 중 UNION 또는 @Inheritance(strategy=InheritanceType.UNION)와 호환되지 않아.

 

이유는 UNION 전략이 여러 테이블(부모 테이블과 자식 테이블들)에 데이터를 분산해서 저장하고, 조회할 때는 이 여러 테이블의 데이터를 UNION SQL 연산으로 결합하기 때문이야. 하지만 IDENTITY 전략은 각 테이블마다 독립적으로 키 값을 생성하는 방식을 사용하기 때문에, 여러 테이블에 걸친 유니온 연산에서는 키 생성이 일관성 있게 동작하지 않아.

 

구체적인 이유는 다음과 같아:

 

1. IDENTITY 특성상 즉시 할당 필요: IDENTITY 전략은 데이터가 삽입될 때 자동으로 증가하는 값을 즉시 할당해줘. 하지만 UNION 상속 전략에서는 데이터가 여러 테이블에 나뉘어 저장되므로, 키 할당을 테이블 간에 일관되게 적용하기가 어려워.

2. UNION 전략의 복잡성: UNION 상속 전략은 부모와 자식 테이블의 데이터를 하나로 묶어서 조회하는 방식을 사용하므로, IDENTITY처럼 자동 증가하는 키가 여러 테이블에 걸쳐 관리되는 구조에서는 비효율적이거나 충돌이 발생할 수 있어.

 

따라서, UNION 상속 전략IDENTITY 키 생성 방식은 서로 충돌할 수 있으며, 이 경우에는 SEQUENCETABLE 같은 다른 키 생성 전략을 사용하는 것이 적합해. SEQUENCE는 데이터베이스의 시퀀스를 사용하고, TABLE은 테이블을 이용해 일련 번호를 관리하므로, 여러 테이블 간에 일관된 키 생성이 가능해.

 

@MappedSuperclass는 JPA에서 공통 속성을 여러 엔티티에서 재사용할 때 사용하는 어노테이션이야. 이 어노테이션을 사용하면 부모 클래스에 정의된 필드와 매핑 정보가 자식 엔티티에 상속되지만, 독립적인 엔티티로는 취급되지 않아 데이터베이스에 직접 매핑되지 않아.

 

주요 특징:

 

1. 엔티티가 아님: @MappedSuperclass로 지정된 클래스는 데이터베이스에 테이블로 매핑되지 않아. 즉, 독립적인 엔티티가 아니므로, 데이터베이스에 이 클래스에 대한 테이블이 생성되지 않음.

2. 공통 속성 재사용: 여러 엔티티에서 공통적으로 사용하는 필드(예: id, createdAt, updatedAt 등)를 @MappedSuperclass로 상속할 수 있어. 이로 인해 코드 중복을 줄이고, 유지보수를 쉽게 할 수 있음.

3. 상속 관계: @MappedSuperclass로 지정된 클래스는 일반 상속처럼 자식 엔티티가 상속받을 수 있지만, 부모 클래스 자체는 엔티티가 아니므로 데이터베이스에 직접적으로 영향을 주지 않음.

 

예시:

@MappedSuperclass
public abstract class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    // Getters and setters...
}

 

위 코드에서는 BaseEntity@MappedSuperclass로 정의되어, 이 클래스의 필드인 id, createdAt, updatedAt가 상속받는 모든 엔티티에 포함돼. 하지만 BaseEntity 자체는 독립적으로 테이블이 생성되지 않아.

@Entity
public class User extends BaseEntity {

    private String username;
    private String password;

    // Other fields...
}

 

User 엔티티는 BaseEntity로부터 id, createdAt, updatedAt 필드를 상속받고, 이 필드들은 User 테이블에 포함돼.

 

언제 사용하나?

 

공통적인 속성이나 기능을 여러 엔티티에서 사용해야 할 때.

데이터베이스에 공통 필드가 들어가야 하지만, 해당 클래스를 독립적인 테이블로 만들 필요는 없을 때.

 

결론적으로, @MappedSuperclass공통 속성의 재사용을 위해 자주 사용되며, 이를 통해 코드 중복을 줄이면서도 효율적으로 엔티티 간의 상속을 관리할 수 있어.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유