07. 고급 매핑

zwundzwzig·2023년 4월 12일
0
post-thumbnail

상속 관계 매핑

RDBMS에선 상속이 없는 대신 슈퍼타입-서브타입 관계 Super-Type Sub-Type Relationship가 있다. ORM의 상속 관계 매핑은 이를 객체의 상속 구조와 매핑하는 것이다. 다음 세 가지 방법이 있다.

조인 전략

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모의 기본 키를 받아 외래키와 매핑해 사용한다.

조회할 때 많이 사용한다.

객체와 달리 테이블엔 타입의 개념이 없기 때문에, 타입을 구분하는 컬럼을 추가해야 한다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략을 사용하겠다는 의미
@DiscriminatorColumn(name = "DTYPE") // 부모 클래스에 구분 컬럼 지정, 기본값이 DTYPE
public abstract class Item {
	
    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id; 
    ....
    
}

@Entity
@DiscriminatorValue("M") // 구분 컬럼에 입력할 값 지정
@PrimaryKeyJoinColumn(name = "MOVIE_ID") // ID 재정의
public class Movie extends Item {
	
    private String director;
    private String actor;
    ....
    
}

조인 전략의 장점은

  • 테이블이 정규화되고
  • 외래 키 참조 무결성 제약 조건을 활용할 수 있으며
  • 저장 공간을 효율적으로 사용할 수 있다.

단점으로는

  • 잦은 조인으로 성능이 저하될 수 있고
  • 조회 쿼리가 복잡하며
  • 데이터를 등록할 INSERT 문을 두 번 실행해야 한다.

JPA 표준 명세에는 구분 컬럼을 명시하도록 하지만, 하이버네이트를 포함한 몇몇 구현체에선 구분 컬럼이 생략 가능하다.

단일 테이블 전략

하나의 통합 테이블로 변환하는 방식을 말한다.

위 코드와 비슷한데, @Inheritance(strategy = InheritanceType.SINGLE_TABLE)로 지정해주고 자식 엔티티 매핑 컬럼은 null을 허용해줘야 한다.

단일 테이블 전략의 장점은

  • 조인이 필요없어 조회 성능이 가장 빠르고,
  • 조회 쿼리가 단순하다.

단점으로는

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 하는 불편함이 있고
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커져 오히려 성능이 저하될 수 있다.

구분 컬럼이 필수이며 만약 지정하지 않을 경우 엔티티 이름이 기본적으로 사용된다.

이외에도 서브타입마다 하나의 테이블로 변환하는 방식인 구현 클래스마다 테이블 전략@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)이 있지만, SQL에 UNION을 사용하는 등 성능 저하에 문제가 있어 추천하지 않으시댄다.

@MappedSuperclass

자식 클래스만 테이블과 매핑하고 이들이 상속하는 부모 클래스를 활용하고 싶을 때 사용한다. 추상클래스 개념과 비슷하고, 즉 매핑 정보를 상속할 목적으로만 사용한다.

@MappedSuperclass // 응 난 테이블이랑 매핑 안해~ 내 자식들이랑 매핑하던가 해 난 클래스야~
public abstract class BaseEntity {
	@Id @GeneratedValue
    private Long id;
    private String name;
} 

@Entity
@AttributeOverrides({
  @AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
  @AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
}) // 부모에게 물려받은 매핑 정보를 변경 가능하고, 이렇게 복수가 아니라 단일로도 가능하다.
public class Member extends BaseEntity {...}

복합 키와 식별 관계 매핑

테이블 사이에 관계는 외래키가 기본 키에 포함되는지에 따라 식별 관계와 비식별 관계를 구분할 수 있다.

식별관계 : 부모 테이블의 기본 키를 받아 자식 테이블의 기본키 + 외래 키로 사용하는 관계
비식별관계 : 부모 테이블의 기본 키를 받아 자식 테이블의 외래 키로만 사용하는 관계

여기서 비식별 관계는 외래 키에 NULL 허용 여부에 따라 또 둘로 나뉜다.

  • 필수적 비식별 관계 Mandatory : NULL 허용 X, 연관관계를 필수로 맺어야 함
  • 선택적 비식별 관계 Optional : NULL 허용. 선택적 연관관계.

