Book Review

자바 ORM 표준 JPA 프로그래밍 - 고급 매핑

빈코 2022. 11. 6. 16:15

이전 포스팅에서는 JPA의 다양한 연관관계에 대해 알아보았다. 오늘은 상속관계 매핑과 조인 테이블 등 JPA에서 제공하는 고급 매핑에 대해 알아보려고 한다😄

 

로고
고급 매핑

 

개요

서두와 같이 이번 포스팅에서는 객체의 상속 관계를 데이터베이스에 어떻게 매핑하는지, 등록일·수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보 상속 방법, 연결 테이블을 매핑하는 조인 테이블 등 다양한 고급 매핑에 대해 알아보려고 한다.

 

상속 관계 매핑📔

슈퍼타입 서브타입
슈퍼 타입 서브 타입

 

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다. 대신에 위 그림과 같이 슈퍼 타입 서브타입 관계(Super-Type Sub-Type Relationship)라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.

 

슈퍼 타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다

 

  • 각각의 테이블로 변환 : 각각을 모두 테이블로 만들고 조회할 때 조인을 사용한다. JPA에서는 조인 전략이라 한다.
  • 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합한다. JPA에서는 단일 테이블 전략이라 한다.
  • 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략이라 한다.

조인 전략 📗

조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 따라서 조회할 때 조인을 자주 사용한다.

 

이 전략을 사용할 때 주의할 점이 있는데 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 칼럼을 추가해야 한다. 여기서는 DTYPE 칼럼을 구분 칼럼으로 사용할 예정이다.

 

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

 

상속 매핑은 부모 클래스에 @Inheritance 어노테이션을 사용해야 한다. 그리고 매핑 전략을 지정해야 하는데 여기서는 조인 전략을 사용하기 때문에 JOINED로 지정하였다. 또한, 부모 클래스에 구분 컬럼을 지정해야 하므로@DiscriminatorColumn을 사용해서 자식 테이블을 구분 지을 수 있게 하였다.

 

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
	
    private String director; // 감독
    private String actor; // 배우
}

 

자식 클래스에서는 @DiscriminatorValue 어노테이션을 통해 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 위 코드와 같이 영화 엔티티를 저장하면 구분 칼럼인 DTYPE에 값 M이 저장된다.

 

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ID 재정의
public class Book extends Item {
	
    private String author; // 작가
    private String isbn; // ISBN
}

 

기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 칼럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn을 사용하면 된다.

 

조인 전략의 장점으로는 테이블이 정규화되고, 저장공간을 효율적으로 사용할 수 있지만 단점으로는 조회 쿼리가 복잡하며, 조인이 많이 사용되므로 성능이 저하될 수 있다.


단일 테이블 전략📙

단일 테이블 전략은 이름 그대로 테이블을 하나만 사용한다. 그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할 때 조인을 사용하지 않기 때문에 일반적으로 가장 빠르다😁

 

이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다. 예를 들어 Book 엔티티를 저장하면 ITEM 테이블의 Book 관련된 필드만 사용하고 다른 엔티티와 매핑된 ARTIST, ACTOR 필드는 사용하지 않으므로 null이 입력되기 때문이다.

 

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

@Entity
@DiscriminatorValue("A")
public class Album extends Item {...}

...

 

InheritanceType.SINGLE_TABLE로 지정하면 단일 테이블 전략을 사용한다. 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다. 장점으로는 조인이 필요 없으므로 일반적으로 조회 성능이 가장 빠르고 쿼리 또한 단순한다.

 

단점으로는 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있으며 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

 

@MappedSuperClass📒

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 받는 자식 클래스에게 매핑 정보만 제공하고 싶다면 @MappedSuperclass를 사용하면 된다. 이것은 추상 클래스와 비슷한데 @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과 매핑되지 않는다. 이것은 단순히 매핑 정보를 상속할 목적으로만 사용된다.

 

관계도
관계도

 

위 사진과 같이 회원(Member)판매자(Seller)는 서로 관계가 없는 테이블과 엔티티다. 테이블은 그대로 두고 객체 모델의 id, name 두 공통 속성을 부모 클래스로 모으고 객체 상속 관계를 만들어 볼 예정이다.

 

@MappedSuperclass
public abstract class BaseEntity {
	
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    ...
}

@Entity
public class Member extends BaseEntity {
	
