서론

이번 피드에서 다루는것

상속 매핑 전략

다형적 연관관계


상속?

 

Java에서 상속이란, 특정 클래스의 필드와 메서드를 새로운 클래스가 물려받는것을 의미한다.

이때. 특정 클래스를 부모/슈퍼 클래스라 칭하고 새로운 클래스를 자식/서브 클래스라고 부른다.

 

 

상속의 장점?

1. 코드 재사용성 : 중복되는 코드를 최소화 할 수 있다.

 

2. 다형성 : 슈퍼클래스 타입의 변수로 서브클래스를 참조할 수 있다.

이는 곧, 슈퍼 클래스 타입으로 인스턴스를 싸잡고. 특정 메서드 실행시점에 알맞은 하위타입의 동작이 실행됨을 보장한다.

추상화해서 표현하면 "공통 인터페이스를 통해 서로 다른 구체화된 객체를 같은 방식으로 다룰 수 있다" 가 되겠다.

이는 확장/변경 가능성이 많은 도메인이나, 플러그인 아키텍쳐나 전략패턴을 구현할 때 크게 도움이 된다.

 

* 이때, 메서드 오버라이딩을 통해 각 서브 클래스별 동작을 구체화 할 수 있다.

 

왜 JPA에서까지 상속을 신경써야 할까? 왜 JAVA의 상속정보가 DB에서도 유지가 되어야만 하지?

 

도메인 모델링 관점과 쿼리/퍼포먼스 관점에서의 장점이 있기 때문.

 

도메인 모델링 관점 -> 객체지향적 장점 유지

- 현실세계 개념을 자연스럽게 모델링

  Item이라는 공통 추상 개념을 코드로 표현해 Movie, Book, Album 등을 구체타입으로 설게할 수 있다.

  도메인 설계에서 상속을 쓰면 중복을 제거하면서, 타입 구조 자체가 코드로 드러나게 된다.

 

- 다형성을 그대로 활용 가능하다

  JPA는 엔티티 객체 그대로 핸들링하므로, "상위 타입으로 조회, 하위 타입으로 사용" 이라는 객체지향적 다형성을 DB에서도 쓸 수 있게된다.

List<Item> items = em.createQuery("select i from Item i", Item.class).getResultList();

위 EntityManager 쿼리 호출은 Movie, Book, Album이 모두다 items 리스트에 담겨지게 된다.

예를들어, Table per class 전략(서브클래스 당 테이블 1개씩 생성)을 쓰면 각 하위테이블에서 SELECT한 결과를 UNION all로 합쳐서 리턴해준다.

 

JPA/DB 관점 -> 데이터 관리 효율

- 다형성 쿼리 지원

  상속 매핑을 하지 않으면, Item이라는 추상 개념으로 한번에 쿼리를 날리는게 불가능며, Repository 여러개를 만들어다가 따로 쿼리한 뒤 

  Java 코드상에서 어떻게든 Merge하거나 Union을 해야한다. JPA 상속매핑을 사용하면 JPA가 이를 자동으로 처리해준다.

- 일관된 ID 관리

  상속 구조를 매핑하면 상위 엔티티의 PK가 하위 엔티티에서도 공유된다. 참조 무결성과 관계 매핑이 깔끔해진다.

  EX: Order -> Item 관계를 맺으면, 이 Item을 상속한 Book 이든 Movie든 다 연결할 수 있다.

- ORM 관점 투명성

  자바 객체 상속구조와 DB 테이블 구조가 불일치하면 JPA가 영속화/조회 시점에 변환 로직이 더 필요해지고 코드가 난잡해진다.

  상속 매핑을 쓰면 JPA가 변환 과정을 책임지므로 개발자가 좀더 비즈니스 로직에 집중할 수 있다.

 

결국, 상위 엔티티를 참조할 때 자연스럽게 하위 엔티티 전부를 자연스럽게 연결할 수 있다는게 장점이라는 소리다.


여기서, 지금까지의 실무 경험을 되짚어본다.

지금까지는 모두 Mybatis를 사용한 실무를 경험해왔다.

