스터디/JPA 스터디

[JPA] 다양한 연관 관계 매핑

제이온 (Jayon) 2021. 12. 11.

jpa-study에서 스터디를 진행하고 있습니다.

다대일

다대일 단방향 [N:1]

Untitled

 

N쪽에 해당하는 Member가 연관 관계의 주인으로 정해주면 된다. 반대의 경우는 아래에서 설명한다.

 

다대일 양방향 [N:1, 1:N]

 

  • 양방향은 외래 키가 있는 쪽이 연관 관계의 주인이다.
  • 양방향 연관 관계는 항상 서로를 참조해야 한다.

 

일대다

일대다 단방향 [1:N]

Untitled

 

일대다 단방향 관계는 팀 엔티티가 회원 엔티티의 외래 키를 관리하고 있다. 보통 자신이 매핑한 테이블의 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 외래 키를 관리한다.

 

  • 단점
    • 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다.
    • INSERT SQL 외에 UPDATE SQL이 추가로 발생한다.

 

일대다 단방향 매핑보다는 다대일 양방향 매핑을 고려해 보자.

 

일대다 양방향 [1:N, N:1]

Untitled

 

일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가한다.

 

@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();

}

 

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;

}

 

일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 추가하였다. 이때 일대다 단방향 매핑과 같은 TEAM_ID 외래키 컬럼을 매핑했다. 둘 다 같은 키를 관리하면 문제가 발생할 수 있으므로 다대일 쪽은 읽기 전용으로 만들었다. 이 방법은 사용하지 않는 것을 권장한다.

 

일대일 [1:1]

일대일 관계는 양쪽이 서로 하나의 관계만 갖는다. 테이블 관계에서 일대다, 다대일은 항상 N쪽이 외래 키를 가지지만, 일대일 관계는 주 테이블이나 대상 테이블 중 어느 곳이나 외래 키를 가질 수 있다.

 

  • 주 테이블에 외래 키
    • 주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 참조한다.
    • 객체 지향 개발자들이 선호하는 방식이다.
  • 대상 테이블에 외래 키
    • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있는 경우가 많다. 가령, Member가 Locker를 하나씩 가질 수 있는 규칙이 바뀌어서 Lock를 여러 개 가질 수 있도록 만들 수 있다. 이때 Lock이 외래 키를 관리하면 규칙이 변경되어도 테이블 구조는 바뀌지 않는다.
    • 전통적인 데이터베이스 개발자들이 선호하는 방식이다.

 

주 테이블에 외래 키

단방향

 

@Entity
public class Member {

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

    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

}

 

@Entity
public class Locker {

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

    private String name;

}

 

데이터베이스에는 LOCKER_ID 외래 키에 대해 유니크 제약 조건을 추가해야 한다. 물론 JPA 단에서 DDL을 수행할 때 자동으로 유니크 제약 조건을 걸어 주는 방법도 있다. @JoinColumn의 속성으로 unique 옵션을 true로 활성화하거나 @Table의 속성으로 unique 제약 조건을 추가해 주면 된다. unique 옵션이 여러 개 추가될 수 있으므로 후자의 방법을 추천한다.

 

양방향

 

@Entity
public class Member {

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

    private String username;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

}

 

@Entity
public class Locker {

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

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;

}

 

양방향이므로 연관 관계의 주인을 Member로 정했고, Locker는 mappedBy 를 이용하여 Locker.member 가 연관 관계의 주인이 아니라고 설정하였다.

 

대상 테이블에 외래 키

단방향

Untitled

 

일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 이때는 단방향 관계를 Locker에서 Member 방향으로 수정하거나, 양방향 관계로 만들고 Locker를 연관 관계의 주인으로 설정해야 한다.

 

양방향

 

@Entity
public class Member {

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

    private String username;

    @OneToOne(mappedBy = "member")
    private Locker locker;

}

 

@Entity
public class Locker {

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

    private String name;

    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

}

 

