개발/JPA

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

highright96 2021. 12. 14.

JPA 스터디를 진행하며 작성한 글입니다.

 

다대일

다대일은 이전 글에서 다루었으니 간단하게만 짚고 넘어가겠다.

 

다대일 단방향 [N:1]

@Entity
public class Member {

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;
    private String name;
}

 

회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 반대로 팀에는 회원을 참조하는 필드가 없다. 따라서 회원과 팀은 다대일 단방향 연관관계이다.

 

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

@Entity
public class Member {
    //위 예제와 동일
}

public class Team {

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

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

 

팀도 Team.members로 회원 엔티티를 조회할 수 있다. 따라서 회원과 팀은 다대일 양방향 연관관계이다. 데이터베이스 테이블의 일(1), 다(N) 관계에서 외래 키는 항상 다쪽에 있다. 따라서 객체의 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

 

일대다

일대다 단방향 [1:N]

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    private String username;
}

public class Team {

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

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

 

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

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.

 

일대다 단방향 매핑의 단점

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다. 다른 테이블에 외래 키가 있으면 INSERT 쿼리뿐만 아니라 UPDATE 쿼리가 추가로 실행된다. 따라서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 권장한다.

 

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

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

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

@Entity
public class Team {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

 

일대다 양방향 매핑은 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가해서 일대다 양방향처럼 보이도록 하는 방법이다. 따라서 일대다 단반향 매핑이 가지는 단점을 그대로 가진다. 될 수 있으면 다대일 앙방향 매핑을 사용해야 한다.

 

일대일 [1:1]

일대일 관계는 양쪽이 서로 하나의 관계만 갖는다. 테이블은 주 테이블이든 대상 테이블이든 외래 키 하나만 있으면 양쪽으로 조회할 수 있다. 따라서 일대일 관계는 주 테이블이나 대상 태이블 중에 누가 외래 키를 가질지 선택해야 한다.

 

주 테이블에 외래 키

주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조한다.

 

단방향

@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을 사용했고 데이터베이스에는 LOCKER_ID 외래 키에 유니크 제약 조건(UNI)를 추가했다.

 

양방향

@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;
}

 

대상 테이블에 외래 키

대상 테이블에 외래 키를 두고 주 테이블을 참조한다. 테이블 관계를 일대일에서 일대다로 변경할 떄 테이블 구조를 그대로 유지할 수 있는 장점이 있다.

 

단방향

아래 그림과 같이 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 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;
}

 

다대다 [N:N]

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 일대일, 다대일 관계로 풀어내는 연결 테이블을 사용한다.

그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

 

다대다: 단방향

@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 ArrayList<>();

}

@Entity
public class Product {

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

 

회원 엔티티와 상품 엔티티를 @ManyToMany로 매핑했다. 여기서 중요한 점은 @ManyToMany와 @JoinTable을 사용해서 연결 테이블을 바로 매핑한 것이다. 따라서 회원_상품 엔티티 없이 매핑을 완료할 수 있다.

 

@JoinTable 속성

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

 

다대다: 양방향

@Entity
public class Member {
    //위 예제와 동일
}

@Entity
public class Product {

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

    private String name;

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

 

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

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

이런 상황이 발생하면 @ManyToMany 대신 연결 테이블을 매핑하는 연결 엔티티를 만들고 일대다, 다대일 관계로 설계하는 것이 좋다.

@Entity
public class Member {

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

    private String username;

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

@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
}

 

MemberProduct 엔티티를 보면 기본 키를 매핑하는 @Id와 외래 키를 매핑하는 @JoinColumn을 동시에 사용해서 기본 키와 외래 키를 한 번에 매핑했다. 그리고 @IdClass를 사용해서 복합 기본 키를 매핑했다.

복합 기본 키에 대해서는 추후에 설명하겠다.

 

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

연결 테이블에 새로운 기본 키를 사용할 수 있다. 회원상품(MemberProduct)을 주문(Order)이라는 이름으로 변경했다.

@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) 2022.01.08
[JPA] 고급 매핑  (0) 2021.12.21
[JPA] 연관관계 매핑 기초  (0) 2021.12.14
[JPA] 엔티티 매핑  (0) 2021.12.08
[JPA] 영속성 관리  (0) 2021.12.08

댓글