Mybatis는 직접 매핑할 객체를 지정할 수도 있고 (resultType), Map에 받아서 모조리 처리해버릴 수도 있다.(리턴타입을 보려면 .xml을 보거나 디버그를 한번은 돌려야 하는 불편함이 있지만).

 

이런 경험을 되짚어보면, Mybatis를 사용하는 프로젝트는 "DB중점(sql을 심혈을 기울여서 짜야함), 결과중심(뭐가 됐든 DTO든 엔티티든 Java 영역으로 들어왔으면 됐지)" 느낌이 강하다. JPA는 "Java 중심(어떻게든 Java를 최대한 활용해서 DB랑 연결하려함), 과정중심(Java의 제약을 모두 지키면서, Java의 성격 자체를 녹여내려는 느낌") 이 강한것같다.


그리하여,

 

상속 매핑

 

상속은 Java같은 객체지향 세계에서나 유효한, 다시말해 객체지향 세계와 관계형(RDB) 세계의 대표적인 구조 불일치 항목이다.

객체지향 모델은 is-a, has-a 관계를 제공하지만, SQL 기반 모델은 has-a 관계만 제공하기 떄문.

따라서, 특정 "전략"을 통해 상속 계층 구조를 RDB 세계에 표현해야 한다.

 

먼저, JPA에서 제공하는 전략을 사용하기 전, @MappedSuperclass 를 사용해

"상위 클래스의 필드/매핑 정보만 하위 엔티티에 올려주는" 기법/패턴을 확인해보자.

 

@MappedSuperclass를 활용한 암시적 다형성을 활용한 서브 클래스별 테이블

 

상위 클래스의 필드, 매핑 정보만 하위 엔티티에 물려주는 코드 재사용 장치인게 특징이다.

이 기법으로는 진정한 다형적 연관관계를 표현할 수 없음에 유의하자.

 

@MappedSuperclass인 슈퍼클래스는 엔티티로 취급받지 않으며(= 별도 테이블이 생성되지 않으며) 서브클래스는 별도 테이블을 생성하여 관리한다.

이는 곧, JPA가 제공하는 다형성 쿼리 대상이 될 수 없음을 의미한다.

 

또한 @MappedSuperclass를 슈퍼클래스에 지정하지 않으면, 슈퍼클래스의 프로퍼티가 무시되며 영속화되지 않는다.

 

상위클래스(@MappedSuperclass) 선언

여기서 클래스가 추상클래스(abstract) 임에 유의하자.

별도로 BillingDetails는 개념적인 추상화된 공통 부분을 추린것이다. 따라서 인스턴스화가 될 필요도 없고, 별도 테이블도 필요하지 않다.

이에 따라 abstract로 선언한것.

@MappedSuperclass
public abstract class BillingDetails {

    @Id
    @GeneratedValue(generator = "ID_GENERATOR")
    private Long id;

    @NotNull
    private String owner;

    protected BillingDetails() {
    }

    protected BillingDetails(String owner) {
        this.owner = owner;
    }

    public Long getId() {
        return id;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }
}

* @MappedSuperclass를 선언해두어야 슈퍼클래스의 프로퍼티가 서브클래스의 프로퍼티로 추가되어 영속화대상이 된다.

 

서브클래스 1(@Entity)

@Entity
public class BankAccount extends BillingDetails {
    @NotNull
    private String account;

    @NotNull
    private String bankname;

    @NotNull
    private String swift;

	// 기타 NoArgsConstructor, AllArgsConstructor, Getter, Setter..
   
}

 

 

서브클래스2(@Entity)

참고 : 슈퍼클래스의 프로퍼티 정보를 @AttributeOverride로 엎어칠 수 있다.

@Entity
@AttributeOverride(
        name = "owner",
        column = @Column(name = "CC_OWNER", nullable = false))
public class CreditCard extends BillingDetails {

    @NotNull
    private String cardNumber;

    @NotNull
    private String expMonth;

    @NotNull
    private String expYear;

	// 기타 NoArgsConstructor, AllArgsConstructor
    // 기타 getter, setter

}

 