책에서는 필수적 비식별 관계를 추천하는데, NULL을 허용하는 선택적 비식별 관계는 외부 조인을 사용해야 하기 때문이다.

참고로, 복합키에는 @GeneratedValue를 사용할 수 없고, 복합키를 구성하는 여러 컬럼에서도 사용할 수 없다.

복합키 : 비식별 관계 매핑

식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 거기서 equals, hashCode를 구현해야 한다.

JPA에서 2가지 방법을 활용해 볼 수 있다.

@IdClass

RDBMS에 특화된 방식이다.

@Entity
@IdClass(ParentId.class)
public class Parent {

	@Id @Column(name = "PARENT_ID_FIRST")
    private String first;

	@Id @Column(name = "PARENT_ID_SECOND")
    private String second;
    
}

public class ParentId implements Serializable {

	private String first; private String second; // 각각 Parent 클래스 아이디와 매핑
    
    public ParentId() {
    }
    
    public ParentId(String first, String second) {
    	this.first = first;
        this.second = second;
    }
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}

}

@Entity
public class Child {

	@Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID_FIRST",
        	referencedColumnName = "PARENT_ID_FIRST"),
        @JoinColumn(name = "PARENT_ID_SECOND",
        	referencedColumnName = "PARENT_ID_SECOND"),
    })
	private Parent parent;
}

여기서, 식별자 클래스(ParentId)의 속성명과 엔티티(Parent)에서 사용하는 식별자의 속성명이 같아야 한다.

Parent 엔티티의 기본 키 컬럼이 복합 키이므로 Child 테이블의 외래 키도 복합 키이다.

따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns을 사용하는데, JoinColumn의 name과 referencedColumnName이 같으면 생략 가능하다.

@EmbeddedId

객체 지향적인 방법이다.

@Entity
public class Parent {

	@EmbeddedId 
    private ParentId id;
    ...
    
}

@Embeddable
public class ParentId implements Serializable {

	@Column(name = "PARENT_ID_FIRST")
	pravate String first; 
    @Column(name = "PARENT_ID_SECOND")
    pravate String second;
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}

}

식별자 클래스에 기본 키를 직접 매핑한다. 보다 객체지향적이고 중복이 없어 좋아보이지만, JPQL이 길어질 수 있는 단점이 존재한다.

복합키 : 식별 관계 매핑

@IdClass

@Entity
public class Parent {

	@Id @Column(name = "PARENT_ID")
    private String id;
   ...

}

@Entity
@IdClass(ChildId.class)
public class Child {

	@Id @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @Id @Column(name = "CHILD_ID")
    private String childId;
    ...

}

public class ChildId implements Serializable {

	private String parent; // Child.parent 매핑
    private String childId; // Child.childId 매핑

	@Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

@Entity
@IdClass(GrandChildId.class)
public class GrandChild {

	@Id @ManyToOne
    @JoinColumns({
      @JoinColumn(name = "PARENT_ID"),
      @JoinColumn(name = "CHILD_ID")
    })
    public Child child;
    
    @Id @Column(name = "GRANDCHILD_ID")
    private String id;
    ...

}

public class GrandChildId implements Serializable {

	private Child child; // GrandChild.child 매핑
    private String id; // GrandChild.id 매핑

	@Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

식별 관계는 기본 키와 외래 키를 같이 매핑해야 하기 때문에 @ManyToOne 사용하면 된다.

@EmbeddedId

@Entity
public class Parent {

	@Id @Column(name = "PARENT_ID")
    private String id;
   ...

}

@Entity
public class Child {

	@EmbeddedId
    private ChildId id;
    
    @MapsId("parentId") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    ...

}

@Embeddable
public class ChildId implements Serializable {

	private String parentId; // @MapsId("parentId")로 매핑
    
    @Column(name = "CHILD_ID")
    private String id;

	@Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

@Entity
public class GrandChild {

	@EmbeddedId
    private GrandChildId id;

