자바 ORM 표준 JPA 프로그래밍 - 기본편 : 연관관계 매핑 심화

jkky98·2024년 9월 30일
0

Spring

목록 보기
50/77

1대1 연관관계

1대1 연관관계의 경우 N:1 상황처럼 외래키의 위치가 고정되어있지 않다. 어느쪽에 외래키를 두어도 괜찮은 경우이며 외래키의 위치에 연관관계의 주인을 걸어주면 된다. 그렇다면 1대1 연관관계에서 중요한 것은 어느 엔티티에 JoinColumn을 선언하여 외래키를 설정하느냐에 대한 것이다.

Member(회원)엔티티와 Locker(보관함)엔티티가 존재하고 이 둘은 OneToOne 관계라고 가정할 때 두 테이블중 어느 곳에 (연관관계의 주인 + 외래키)를 세팅하는 것이 좋은지에 대해서는 Trade-Off가 존재한다.

비즈니스를 파악해보면 Member가 주 테이블이 되고 Locker가 대상 테이블이 된다.

이 Trade-Off에서 어플리케이션 개발자(객체지향적)와 DBA(데이터베이스 관리자가 각각 좋아하는 방향으로의 두 가지 키 세팅이 존재한다.

주 테이블에 외래키를 두는 것은 객체지향 개발자가 선호할 수 있다. 실제 어플리케이션 비즈니스 코드를 작성할 때 주 테이블을 조회할 경우가 많고 주 테이블에 대상 테이블(Locker)를 곧바로 다룰 수 있기 때문이다.

하지만 주 테이블에 연관된 대상 테이블이 존재하지 않는다면 주 테이블의 외래키는 null이 되기 때문에 null 때문에 고려해야할 점이 많아지는 DBA 입장에서는 이 부분이 까다로워진다.

또한 대상 테이블쪽에 외래키가 존재할 경우 1:1 테이블이 1:N 테이블로 확장될 때 대상 테이블(1 to N)에 외래키가 존재해야하므로 DBA 입장에서는 확장시 수정의 요소가 적은 대상 테이블 쪽에 외래키를 넣는 경우를 보통 선호한다.

  • 하지만 불확실한 미래에 대해 확장까지 예측해서 설계하기보다는 주 테이블에 외래키를 형성해서 비즈니스 로직상의 코드를 편하게 만드는 것이 중요하다고 생각한다.

N 연관관계 매핑

1:N, N:1, N:M 매핑에 관해 설명한다.

N:1, 1:N의 경우 N쪽에 외래키를 설정하는지 1쪽에 외래키를 설정하는지에 관련한 문제인데 이는 당연히 N에 설정해야 한다. JPA가 반대의 경우도 처리해주긴 하지만 기본적으로 복잡한 문제가 없을 N(외래키, 연관관계 주인):1의 방식으로 처리해야 한다.

N:M의 경우또한 JPA는 @ManyToMany를 지원하지만 이를 사용해도 결국 중개 테이블을 만들게 되고 해당 중개 테이블은 JPA가 자동 생성해주어 따로 중개 테이블에 컬럼을 추가할 수 없게 되어 유연성이 부족해진다. 그러므로 N:M 관계는 개발자가 직접적으로 1:N, N:1로 중개용 엔티티를 따로 만들어 매핑해주도록 한다.

외래키와 연관관계의 주인 세팅은 당연히 N이 위치한 중개용 엔티티에 설정해준다.

상속관계 매핑

데이터베이스는 객체와 달리 상속에 대한 개념이 존재하지 않기에 구조적인 디자인 기법인 슈퍼-서브타입으로 디자인한다면 JPA로 매핑할 수 있다.

슈퍼 타입에는 부모의 속성에 해당하는 공통적으로 가지는 필드들을 가지고, 서브 타입에는 자식 속성들을 열거한다.

Join 전략

@Inheritance(strategy = InheritanceType.JOINED)를 활용한다.

위와 같이

JOIN 방식으로 슈퍼타입-서브타입을 구현하는 방법으로 ITEM 테이블을 두고 외래키로 연결된 서브타입 구체 테이블을 두는 것이다. 이는 위의 나타난 어노테이션을 Item 클래스에 작성하고, 각각의 자식 클래스에 대해서 Item을 상속받기만 하면 매핑이 완료된다.

**이때 Dtype을 두어 해당 Item행이 어떤 구체화인지 나타내는 것이 좋으며 이는 @DiscriminatorColumn(name=“DTYPE”)을 Item 클래스에, @DiscriminatorValue(“XXX”)를 구체화 클래스들에 두어 Item 테이블에서 어떤 자식타입인지 알 수 있게끔 할 수 있다.

SingleTable 전략

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)을 사용하며 아예 애노테이션을 적지 않고 구현 클래스들에 대해 상속만 연결해주어도 SINGLE_TABLE이 디폴트로 적용된다.

single_table 전략은 dtype이 필수적이므로 @DiscriminatorColumn(name=“DTYPE”)@DiscriminatorValue(“XXX”)를 세팅해주지 않아도 dtype 전략이 자동 세팅된다.

singleTable의 경우 위와 같이 해당 dtype에 맞는 필드들에 데이터가 들어온다. 테이블 1개로 모두 표현이 가능하므로 일반적으로 JOIN전략보다 성능이 높다. 하지만 테이블 컬럼이 매우 많아지는 등의 임계점이 넘어갈 경우 JOIN전략보다 성능이 떨어질 수 있다.(이 한계점은 매우 높으니 일반적으론 JOIN전략보다 성능이 우수하고 조회 쿼리가 단순해진다.)

또다른 단점으로는 역시 null이 많아져 DBA에게는 껄끄러운 선택일지도 모른다.

구현 클래스마다 테이블 전략

부모타입의 테이블을 생성하지않고 부모타입의 속성들을 모든 자식 타입들에 대해서 집어넣어준다.

이 방식은 보통적으로 고려되지 않는다. 심지어 이 전략은 DBA와 ORM 개발자 모두 좋아하지 않는 방식이다. 여러 자식 테이블을 함께 조회할 때 성능이 느려지고 자식 테이블들을 통합한 쿼리가 어렵기 때문이다.

@MappedSuperclass

모든 테이블에 공통적으로 들어가는 필드들이 있다면 이를 모든 엔티티에 작성하는 것은 중복작업에 해당한다. 이를 @MappedSuperclass 애노테이션이 달린 클래스에 작성하고 이 클래스를 상속받아 공통된 필드에 대해서 매핑할 수 있다.

@MappedSuperclass
public class BaseEntity {

    private String createAt;

    public String getCreateAt() {
        return createAt;
    }

    public void setCreateAt(String createAt) {
        this.createAt = createAt;
    }
}

createAt이라는 엔티티가 생성되었을때의 시간을 기록하는 필드를 생각해보자. 이는 모든 엔티티에 적용되어야 하고 이를 private String createAt을 모든 엔티티에 적는 것보단 위와 같이 BaseEntity에 작성해서 이를 필요한 곳에서 상속받아 사용할 수 있다.

@MappedSuperclass는 엔티티가 아니기 때문에 테이블이 따로 생성되지 않는다. 단순히 반복 작성이 많은 필드에 대해서 한번에 처리가 가능하도록 하는 편의성 툴로 생각하자.

createAt과 같은 것을 적어주면 좋다고 생각하지만 실제로는 뒤에서 배울 수단으로하여금 이것보다 더 간단하게 공통 필드들을 엔티티들에 적용할 수 있다.

profile
자바집사의 거북이 수련법

0개의 댓글