이렇게 선언하면, @MappedSuperclass의 메서드와 프로퍼티가 서브클래스에도 포함되며, 프로퍼티 메타데이터도 함께 상속된다.

 

다만 이런경우, 위에서 서술했듯 @MappedSuperclass는 별도의 엔티티 취급을 받지 않으며, 단순히 Java 레벨에서만 상속관계가 유지되게 된다.

이말인 즉슨, JPQL 엔진은 @MappedSuperclass의 존재를 알지 못하기 때문에, 각 서브클래스 별 JPARepository를 별도로 생성해주어야 한다.

 

슈퍼클래스 Repository

@NoRepositoryBean
public interface BillingDetailsRepository<T extends BillingDetails, ID> extends JpaRepository<T, ID> {
	List<T> findByOwner(String owner);

}

 

 

도메인 모델이 슈퍼클래스를 확장/상속해 서브클래스를 구성했듯, Repository도 슈퍼클래스에 대한 repository를 만들어 상속 및 확장한다.

 

다만, BillingDetails 자체가 @MappedSuperclass 를 사용해 별도 테이블이 만들어지지 않았으므로,
JPA나 JPQL 엔진은 저 엔티티를 알지 못한다. 따라서 @NoRepositoryBean 어노테이션을 선언해, respotiroy bean으로 만들어지는것을 방지한다.

 

이제 실제로 사용할 서브클래스에 대한 리파지터리를 두개 만들어야 한다.

public interface BankAccountRepository extends BillingDetailsRepository<BankAccount, Long> {
    List<BankAccount> findBySwift(String swift);
}
public interface CreditCardRepository extends BillingDetailsRepository<CreditCard, Long> {
    List<CreditCard> findByExpYear(String expYear);
}

 

이렇게 하면, 코드에서 BillingDetails를 확장한 두개의 서브클래스를 활용할 수 있다.

    void storeLoadEntities() {

        CreditCard creditCard = new CreditCard("John Smith", "123456789", "10", "2030");
        creditCardRepository.save(creditCard);

        BankAccount bankAccount = new BankAccount("Mike Johnson", "12345", "Delta Bank", "BANKXY12");
        bankAccountRepository.save(bankAccount);

        List<CreditCard> creditCards = creditCardRepository.findByOwner("John Smith");
        List<BankAccount> bankAccounts = bankAccountRepository.findByOwner("Mike Johnson");
        List<CreditCard> creditCards2 = creditCardRepository.findByExpYear("2030");
        List<BankAccount> bankAccounts2 = bankAccountRepository.findBySwift("BANKXY12");
}

 

다만, 그 어디에도 BillingDetails로 엔티티들을 묶어서 처리하는 코드를 볼 수 없다.

@MappedSuperclass는 단순히 엔티티에 프로퍼티를 넘기는 역할만 할뿐, JPA와 연결하여 다형성을 누릴 수 없다.

즉 JPA 전략이 아닌, @MappedSuperclass로는 진정한 다형적 연관관계를 지원할 수 없다.

그렇다면, 진정한 다형적 연관관계를 지원하는 상속 관계 표현 방법에는 무엇이 있는가?

 

Union을 활용한 구체 클래스별 테이블 (@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS))

 

이 전략은 슈퍼클래스를 추상클래스(abstract)로 유지하며, 서브클래스 별로 테이블을 하나씩 만들어서 관리한다는 특징이 있다.

이 상황에서는 SQL 스키마는 상속을 인식하지 못하며, 슈퍼클래스의 테이블 역시 없다.

(TABLE_PER_CLASS는 슈퍼클래스의 테이블을 만들지 않는 전략임)

그러나, JPQL을 통해 상위 타입으로 조회하면 -> Hibernate가 이를 인식해서 UNION ALL이 포함된 SQL을 생성한다.

 

슈퍼클래스 예시코드 (@Entity @Inheritance abstract class) 선언

 

여전히 abstract 클래스임에 유의하자. 이유는 @MappedSuperclass와 동일!

다만 이제는 @Entity가 붙는다.