	@MapsId("childId") // GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
      @JoinColumn(name = "PARENT_ID"),
      @JoinColumn(name = "CHILD_ID")
    })
    public Child child;
    ...

}

@Embeddable
public class GrandChildId implements Serializable {

	private ChildId childId; // @MapsId("childId")로 매핑
    
    @Column(name = "GRANDCHILD_ID")
    private String id;

	@Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

외래키와 매핑한 연관관계를 기본키에도 매핑하기 위해 @Id 대신 @MapsId를 사용한다. @MapsId의 속성값은 @EmbeddedId를 사용한 식별자 클래스(ChildId)의 기본 키 필드를 지정하면 된다.

식별, 비식별 관계의 장단점

RDMBS 관점에서 비식별 관계의 장점은

  • 식별 관계는 2개 이상의 컬럼을 합해 복합 기본 키를 만들어야 하는 경우가 많고, 상속될수록 더 많은 수의 기본 키 컬럼이 생기기 때문에 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 비식별 관계의 기본키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용하기 때문에 낮은 비즈니스 의존성을 갖는다.
  • 비식별 관계는 테이블 구조가 유연하다.

객체지향 관점에서 비식별 관계의 장점은

  • 위에서 봤듯 복합 키를 위한 클래스를 만들 필요가 없다.
  • @GeneratedValue 같은 어노테이션으로 간편하게 구현할 수 있다.

식별 관계의 장점은 상속을 통해 특정 상황에서 자손 테이블로 부모 테이블의 정보를 검색할 수 있다는 점이다.

조인 테이블

객체와 테이블을 연결할 때 조인 컬럼은 @JoinColumn, 조인 테이블은 @JoinTable을 활용한다.

@JoinTable은

  • name : 매핑할 조인 테이블 이름
  • joinColumns : 현재 엔티티를 참조하는 외래키
  • inverseJoinColumns : 반대 방향 엔티티를 참조하는 외래키

일대일 조인 테이블

@Entity
public class Parent {

	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    @OneToOne
    @JoinTable(name = "PARENT_CHILD",
    	joinColumns = @JoinColumn(name = "PARENT_ID"),
        inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private Child child;
    ...

}

@Entity
public class Child {

	@Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    ...

}

일대다 조인 테이블 (단방향)

@Entity
public class Parent {

	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    @OneToMany
    @JoinTable(name = "PARENT_CHILD",
    	joinColumns = @JoinColumn(name = "PARENT_ID"),
        inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private List<Child> child = new ArrayList<Child>();
    ...

}

@Entity
public class Child {

	@Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    ...

}

다대일 조인 테이블 (양방향)

@Entity
public class Parent {

	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    @OneToMany(mappedBy = "parent")
    private List<Child> child = new ArrayList<Child>();
    ...

}

@Entity
public class Child {

	@Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    
    @ManyToOne(optional = false)
    @JoinTable(name = "PARENT_CHILD",
    	joinColumns = @JoinColumn(name = "CHILD_ID"),
        inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
    )
    private Parent parent; 
    ...

}

다대다 조인 테이블

다대다를 만들려면 조인 테이블에 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 한다.

@Entity
public class Parent {

	@Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    @ManyToMany
    @JoinTable(name = "PARENT_CHILD",
    	joinColumns = @JoinColumn(name = "PARENT_ID"),
        inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private List<Child> child = new ArrayList<Child>();
    ...

}

@Entity
public class Child {

	@Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    ...

}

만약 조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없고 새로운 엔티티를 만들어 조인 테이블과 매핑해야 한다.

엔티티 하나에 여러 테이블 매핑

@SecondaryTable 어노테이션으로 한 엔티티에 여러 테이블을 매핑할 수 있다. (잘 사용 x)

@Entity
@Table(name = "BOARD") // BOARD 테이블과 매핑
@SecondaryTable(
	name = "BOARD_DETAIL",
	pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID")
) // 추가로 BOARD_DETAIL 테이블 매핑
public class Board {

	...
    @Column(table = "BOARD_DETAIL") // BOARD_DETAIL 테이블의 컬럼 매핑
    private STring content;

}

🧷 참조 교재

  • 김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)
profile
개발이란?

0개의 댓글