본문 바로가기
  • 실행력이 모든걸 결정한다
Spring 사전 준비/JPA Hibernate

[JPA] 일대일 매핑과 다대다 매핑

by 김코더 김주역 2022. 7. 26.
반응형

1. 일대일 매핑

1) 특징

- 둘 중 어느 곳에나 외래 키를 가질 수 있다.

- 객체 매핑에 @OneToOne 어노테이션을 사용하고 DB의 외래 키에는 유니크 제약 조건을 건다.

- 예시로 직원과 사물함의 관계가 있다.

 

 

2) 외래 키 전략

(1) 주 테이블에 외래 키를 두는 방법

- 외래 키를 객체 참조와 비슷하게 사용할 수 있음

 

(2) 대상 테이블에 외래 키를 두는 방법

- 테이블 관계가 일대일에서 일대다로 변경되어도 테이블의 구조를 유지하기 쉬움

- 단방향으로는 불가능한 전략이기 때문에 양방향으로 서로 참조하고 있어야 한다.

 

 

 

2. 다대다 매핑

1) 특징

- 일반적으로 연결 엔티티를 추가하여 일대다/다대일 관계로 분리한다.

- 결론부터 말하자면, [4) 연결 엔티티에 새로운 기본 키를 부여하는 방식] 전략을 사용하는 것이 제일 유연하고 깔끔하다. 그 이유는 2), 3)을 읽어보면서 이해하는 것을 권장한다.

 

 

2) @ManyToMany 방식

- 실무에서 사용하기에는 한계가 있지만 사용법 정도는 가볍게 짚고 넘어가보자.

- 회원과 상품을 다대다 관계의 예시로 들어보자. 하나의 회원은 여러 상품을 주문할 수 있고, 하나의 상품(종류)는 여러 회원들에 의해 주문된다.

 

<Member.class>

@Entity
public class Member {
   
   @Id @Column(name="MEMBER_ID")
   private String 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<Product>();
   
   ...
}

- 여기서 @JoinTable은 연결 테이블을 생성하기 위한 어노테이션으로, 각 속성의 의미는 다음과 같다.

  • name : 연결 테이블의 이름
  • joinColumns : 현재 방향에서 매핑할 @JoinColumn 정보를 지정
  • inverseJoinColumns : 반대 방향에서 매핑할 @JoinColumn 정보를 지정

 

<Product.class>

@Entity
public class Product {
    @Id @Column(name="PRODUCT_ID")
    private String id;
    
    private String name;
    
    @ManyToMany(mappedBy="products")
    private List<Member> members;
    
    ...
}

 

- 이런식으로 @ManyToMany를 사용하여 연결 테이블을 자동으로 만들도록 할 수는 있다. 하지만, 연결 테이블에는 양 쪽 테이블의 기본키 외에도 추가적인 컬럼이 필요한 경우가 많다. 그래서 엔티티에서는 이 추가적인 컬럼들을 매핑할 방법이 없게 되는 것이다.

 

 

3) 복합 키를 갖는 연결 엔티티 방식

- 연결 엔티티를 추가하면 연결 엔티티 안에 추가되는 다양한 컬럼들을 사용할 수 있다. 이번에도 회원과 상품을 예시로 들어보자.

- 연결 엔티티를 통해서 다대다 관계가 일대다/다대일 두 쌍의 관계로 분리되기 때문에, 양방향 매핑 시에는 항상 연결 엔티티쪽이 연관관계의 주인이 된다.

- 복합 키와 복합 키를 구성하는 컬럼에는 @GenerateValue를 사용할 수 없다.

 

(1) 연결 엔티티 생성

<MemberProduct.class>

@Entity
@IdClass(MemberProductId.class) // 식별자 클래스를 지정 (잠시 후에 살펴보자)
public class MemberProduct {
     
     @Id
     @ManyToOne
     @JoinColumn(name="MEMBER_ID")
     private Member member;
     
     @Id
     @ManyToOne
     @JoinColumn(name="PRODUCT_ID")
     private Product product;
     
     private int orderAmount;
     
     ...
}

- 다대다 관계를 갖는 두 엔티티의 기본 키를 각각 가져와야 하는데, 가져온 각 기본 키는 연결 엔티티에서 기본 키로 사용함과 동시에 외래 키로도 활용한다. 그래서 @Id와 @JoinColumn을 같이 사용하는 것이다.

- 위와 같이 복합 키를 사용하기 위해서는 두 기본 키를 엮기 위한 해당 식별자 클래스가 있어야 한다. 식별자 클래스를 지정하기 위한 어노테이션으로는 @IdClass와 @EmbeddedId가 있다.

 

(2) 연결 엔티티 식별자 클래스 생성

<MemberProductId.class>

@EqualsAndHashCode
public class MemberProductId implements Serializable {
    private String member;
    private String product;
    
    ...
}