@Entity가 붙어서 엔티티 취급을 하지만 abstrat 클래스이므로 인스턴스화 불가능, -> JPA가 직접 저장/조회하지 못하게 됨

@Inheritance.TABLE_PER_CLASS -> 전략 상 abstract인 상위 테이블을 만들지 않는 설계임.

*만약 @Entity @Inheritance.TABLE_PER_CLASS public class BillingDetails (일반클래스) 라면 테이블이 생성되게 됨.

 

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BillingDetails {

    @Id
    @GeneratedValue(generator = "ID_GENERATOR")
    private Long id;

    @NotNull
    private String owner;

    protected BillingDetails() {
    }

    protected BillingDetails(String owner) {
        this.owner = owner;
    }

    public Long getId() {
        return id;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }
}

 

 

서브클래스 선언 (변화 없음)

@Entity
@AttributeOverride(
        name = "owner",
        column = @Column(name = "CC_OWNER", nullable = false))
public class CreditCard extends BillingDetails {

    @NotNull
    private String cardNumber;

    @NotNull
    private String expMonth;

    @NotNull
    private String expYear;

	// 기타 NoArgsConstructor, AllArgsConstructor
    // 기타 getter, setter

}

 

이렇게 되면, BillingDetailsRepository에서 다형성을 활용하여 1개의 리파지토리만으로 서브클래스를 한번에 조회해 올 수 있다.

이제부터는 직접 BillingDetailsRepository 가 DB와 상호작용할 것이므로 @NoRepositoryBean 어노테이션을 제거해야 한다.

public interface BillingDetailsRepository<T extends BillingDetails, ID> extends JpaRepository<T, ID> {
	List<T> findByOwner(String owner);

}

 

이렇게 활용하면, Hibernate가 상속 전략에 따 UNION ALL을 붙여서 발행하며, UNION ALL이 붙은채로 SQL이 발행된다.

SELECT ID, OWNER, EXPMONTH, EXPYEAR, CARDNUMBER,
  ACCOUNT, BANKNAME, SWIFT, CLAZZ_
FROM
 (
 	SELECT ID, OWNER, EXPMONTH, EXPYEAR, CARDNUMBER
    null as ACCOUNT, null as BANKNAME, null as SWIFT, 1 as CLAZZ_
    FROM CREDITCARD
    
    union all
    
    SELECT id, owner, ACCOUNT, BANKNAME, SWIFT
    null as EXPMONTH, null as EXPYEAR, null as CARDNUMBER, 2 as CLAZZ_
    FROM BANKACCOUNT
    ) as BILLINGDETAILS

 

모든 구체 클래스로 부터 BillingDetails의 인스턴스를 조회하며, 테이블은 UNION ALL로 결합된다.

이때 리터럴이 중간 결과에 삽입되며, 이를 활용해 하이버네이트가 특정 로우의 데이터로 올바른 클래스를 인스턴스화 한다.

** 유니온 연산을 위해서는 모든 쿼리가 동일한 컬럼을 출력해야 하므로, 인스턴스에 없는 칼럼은 NULL로 날아간다.

** 다만 이경우, 성능 (Union all + 정렬/페이징 시 성능 유의해야함) 을 케어해야 한다.

 

위 전략을 사용해 "다형적 연관관계" 매핑을 처리할 수 있고, SQL 스키마에 별도로 고민할 필요가 없이 개발할 수가 있다.

 

반면, 전략은 SQL 스키마에 대해 고민을 좀 할 필요가 있다.

 

클래스 계층 구조별 테이블 (@Inheritance(strategy = InheritanceType.SINGLE_TABLE))

 

이 전략은, 클래스 계층 전체를 하나의 테이블로 저장하는 방식이다.

즉, BillingDetails 슈퍼클래스 엔티티의 테이블에 모든 서브클래스(CreditCard, BankAccount)의 프로퍼티를 추가하는 방식이다.

이경우 DTYPE 구분으로 컬럼 구분을 추가해주어야 한다.

 

* 여전히 슈퍼클래스는 추상클래스다. 