참고로 위와 같이 설계하면 Locker.member 는 지연 로딩이 가능하지만, Member.locker 은 지연 로딩이 불가능하다. 이 개념은 추후 프록시와 지연 로딩 파트에서 알아 보자.

 

다대다 [N:N]

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없고, 중간에 연결 테이블을 추가해야 한다. 하지만 객체는 객체 2개로 다대다 관계를 만들 수 있다. 여기서 패러다임 불일치를 @ManyToMany로 해결할 수 있다.

 

다대다: 단방향

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
                    joinColumns = @JoinColumn(name="MEMBER_ID"),  
                    inverseJoinColumns = @JoinColumn(name="PRODUCT_ID")
                    )
    private List<Product> products = new ArrayLit<>();

}

 

@Entity
public class Product {

    @id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

}

 

@JoinTable 속성

  • name
    • 연결 테이블을 지정한다.
  • joinColumns
    • 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.
  • inverseJoinColumns
    • 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

 

다대다: 양방향

Member 코드는 그대로 두고, Product 코드를 수정한다.

 

@Entity
public class Product {

    @id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

    @ManyToMany(mappedBy = "products")
    private List<Member> members;

}

 

다대다: 매핑의 한계와 극복, 연결 엔티티 사용

@ManyToMany를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편하다. 하지만 회원이 상품을 주문하면 연결 테이블에 단순히 주문한 회원 아이디와 상품 아이디만 담고 끝나지 않는다. 보통은 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다.

 

Untitled

 

이렇게 새로운 컬럼이 필요하면 @ManyToMany를 사용할 수 없다. 그래서 연결 테이블을 매핑하는 연결 엔티티를 만들고 1:N, N:1 관계로 설계하는 것이 바람직하다.

 

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> products = new ArrayLit<>();

}

 

@Entity
public class Product {

    @id
    @Column(name = "PRODUCT_ID")
    private String id;

    private String name;

}

 

회원 엔티티는 회원 상품 엔티티와 연관 관계를 맺어 주고, 상품 엔티티와의 관계는 해제하였다.

 

@Entity
@IdClass(MemeberProductId.class)
public class MemberProduct {

    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @Id
    @ManyOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private int orderAmount;

}

public class MemberProductId implements Serializable {

    private String member; // MemberProduct.member와 연결

    private String product; // MemberProduct.product와 연결

    // hashCode and equals 재정의

}

 

회원 상품 엔티티를 보면 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용하여 기본 키와 외래 키를 한 번에 매핑했다. 또한, @IdClass를 사용해서 복합 기본 키를 매핑했다.

 

복합 기본 키

회원상품 엔티티는 기본 키가 2개 존재하는 복합 키 형태이다. 이를 구현하려면 @IdClass를 사용해서 식별자 클래스를 지정할 수 있다. 자세한 식별자 클래스 활용법은 추후 설명한다.

 

다대다: 새로운 기본 키 사용

Untitled

 

권장하는 기본 키 생성 전략은 데이터베이스에 위임하는 것이다. 위와 같이 회원 엔티티와 상품 엔티티에 대해 대리 키를 만들고, 주문 (위에서 보았던 회원 상품) 엔티티도 대리 키를 만들어 주면 된다. 회원 엔티티와 상품 엔티티는 기존 코드에서 ID만 대리 키 방식으로 변경하면 되니, 주문 엔티티만 살펴 보자.

 

@Entity
public class Order {

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

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private int orderAmount;

}

 

대리 키를 사용함으로써 복합 키 방식보다 훨씬 간단해졌다. 이처럼 새로운 기본 키를 할당해서 다대다 관계를 풀어내는 것이 좋다.

 

출처

김영한 - 자바 ORM 표준 JPA 프로그래밍

'스터디 > JPA 스터디' 카테고리의 다른 글

[JPA] 프록시와 연관 관계 관리  (0) 2021.12.25
[JPA] 고급 매핑  (0) 2021.12.15
[JPA] 연관 관계 매핑 기초  (0) 2021.12.09
[JPA] 엔티티 매핑  (0) 2021.12.04
[JPA] 영속성 관리  (0) 2021.11.28

댓글

추천 글