- 복합 키를 위한 식별자 클래스는 public이어야 하고, Serializable 인터페이스를 상속해야 하고, equals()와 hashCode() 메소드를 구현해야 하고, 기본 생성자가 있어야 한다. 그리고 식별자 클래스의 속성명과 엔티티 클래스의 속성명은 서로 같아야 한다.

※ equals()와 hashCode()를 구현하지 않으면 식별자 객체의 동등성이 지켜지지 않아서 엔티티 관리에 심각한 문제가 발생할 수 있다.

 

(3) 엔티티

<Member.class>

@Entity
public class Member {
   
   @Id @Column(name="MEMBER_ID")
   private String id;
   
   private String username;
   
   @OneToMany(mappedBy="member") // Member은 연관관계의 주인이 아님
   private List<Product> products = new ArrayList<Product>();
   
   ...
}

 

<Product.class>

@Entity
public class Product {
    @Id @Column(name="PRODUCT_ID")
    private String id;
    
    private String name;
    
    ...
}

 

(4) 조회

- 다음과 같이 영속성 컨텍스트에 저장되었다고 가정하자.

Member member1 = new Member();
member1.setId("member1");
member1.setName("jooyeok");
em.persist(member1);

Product productA = new Product();
productA.setId("productA");
productA.setName("사과");
em.persist(productA);

MemberProduct memberProduct = new MemberProduct();
memberProduct.setMember(member1);
memberProduct.setProduct(productA);
memberProduct.setOrderAmount(5);
em.persist(memberProduct);

- 조회는 MemberProductId를 통해 MemberProduct 엔티티를 조회하고, MemberProduct를 통해 다시 Member 객체와 Product 객체를 가져오는 식으로 동작한다.

MemberProductId memberProductId = new MemberProductId();
memberProductId.setMember("member1");
memberProductId.setProduct("productA");

MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);

Member member = memberProduct.getMember();
Product product = memberProduct.getProduct();

 

(5) 연결 엔티티 참조

- 연결 엔티티를 참조하는 필드에 매핑되는 외래 키는 복합 키가 된다. 그래서 이 외래 키를 매핑 할 때는 여러 컬럼을 매핑해야 하므로 @JoinColumns를 사용해서 각 외래 키 컬럼을 @JoinColumn으로 매핑하자.

- @JoinColumn의 name은 외래 키의 이름이고, referencedColumn은 외래 키가 조인할 대상 테이블의 컬럼 이름이다.

@JoinColumns({
    @JoinColumn(name="MEMBER_ID", referencedColumnName="MEMBER_ID"),
    @JoinColumn(name="PRODUCT_ID", referencedColumnName="PRODUCT_ID")
})
private MemberProduct memberProduct;

※ JoinColumn의 name 속성값과 referencedColumnName 속성값이 같다면 referencedColumnName 속성은 생략해도 된다.

 

 

4) 연결 엔티티에 새로운 기본 키를 부여하는 방식

- 복합 키를 갖는 연결 엔티티 방식도 나쁘진 않아보이지만 일일이 식별자 클래스를 만드는 것이 번거로울 수 있다. 그래서 연결 엔티티에는 새로운 기본 키 하나만 부여해주고 기존에 사용하던 복합 키는 모두 외래 키로 사용하는 방식을 생각해볼 수도 있다.

※ 이 때, 기본 키 생성 전략은 DB에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 전략을 사용하자. 간편하면서도, 기본 키를 거의 영구적으로 쓸 수 있고 비즈니스에 의존하지 않는다는 장점이 생긴다.

- 이제 이 전략을 적용한 소스 코드를 살펴보자. 이번에도 회원과 상품을 예시로 들어보자.

 

(1) 연결 엔티티

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

 

(2) 엔티티

- 변경 사항 없음

 

(3) 조회

- 다음과 같이 영속성 컨텍스트에 저장되었다고 가정하자.

Member member1 = new Member();
member1.setId("member1");
member1.setName("jooyeok");
em.persist(member1);

Product productA = new Product();
productA.setId("productA");
productA.setName("사과");
em.persist(productA);

Order order = new Order();
order.setMember(member1);
order.setProduct(productA);
order.setOrderAmount(5);
em.persist(order);

- 이번에는 단순히 Order 엔티티의 id를 통해 조회할 수 있게 되었다.

Long orderId = 1L;
Order order = em.find(Order.class, orderId);

Member member = order.getMember();
Product product = order.getProduct();

 

 

 

 

● 참고자료 : 자바 ORM 표준 JPA 프로그래밍

반응형

'Spring 사전 준비 > JPA Hibernate' 카테고리의 다른 글

[JPA] 프록시 객체  (0) 2022.08.01
[JPA] 고급 매핑 기술  (0) 2022.07.28
[JPA] 단방향 매핑과 양방향 매핑  (0) 2021.08.18
[JPA] EntityManager / persistence.xml  (0) 2021.08.16
[JPA] Entity Annotations  (0) 2021.08.16

댓글