하지만, interitanceType이 SINGLE_TABLE 이므로, 슈퍼클래스에 대한 테이블이 만들어진다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "BD_TYPE")
public abstract class BillingDetails {

    @Id
    @GeneratedValue(generator = "ID_GENERATOR")
    private Long id;

    @NotNull
    @Column(nullable = false)
    private String owner;

    protected BillingDetails() {
    }

    protected BillingDetails(String owner) {
        this.owner = owner;
    }

    public Long getId() {
        return id;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }
}

 

위 선언을 통해, BillingDetails가 테이블에 자동 매핑이 된다.

또한 @DiscriminatorColumn을 지정해야 하는데, 이 식별자 칼럼은 엔티티 프로퍼티가 아니라 하이버네이트 내부에서 사용할 값이다.

한 테이블에 여러 하위클래스의 데이터를 저장하기 때문에, 이 로우가 어느 구체클래스에 해당하는지를 알기 위한 값이며,

discriminator 컬럼에 클래스타입을 구분하는 값이 저장된다.

 @DiscriminatorColumn 의 인자값 name 은 컬럼명이다.

 

서브클래스1 (CreditCard)

@Entity
@DiscriminatorValue("CC")
public class CreditCard extends BillingDetails {

    @NotNull
    private String cardNumber;

    @NotNull
    private String expMonth;

    @NotNull
    private String expYear;

    // @NoArgs / @AllArgs Constructor 및 Getter, Setter

}

슈퍼클래스에서 선언한 @DiscriminatorColumn의 값에 "CC" 를 저장하게 된다.

 

서브클래스2(BankAccount)

@Entity
@DiscriminatorValue("BA")
public class BankAccount extends BillingDetails {
    @NotNull
    private String account;

    @NotNull
    private String bankname;

    @NotNull
    private String swift;
 
 // @NoArgs, @AllArgs constructor, Getter, Setter
}

마찬가지로, @DiscriminatorColumn 의 값에 BA를 저장한다.

 

이렇게 구성하면, BillingDetails 테이블에 서브클래스 스키마가 함께 매핑되며, 이 경우 서브클래스의 프로퍼티의 DDL의 경우 not null이 있어서는 안된다. @NotNull은 DDL 생성시에는 무시되지만, 런타임에서 로우를 삽입하기 전에 판단되므로 서브클래스 인스턴스를 만들때에 감시되도록 지정할 수 있다.

 

이렇게 구성한다면, BillingDetailsRepository 를 통해 전체 클래스 계층을 조회하거나,

별도의 CreditCardRepository를 통해 creditCard 타입의 구체 클래스만 참조할 수 있게된다.

billingDetailsRepository.findAll(); // -> BillingDetails 테이블을 모두 훑음

creditCardRepository.findAll(); // -> JPQL 쿼리로 select cc from CreditCard cc 를 발행할 수 있으며,
				// SQL 파싱 시 where BD_TYPE='CC' 가 붙어서 발행된다.

 

만약, 판별자 컬럼을 추가로 포함할 수 없는경우 (@DiscriminatorColumn 생성이 불가능한경우)

@DiscriminatorFormula 라는 네이티브 하이버네이트 기능을 사용할 수 있다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@org.hibernate.annotations.DiscriminatorFormula(
	"case when CARDNUMBER is not null then 'CC' else 'BA' end"
)
public abstract class BillingDetails {

    @Id
    @GeneratedValue(generator = "ID_GENERATOR")
    private Long id;

    @NotNull
    @Column(nullable = false)
    private String owner;

	// ...
}

 

매핑전략은 성능과 단순성 측면에서 모두 효과적이다. (조인/유니온이 필요 없어서 조회 성능이 좋다)

하지만, 인스턴스별로 없는 프로퍼티가 있을 수 있으므로, 하위 클래스의 프로퍼티에 대한 컬럼에 nullable을 허용해줘야 한다.

(프로퍼티 모든 컬럼이 하나의 테이블에 선언이 되며, 당 엔티티에 없는 프로퍼티의 값은 모두 NULL로 채워진다. 이로 인헤 테이블이 커질 수 있다.)

 

