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

이가희·2024년 12월 1일
3

JPA

목록 보기
6/16
post-thumbnail

여태까지 다대일 (다쪽이 연관관계 주인인) 단방향, 양방향 매핑에 대해 알아보았다.
이번에는 다양한 (일대다, 일대일, 다대다) 연관관계 매핑을 알아 볼 것이다.
여기서 왼쪽을 (일대다의 경우 일이 연관관계 주인) 연관관계 주인으로 가정하고 하나씩 살펴보겠다.

Chapter

  1. 일대다 연관관계 매핑
  2. 일대일 연관관계 매핑
  3. 다대다 연관관계 매핑

1. 일대다 연관관계 매핑

▶ 일대다 : 단방향

팀과 회원의 경우, 하나의 팀은 여러 개의 회원을 가질 수 있으니 일대다 관계이다.
앞서 설명 했듯이 일대다의 경우 항상 다 쪽이 외래키를 가진다.
그래서 일 쪽을 연관관계 주인으로 설정할 경우 반대편 (여기서는 회원) 에 있는 외래키를 관리하게 된다. 그래서 별로 추천하지 않는 매핑 방법이다.

실제 코드를 살펴보면, 엔티티는 아래와 같다.
Team

@Entity
public class Team{
	@Id @GeneratedValue
 	@Column (name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @Column (name = "TEAM_ID) //여기서 TEAM_ID는 MEMBER 테이블에 있는 외래키 TEAM_ID를 말한다.
    private List<Member> members = new Arraylist<Member>();
}

Member

@Entity
public class Member{
	@Id @GeneratedValue
    @Column (name = "MEMBER_ID)
    private Long id;
    
    private String username;
}

앞서서 계속 다대일/일대다 관계일 때 연관관계 주인을 다 쪽에 주어야한다고 강조했다.
왜냐하면 이처럼 연관관계 주인을 외래키가 있는 쪽이 두면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 하기 때문이다.

Member member = new Member("member");

Team team = new Team("team");
team.getMembers().add(member);

em.persist(member1); //INSERT member1
em.persist(team); //INSERT team + UPDATE member.fk
//-> 연관관계 주인이 member이면 UPDATE member.fk는 하지 않아도 됨

따라서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 권장된다. (앞선 포스팅을 읽으면 더 확실히 이해가 될 것이다.)

▶ 일대다 : 양방향

앞선 포스팅에서 다대일/ 일대다 관계에서 항상 연관관계 주인은 다 쪽이 되기 때문에 @ManyToOne에는 mappedBy속성이 없다고 했다.
그래서 일대다 양방향 매핑을 하기 위해선 일대다 단반향 매핑 반대편(여기서는 Member)에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가해야 한다.

@Entity
public class Member{
	@Id @GeneratedValue
    @Column (name = "MEMBER_ID)
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn (name = "TEAM_ID), insertable = false, updatable = false)
    private Team team;
}

insertable 과 updatable을 추가한 이유는 만약 추가하지 않으면 Team 과 Member 모두 같은 키를 관리하여 문제가 발생할 수 있다. (Member에서는 해당 키를 UPDATE했지만, 영속성 컨텍스트에 Team은 UPDATE 하기 이전의 값을 지니게 되어 데이터 불일치 문제 등)
그래서 JPA는 연관관계 주인만이 외래키를 관리할 수 있게 하고, 따라서 여기서도 그렇게 만드는 것이 좋다. 더 좋은건 다대일 양방향을 사용하자.


2. 일대일 연관관계 매핑

주 테이블에 외래 키가 있는 경우

회원은 하나의 사물함만 사용하고, 사물함도 하나의 회원에 의해서만 사용될 때 회원과 사물함은 일대일 관계이다.
이때, 마찬가지로 일대일 연관관계에서도 개발자들은 보통 주 테이블에 외래 키가 있는 것을 선호한다. 먼저 단방향부터 살펴보겠다.

▶ 단 반향
여기서 Member 이 주 테이블이고 , LOCKER이 대상 테이블이다.

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

다대일 단방향과 크게 다를 것이 없다.
이제 양방향 관계로 만들어 보겠다.

▶ 양 방향

@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") //똑같이 OneToOne 어노테이을 사용했지만, mappedBy를 통해 주인이 Member임을 나타내었따.
    private Member member;
}

일대일 단방향 일 때 일반적으로 사용하는 형식이다.
이번에는 대상 테이블에 외래 키가 있는 일대일 관계를 보며 비교해보자.

대상 테이블에 외래 키가 있는 경우

일대일 관계 중에 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 그리고 어떻게든 다른 방법으로 하려고 해도, 이런 모양으로 매핑할 수 있는 방법도 없다.
그래서 주 테이블에 외래 키가 있는 경우을 권장한다.
양방향은 아래와 같이 설정하면 된다.

▶ 양 방향

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

이렇게 대상 테이블에 외래 키가 있는 경우에 대해 연관관계 매핑을 하고 싶다면, 반드시 양방향으로 만들어야 한다.


3. 다대다 연관관계 매핑

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
회원이 여러 개의 상품을 주문하고, 상품은 여러 회원들에 의해 주문 될 때 이 둘은 다대다 관계이다. 회원 테이블에 회원 아이디가 Pk이고 상품 아이디가 Fk일 때, 회원 한 명이 여러 개의 상품을 주문한 것을 회원 테이블에 담을 수 없다. (PK 유일성 때문에 저장 못함) 그렇게 때문에 중간 연결 테이블을 추가해야 한다.

그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.
@ManyToMany를 이용한 단반향 매핑부터 알아보자.

▶ 단방향
회원

@Entity
public class Member {
	@Id @Column (name = "MEMBER_ID")
    private String id;
    
    private String username;
    
    @ManyToMany
    @JoinTable (name = "MEMBER_PROUDCT",
    	joinClumns = @JoinColumn(name = "MEMBER_ID"),
        inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> products = new ArrayList<Product>();
}
    

상품

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

@ManyToMany 와 @JoinTable을 사용해서 연결 테이블을 바로 매핑하였다.
그래서 중간 연결 엔티티 (MEMBER_PRODUCT) 없이 매핑을 할 수 완료 할 수 있었다.

@JoinTable 속성

  • @JoinTable.name : 연결 테이블 지정
  • @JoinTable.joinColumns : 현재 방향 엔티티 (여기서는 회원)과 매핑할 조인 컬럼 정보를 지정
  • @JoinTable.inverseJoinColumns : 반대 방향 (여기서는 상품)과 매핑할 조인 컬럼 정보를 지정

이렇게 매핑하고, 회원을 조회한 다음 member.getProducts()를 통해 상품들을 탐색하면

SELECT * FROM MEMBER_PRODUCT MP
INNER JOIN PRODUCT P ON MP.PRODUCT_ID = P.PRODUCT_ID
WHERE MP.MEMBER_ID = ?

라는 SQL이 실행되게 된다.

▶ 양방향
상품 엔티티에 mappedBy를 추가하면 된다. (이렇게 되면, 연관관계의 주인은 회원이 된다.)
상품

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

양방향일 때는 연관관계 편의 메소드를 추가하는 것이 좋다.

public void addProduct (Product product){
	products.add(product);
    products.getMembers().add(this);
 }
 //회원 엔티티에 편의 메소드 추가

앞선 장에서 설명 했듯 편의 메소드를 작성 할 땐,
비즈니스를 잘 분석하여 중복 방지 등을 추가해 견고히 작성해야 한다.
여기서는 간단히 작성하였다.

이렇게 하면 이제 상품에서도 회원을 탐색할 수 있게 된다.

그런데, 여기서는 MEMBER_PRODUCT에 회원 아이디, 상품 아이디만 있어, 따로 엔티티를 만들지 않아도 되었다. 하지만 일반적으로 상품을 주문 할 땐 주문 수량이나 주문 날짜 등의 추가 정보들이 필요하다.
그래서 이런 경우엔 이전 처럼 @ManyToMany를 사용하지 못하고 연결 엔티티를 만들어야 한다. (여담이지만, @ManyToMany는 웬만해서는 사용하지 않는 것이 좋다.)

▶ 연결 엔티티 사용


MemberProduct 엔티티를 생성해서 매핑을 해 볼 것이다.
외래 키는 모두 회원 상품 엔티티 쪽이 가지고 있다. 따라서 여기서 주인은 회원 상품 엔티티이다.

@Entity
public class Member {
	@Id @Column (name = "MEMBER_ID")
    private String id;
    
    private String username;
    
   @OneToMany (mappedBy = "member")
   private List<MemberProduct> memberProducts;
}

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

회원 엔티티는 다음과 같이 작성한다.
상품에서 회원상품 엔티티로 조회가 필요할 것 같지는 않아, 상품 엔티티에는 연관 관계를 만들지 않았다.


//회원상품 엔티티
@Entity
@IdClass (MemberProductId.class)
public class MemberProduct {
	@Id
    @ManyToOne
    @JoinColumn (name = "MEMBER_ID")
    private Member member; //MemberProductId.member와 연결
    @Id
    @ManyToOne
    @JoinColumn (name = "PRODUCT_ID")
    private Product product; //MemberProductId.product와 연결
    
    private int orderAmout;
}

//회원상품 식별자 클래스

public class MemberProductId implements Serializable {
	private String member; //MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결
    
    //equals, hashCode overriding 필수..
 }
    

회원 상품 엔티티는 PK를 매핑하는 @Id와 FK를 매핑하는 @JoinColumn을 동시에 사용해 기본 키 + 외래 키를 한 번에 매핑하였다.
여기서 회원 상품 엔티티는 PK가 2개인 복합 키로 되어 있는데, JPA에서 복합 키를 사용하려면 별도의 식별자 클래스 (여기서는 MemberProductId 클래스) 를 만들어야 한다. 그리고 엔티티에 @IdClass 를 이용해 식별자 클래스를 지정하면 된다.

식별자 클래스 특징

  • Serializable을 구현해야 한다. (엔티티를 영속성 컨텍스트에 저장하고 파일 혹은 네트워크로 보낼 때 식별자 클래스도 직렬화가 가능해야 하기 때문)
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

여기서 회원상품처럼 부모 테이블 (회원, 상품)의 기본 키를 받아 자신의 기본키 + 외래 키로 사용하는 것을 식별 관계라고 한다.

이렇게 매핑 한 것을 조회하려면 아래와 같이 하면 된다.

public void find() {
 //기본 키 생성
 MemberProductId memberProductId = new MemberProductId();
 memberProductId.setMember("member");
 memberPRoductId.setProduct("product");
 
 MemberProduct memberProduct = entityManager.find(MemberProduct.class, memberProductId); //중간 엔티티 조회
 
 Member member = memberProduct.getMember();
 Product product = memberProduct.getProduct();
 //중간 엔티티를 이용해 회원, 상품 탐색
 }

복합 키를 사용해서 다대다 매핑을 풀어보았는데,
식별자 클래스도 만들어야 하고, 복합 키가 2개가 아니라 그 이상인 경우 ORM 매핑에서 처리해야 할 일도 많아진다.

그래서 중간 연결 엔티티에 복합키가 아닌 새로운 기본 키를 할당하여 사용하는 것이 권장된다.

▶ 새로운 기본 키 사용
자동으로 생성해주는 대리 키를 중간 엔티티의 PK로 할당할 것이다.
이러면 아래 코드를 보면 알겠지만 훨씬 간편하고 비즈니스에 의존하지 않아서, 비즈니스에 따라 키를 변경해야 하는 수고스러움도 덜게 된다.

기존과 달리 기본 키가 비즈니스에 의존하지 않다보니, 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;
}

//회원
@Entity
public class Member {
	@Id @Column (name = "MEMBER_ID")
    private String id;
    
    private String username;
    
   @OneToMany (mappedBy = "member")
   private List<Order> orders = new ArrayList<Order>();
}

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

조회는 아래와 같이 하면 된다.

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

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

식별자 클래스를 사용하지 않아 코드가 한결 단순해진 것을 알 수 있다.

다음에는 고급 매핑 (상속 관계, 엔티티 하나에 여러 테이블 매핑 등)을 알아볼 것이다.

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

profile
안녕하세요 개발하는 사람입니다.

0개의 댓글

관련 채용 정보