    // ID 상속
    // NAME 상속
    
    private String email;
    ...
}

@Entity
public class Seller extends BaseEntity {
	
    // ID 상속
    // NAME 상속
    
    private String shopName;
    ...
}

 

BaseEntity에는 객체들이 주로 사용하는 공통 매핑 정보를 정의했다. 그리고 자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려받았다. 여기서 BaseEntity는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 된다. 따라서 @MappedSuperclass를 사용하였다.

 

부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides@AttributeOverride를 사용하고, 연관관계를 재정의하려면 @AssociationOverrides@AssociationOverride를 사용한다.

 

@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
public class Member extends BaseEntity {...}

 

부모에게 상속받은 id 속성의 컬럼명을 MEMBER_ID로 재정의했다. 둘 이상의 재정의하려면 @AttributeOverrides를 사용하면 된다.

 

📌 정리하자면 @MappedSuperclass는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 할 뿐이다. 이 기술을 사용하면 등록일자, 수정 일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다😁

 

조인 테이블📚

조인 테이블
조인 테이블

 

위 사진은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리한다. 조인 칼럼을 사용하는 방법은 단순히 외래 키 칼럼만 추가해서 연관관계를 맺지만 조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인 테이블(MEMBER_LOCKER)을 추가하고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리한다. 따라서 MEMBER와 LOCKER에는 연관관계를 관리하기 위한 외래 키 칼럼이 없다.

 

조인 테이블 데이터
조인 테이블 데이터

 

위 사진을 보면 회원과 사물함 데이터를 각각 등록했다가 회원이 원할 때 사물함을 선택하면 MEMBER_LOCKER 테이블에만 값을 추가하면 된다. 하지만 조인 테이블의 가장 큰 단점은 테이블을 하나 추가해야 한다는 점이다. 

 

따라서, 관리해야 하는 테이블이 늘어나고 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야 한다. 이러한 단점 때문에 기본은 조인 칼럼 방식을 사용하고 필요하고 판단이 될 때만 조인 테이블을 사용하는 것이 좋다😄

 

📌 조인 테이블은 연결 테이블, 링크 테이블이라고도 불리며 주로 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 사용한다.


일대일 조인 테이블📗

일대일
일대일 조인 테이블

 

일대일 관계를 만들려면 조인 테이블의 외래 키 칼럼 각각에 총 2개의 유니크 제약조건을 걸어야 한다. (PARENT_ID는 기본 키이므로 유니크 제약조건이 걸려 있다)

 

// 부모
@Entity
public class Parent {
	
    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    private String name;
    
    @OneToOne(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;
    
    private String name;
    ...
}

 

@JoinTable 어노테이션 안의 name 속성은 매핑할 조인 테이블 이름을 지정하고, joinColumns 속성은 현재 엔티티를 참조하는 외래 키, inverseJoinColumns 속성은 반대 방향 엔티티를 참조하는 외래 키를 지정하면 된다.

 

📌 양방향으로 매핑하려면 Child 클래스에 mappedBy 속성을 사용하면 된다.


일대다 조인 테이블📕

일대다
일대다 조인 테이블

 

사진과 같이 일대다 관계를 만들려면 조인 테이블의 컬럼 중 다(N)와 관련된 칼럼인 CHILD_ID에 유니크 제약조건을 걸어야 한다.(CHILD_ID는 기본 키이므로 유니크 제약조건이 걸려 있다)

 

// 부모
@Entity
public class Parent {
	
    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    
    private String name;
    
    @OneToMany(name = "PARENT_CHILD",
    		joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
    private List<Child> child = new ArrayList<>();
    ...
}

// 자식
@Entity
public class Child {
	
    @Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    
    private String name;
    ...
}

 

사실 일대일 매핑과 다른 점은 @OneToMany를 사용했다는 점과 List 형식으로 필드가 바뀌었다는 점 말고는 없다😄 이 밖의, 다대일 매핑·다대다 매핑 또한 매핑하는 어노테이션만 다를 뿐 코드는 똑같이 작성되기 때문에 생략하겠다.

 

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

마치며

지금까지 JPA에서 제공하는 상속 매핑, @MappedSuperclass, 조인 테이블을 사용하는 방법을 알아보았다. 다음 포스팅은 프록시와 연관관계 관리에 대해 알아볼 예정이다😃

반응형