또한 이 매핑전략은 DB 정규화 중 3정규화를 위반할 수도 있다. 어플리케이션 레벨에서 의존을 생성하기 때문.

(기본키가 아닌 모든 속성이 기본키에 완전 함수 종속이어야 하며, 기본키가 아닌 모든 속성이 기본키에 이행적 함수 종속되지 않아야 한다.

즉, 기본키 X->Y, Y->Z (Z가 기본키 X가 아닌 값에 의해 결정된다.))

 

다음 전략은 이런 3정규화위반과 NULL인 컬럼이 많은 비정규화 경향 문제에 노출되지 않는 전략이다.

 

조인을 활용한 하위 클래스별 테이블 (@Inheritance(strategy = InheritanceType.JOINED)

 

이 전략은 상속구조를 테이블로 표현할 때 가장 정규화된 방식이다.

이 전략은 슈퍼클래스를 테이블로 구성하며, 슈퍼클래스의 프로퍼티는 슈퍼클래스의 테이블에 저장된다.

이후, 서브클래스의 프로퍼티는 서브클래스의 테이블에 저장이 되며, 슈퍼클래스의 PK를 서브클래스의 PK로 저장한다.

 

슈퍼클래스 선언

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class BillingDetails{
	@Id
    @GeneratedValue
    private Long id;
    
    @Notnull
    private String owner;
    
    //...

}

 

서브클래스 1 선언

@Entity
public class BankAccount extends BillingDetails{
	@NotNull
    private String account;
    
    @NotNull
    private String bankName;
    
    @NotNull
    private String swift;
    
    // ...
}

다른 전략과 마찬가지로 서브클래스에 식별자 선언이 없다. 

그러나, Inheritance 전략에 의해 자동으로 @Id 프로퍼티와 칼럼을 상속받으며, 하이버네이트가 자동으로 조인 전략을 세운다.

다만 @PrimaryKeyJoinColumn(name = "컬럼명") 으로 직접 컬럼명을 명시적으로 정할 수 도 있다.

 

서브클래스2 선언

@Entity
@PrimaryKeyJoinColumn(name = "CREDITCARD_ID")
public class CreditCard extends BillingDetails {

    @NotNull
    private String cardNumber;

    @NotNull
    private String expMonth;

    @NotNull
    private String expYear;

	// ...

}

 

위와 같이 선언한다면, 아래와 같이 활용이 가능하다.

billingDetailsRepository.findAll();
// -> JPQL로는 select bd from BillingDetails bd

// SQL로 파싱되면 외부조인을 발행한다.
/*
	select BD.id, BD.OWNER,
    CC.EXPMONTH, CC.EXPYEAR, CC.CARDNUMBER,
    BA.ACCOUNT, BA.BANKNAME, BA.SWIFT
    case
    	when CC.CREDITCARD_ID is not null then 1
        when BA.ID is not null then 2
        when BD.ID is not null then 0
    end
    FROM
    	BILLINGDETAILS BD
        LEFT OUTER JOIN CREDITCARD CC on BD.ID = CC.CREDITCARD_ID
        LEFT OUTER JOIN BANKACCOUNT BA on BD.ID=BA.ID
*/

// 혹은 특정 서브타입만 조회
// 서브타입의 repository를 활용해야 한다.
creditCardRepository.findAll();
// JPQL 기준 select cc from CreditCard cc

// 이경우 SQL 파싱되면 내부조인으로 변한다
/*
	SELECT creditcard_id, owner, expmonth, expyear, cardnumber
    FROM creditcard
    inner join BILLINGDETAILS on creditcard_id = id

*/

 

이 전략의 경우, 복잡한 클래스 계층구조에서는 성능이 저하될 수 있다. 쿼리를 실행할 때마다 여러 테이블에 대해 조인, 순차읽기가 발행되기 때문.

 

어떤 전략을 선택할지에 앞서서 상속 매핑 전략을 혼합해서 사용하는 방법도 존재함을 알아두고 넘어가자.

이는 슈퍼클래스에는 상속전략을 선언하고, 서브클래스 선언 시 @SecondaryTable을 선언해 전략을 조합하는 방법이다.

이때, 서브클래스에 선언한 @SecondaryTable에 table 명과 pkJoinColumns를 명시하고 서브클래스의 각 프로퍼티에 @Column(table = "")를 선언함으로써, 특정 서브클래스만 별도의 보조테이블에 구성해 외부조인으로 직접 가져오도록 변경할 수 있다.

 

이 매핑의 장점으로는, 슈퍼클래스 상속전략이 SINGLE_TABLE 일 경우, nullable이 될 수 있는 프로퍼티를 하나의 테이블로 뽑아낼 수 있다는 장점이 있다. 즉, 무결성이 확보된다.

 

단, 클래스 계층 구조의 폭이 굉-장히 넓은 경우 외부조인에서 문제가 발생할 수 있다. Oracle의 경우 외부조인 연산에서 사용 가능한 테이블 수를 제한하기도 하기 때문. 이런경우 다른 조회 전략으로 전환하는것이 좋다.

 

@Embeddable Class의 상속처리

@Embeddable 클래스는 엔티티의 컴포넌트이므로 (소유 일부), 일반적인 엔티티 상속 규칙이 적용되지 않는다.

단, 네이티브 하이버네이트 기능으로 상위클래스(혹은 인터페이스)로부터 일부 영속성 프로퍼티를 상속하는 @Embeddable 클래스를 매핑할 수 있다.

(@MappedSuperclass 를 @Embeddable 의 슈퍼타입으로 두는 방식이 하이버네이트 확장기능이다. JPA 표준에서는 적용이 안된다.)

 

예를 들어, Item 에 두가지 속성인 Dimension(크기) 와 Weight(무게) 를 생각해보자.

이때, 크기의 단위를 inch 와 cm 로 표현할 수도 있고, 무게도 lbs나 kg로 표현할 수 있다.

이런 측정 단위를 나타내기 위한 공통 속성 (이름, 단위기호) 를 나타내기 위해 Measurement 라는 상위 클래스를 추가로 도입해보자.

 

Measurement

@MappedSuperclass
public abstract class Measurement{
	@NotNull
    private String name;
    @NotNull
    private String symbol;
    
    // ...

}

Measurement 자체는 무게나 크기를 나타낼 때 단위 개념으로 쓰일것이며, 이때문에 abstract 에 @MappedSuperclass 로 선언한다

(이 자체가 테이블로 만들어지지는 않지만, 이 Measurement의 개념이 하위 엔티티 컬럼으로 사용되어야 함)

 

Weight

@Embeddable
@AttributeOverride(name = "name",
        column = @Column(name = "WEIGHT_NAME"))
@AttributeOverride(name = "symbol",
        column = @Column(name = "WEIGHT_SYMBOL"))
public class Weight extends Measurement {

    public static Weight kilograms(BigDecimal weight) {
        return new Weight("kilograms", "kg", weight);
    }

    public static Weight pounds(BigDecimal weight) {
        return new Weight("pounds", "lbs", weight);
    }

    @NotNull
    @Column(name = "WEIGHT")
    private BigDecimal value;

	// 기타 getter, setter, @NoArgs, @AllArgs, toString()
}

 

Dimensions

@Embeddable
@AttributeOverride(name = "name",
        column = @Column(name = "DIMENSIONS_NAME"))
@AttributeOverride(name = "symbol",
        column = @Column(name = "DIMENSIONS_SYMBOL"))
public class Dimensions extends Measurement {

    public static Dimensions centimeters(BigDecimal width, BigDecimal height, BigDecimal depth) {
        return new Dimensions("centimeters", "cm", width, height, depth);
    }

    public static Dimensions inches(BigDecimal width, BigDecimal height, BigDecimal depth) {
        return new Dimensions("inches", "\"", width, height, depth);
    }

    @NotNull
    private BigDecimal depth;

    @NotNull
    private BigDecimal height;

    @NotNull
    private BigDecimal width;

	// 기타 setter, getter, @NoArgs, @AllArgs constructor, toString()
}

 

이때, 프로퍼티 오버라이드를 통해 (@AttirubeOverride) 이름을 변경해 충돌을 방지한다.

이렇게 작성하면 Item @Entity에서 아래와 같이 사용할 수 있다.

@Entity
public class Item{
	@Embedded
	priavate Dimensions dimensions;
    @Embedded
    private Weight weight;
}

 

이때 주의할 점은, 추상 클래스타입 (예: Measurement) 의 프로퍼티를 엔티티에 포함시키면 절대 동작 안한다.

JPA 공급자는 Measurement 인스턴스를 어떻게 동작시켜야 할지 모르기 때문.

즉, @Embeddable 클래스가 @MappedSuperclass로 부터 일부 영속성 프로퍼티를 상속받게 할수는 있지만, 인스턴스에 대한 참조는 다형적이지 않다. 이유는 항상 구체 클래스의 이름을 정하기 때문.

 

전략 선택

그래서, 어떻게 어떤 기준 하에 매핑 전략을 선택해야할까?

 

- 다형적 연관관계나 다형적 쿼리가 굳이 필요하지 않은 경우 구체 클래스별 테이블 (TABLE_PER_CLASS) 사용

- 다형적 연관관계 또는 다형적 쿼리가 필요하고, 하위 클래스의 프로퍼티가 상대적으로 적은경우 SINGLE_TABLE

- 다형적 연관관계 또는 다형적 쿼리가 필요하고, 하위클래스 프로퍼티가 상대적으로 많은경우 JOINED. 비용에 따라서는 TABLE_PER_CLASS

 

다형적 연관관계?

다형성은 Java같은 객체지향 언어를 정의하는 특징중 하나.

다형적 연관관계나 다형적 쿼리를 지원하는것은 Hibernate 같은 ORM 솔루션의 기본기능이다.

 

예를들어, User 화 BillingDetails 의 관계를 생각해보자.

@Entity
@Table(name = "USERS")
public class User{
	@ManyToOne
    private BillingDetails defaultBilling
}

 *예시가 조금 이상한것같겠지만 넘어가자. 내 청구결제번호를 다른사람계정이 참조할수있다는 소리 아닌가?
 * @ManyToOne이 선언된 @Entity 가 Many 쪽이다.

 

위처럼 선언하면, Users 테이블은 이 관계를 나타내는 JOIN/FK 컬럼인 DEFAULTBILLING_ID 를 가진다.

또한, BillingDetails 는 추상클래스이므로 런타임에 연관관계가 서브클래스중 하나인 CreditCard 나 BankAccount 인스턴스를 참조해야 한다.

하이버네이트에서는 다형적 연관관계를 활성화 하기 위해 별도의 작업을 요구하지 않는다.

연관관계 대상 클래스가 @Entity 와 @Inheritance 로 매핑된경우 연관관계가 자연스럽게 다형적으로 만들어지기 때문.

@OneToOne 도 마찬가지다.

 

다형적 컬렉션

각 User가 billingDetails 컬렉션을 가지고 있다면 어떻게 될까?

한명의 사용자가 여러 BillingDetails 참조를 가질 수도 있다.

이를 양방향 일대다 연관관계로 매핑할 수 있다.

@Entity
@Table(name = "USERS")
public class User{
	@OneToMany(mappedBy="user")
    oruvate Set<BillingDetails> billingDetails = new HashSet<>();
}

 

여기에, billingDetails가 FK를 가지도록 선언한다.

(mappedBy가 선언된 쪽이 FK가 없이 참조되는쪽, mappedBy 없이 관계가 선언된 쪽이 테이블에서 FK를 가지게 된다)

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BillingDetails{
	@ManyToOne
    private User user;
}

 

이렇게만 선언해도, 자동으로 다형적 관계가 활성화가 된다.

다만, 다형성을 활용하기 위해서는 @MappedSuperclass가 될 수 없다. @Entity 와 @Inheritance를 사용해야 한다.

 

다음은 드디어 그 악명높은 컬렉션과 연관관계(1:N, N:M 매핑) 내용이다.

+ Recent posts