다양한 연관관계 매핑

LeeKyoungChang·2022년 3월 9일
0
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.

 

✔️ 연관관계 매핑시 고려사항 3가지
(1) 다중성
(2) 단방향, 양방향
(3) 연관관계의 주인

1. 다중성

  • 다대일 : @ManyToOne
  • 일대다 : @OneToMany
  • 일대일 : @OneToOne
  • 다대다 : @ManyToMany

 

2. 단방향, 양방향
테이블

  • 외래 키 하나로 양쪽 조인 가능하기 때문에 방향이라는 개념이 없다.

객체

  • 참조용 필드가 있는 쪽으로만 참조가 가능하다.
    • 한쪽만 참조 : 단방향
    • 양쪽이 서로 참조 : 양방향 (단방향이 두 개인 것이다.)

 

3. 연관관계의 주인

  • 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺는다.
  • 객체 양방향 관계는 A → B, B → A 처럼 참조가 두 곳이다.
  • 객체 양방향 관계는 참조가 두 곳이 있으므로 둘중 테이블의 외래 키를 관리할 곳을 지정해야한다.
    • 연관관계의 주인 : 외래 키를 관리하는 참조
    • 주인의 반대편 : 외래 키에 영향을 주지 않음, 단순 조회만 가능

 

📚 1. 다대일 [N:1]

가 연관관계 주인이다.

📖 A. 다대일 단방향

스크린샷 2022-03-08 오후 6 05 31
  • 가장 많이 사용하는 연관관계
  • 다대일의 반대는 일대다

 

(1) 회원 엔티티

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

 

(2) 팀 엔티티

@Entity 
public class Team { 
	@Id @GeneratedValue 
	@Column(name = "TEAM_ID") 
	private Long id; 
	private String name; 
	... 
}
  • 회원은 Member.team 으로 참조가 가능하지만, 팀에선 회원을 참조할 필드가 없어 단방향이다.
  • 테이블로 구성한다면 외래키는 Member에만 존재한다.

 

jpamain

        try {

            Team team = new Team();
            team.setName("team1");
            em.persist(team);

            Member member = new Member();
            member.setName("chang");
            member.setTeam(team);
            em.persist(member);

            Member member2 = new Member();
            member2.setName("kChang");
            member2.setTeam(team);
            em.persist(member2);

            tx.commit();
//            HelloAb
        } 

 

H2

스크린샷 2022-03-25 오후 4 03 27

 

📖 B. 다대일 양방향

스크린샷 2022-03-09 오후 12 19 07
  • 외래 키가 있는 쪽이 연관관계의 주인
  • 양쪽을 서로 참조하도록 개발

 

(1) 회원 엔티티

@Entity 
public class Member { 
	@Id @GeneratedValue 
	@Column(name = "MEMBER_ID") 
	private Long id; 
	
	private String username; 
	
	@ManyToOne @JoinColumn(name = "TEAM_ID") 
	private Team team; 
	
	public void setTeam(Team team) {
		this.team = team; // 무한루프 방지 
		if(!team.getMembers().contains(this))
		{ 
			team.getMembers().add(this); 
		} 
	} 
}

 

(2) 팀 엔티티

@Entity 
public class Team { 
	@Id @GeneratedValue 
	@Column(name = "TEAM_ID") 
	private Long id; 
	
	private String name;
	
	@OneToMany(mappedBy = "team") 
	private List<Member> members = new ArrayList<>(); 
	
	public void addMember(Member member) {
		this.members.add(member); // 무한루프 방지 
		if (member.getTeam() != this) {
			member.setTeam(this); 
		} 
	} 
}
  • 팀 엔티티에 List가 추가되었고, 주인 필드인 Member.team을 가르키고 있다.
    • Team.members 는 주인이 아니므로 조회가 필요할 때 사용한다.
  • 양방향 연관관계는 항상 서로를 참조해야 함에 주의한다.
    • setTeam, addMember는 서로를 참조할 때 무한루프에 빠지지 않도록 처리되어 있다.

 

JpaMain

          Team team = new Team();
            team.setName("team1");
            em.persist(team);

            Member member = new Member();
            member.setName("chang");
            member.setTeam(team);
            em.persist(member);

            Member member2 = new Member();
            member2.setName("kChang");
            member2.setTeam(team);
            em.persist(member2);

            em.flush();
            em.clear();

            Member foundMember = em.find(Member.class, member.getId());
            Team team2 = foundMember.getTeam();

            List<Member> members = team2.getMembers();

            for (Member member1 : members) {
                System.out.println("member1.getName() = " + member1.getName());
            }

 

실행 결과

스크린샷 2022-03-25 오후 4 11 40

 

📚 2. 일대다 [1:N]

이 연관관계의 주인이다.

📖 A. 일대다 단방향

스크린샷 2022-03-09 오후 12 27 47
  • 객체 : Team을 중심으로 외래 키를 관리하고, Member 입장에서는 Team에 대한 참조가 없다.
  • 테이블 : 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있다.

➡️ 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조이다!

 

(1) 팀 엔티티

@Entity 
public class Team { 
	@Id @GeneratedValue 
	@Column(name = "TEAM_ID") 
	private Long id; 
	private String name; 
	
	@OneToMany 
	@JoinColumn(name = "TEAM_ID") 
	private List<Member> members = new ArrayList<>(); 
	... 
}

 

(2) 회원 엔티티

@Entity 
public class Member { 
	@Id @GeneratedValue 
	@Column(name = "MEMBER_ID") 
	private Long id; 
	
	private String username; 
	... 
}
  • 팀 엔티티의 @JoinColumn 을 보면 일대다인 경우에도 N(다)인 쪽에 외래키가 존재함을 알 수 있다.

 

JpaMain


        try {

            Member member = new Member();
            member.setUsername("chang");
            em.persist(member);

            Team team = new Team();
            team.setName("team1");
            team.getMembers().add(member);
            em.persist(team);

            Team team2 = new Team();
            team2.setName("team2");
            team2.getMembers().add(member);
            em.persist(team2);


            tx.commit();
        } 

 

실행 결과

스크린샷 2022-03-25 오후 4 30 06
  • Member와 Team 테이블을 생성한 후, 팀 테이블의 외래키 member에 update가 일어난다.
  • 이럴 경우 손해가 있다. 소스를 보고 쉽게 판단하기 어렵다.
  • 실행 결과를 보면 팀을 대상으로 일대 다 관계를 매핑했는데 Member가 업데이트 되었다.

 

✔️ 일대다 단방향을 권장하지 않는 이유

  • 테이블에서는 항상 다(N) 쪽에 외래 키가 있기 때문에 패러다임 충돌이 발생한다.
  • @JoinColumn을 꼭 사용해야 한다. 사용하지 않는다면 조인 테이블 방식을 사용해야 한다.
  • 실무에서는 테이블이 수십 개 이상 운영되는데, 관리 및 트레이싱이 매우 어렵다.
  • 객체 TeamList members 값을 변경하면 다른 테이블(Member) 속 외래 키 TEAM_ID를 업데이트 해줘야 한다.

 

✏️ 일대다 단방향 매핑의 단점

  • 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
  • 연관관계 관리를 위해 추가로 UPDATE SQL을 실행해야 한다.

➡️ 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자!

 

📖 B. 일대다 양방향

스크린샷 2022-03-09 오후 12 57 35
  • 이런 매핑은 공식적으로 존재하지 않는다.

 

(1) 팀 엔티티

@Entity 
public class Team { 
	@Id @GeneratedValue
	@Column(name = "TEAM_ID") 
	private Long id; 
	
	private String name; 
	
	@OneToMany 
	@JoinColumn(name = "TEAM_ID") 
	private List<Member> members = new ArrayList<>(); 
	
	... 
}

 

(2) 회원 엔티티

@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; 
	
	... 
}
  • @JoinColumn(insertable=false, updatable=false)

    • 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이다.
    • 이 속성이 없을 시, 연관관계의 주인이 2개인 것처럼 된다.
    • 일대다 단방향 매핑이 갖는 성능, 관리 측면 문제가 그대로 존재하므로 왠만하면 다대일 단방향을 사용해야 한다!

     

    소스를 보면 양방향으로 연관관계 주인을(JoinColumn) 선언하였다. 이는 어긋난 것이다. (외래 키는 하나만 있어야 하고, 주인인 곳에만 있어야 한다.)

➡️ 일대다를 사용하지말고, 다대일 양방향을 사용하자!

 

📚 3. 일대일 [1:1]

  • 일대일 관계는 그 반대도 일대일이다!
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
  • 외래키 소유 전략 2가지
    • 주 테이블이 외래키 소유
      • 외래키를 객체 참조처럼 쓸 수 있어서 객체 지향 개발에 편리하다.
      • 주 테이블이 외래키를 가지므로 주 테이블만 확인해도 대상 테이블과 연관 관계를 확인할 수 있다.
    • 대상 테이블이 외래 키 소유
      • 일반적인 DB 개발자들은 이 방법을 선호한다.
      • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조가 그대로 유지된다.
  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

 

📖 A. 주 테이블

(1) 외래 키 단방향 (일대일 관계에서는 왠만하면 이것을 사용하자)

스크린샷 2022-03-09 오후 1 27 37
  • 다대일(@ManyToOne) 단방향 매핑과 유사하다.

 

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

 

(2) 라커룸 엔티티

@Entity 
public class Locker { 
	@Id @GeneratedValue 
	@Column(name = "LOCKER_ID") 
	private Long id; 
	
	private String name; 
	
	... 
}

주 테이블인 회원에 Locker 필드(외래키)가 포함되어 있다.

 

실행 결과

스크린샷 2022-03-25 오후 4 55 43
  • Locker 테이블 생성 후, Member 테이블에 Loker_ID가 생성됨

 

(2) 외래 키 양방향

스크린샷 2022-03-09 오후 1 27 43
  • 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy 적용

 

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

 

(2) 라커룸 엔티티

@Entity 
public class Locker { 
	@Id @GeneratedValue 
	@Column(name = "LOCKER_ID") 
	private Long id; 
	
	private String name; 
	
	@OneToOne(mappedBy = "member") 
	private Member member; 
	
	... 
}

주인 필드인 회원 엔티티가 외래키를 가지므로 주인 필드를 나타내는 mappedBy 속성과 @OneToOne 어노테이션을 추가하였다.

 

📖 B. 대상 테이블

(1) 외래 키 단방향

스크린샷 2022-03-09 오후 3 10 18
  • 단방향 관계는 JPA 지원하지 않는다.
  • 양방향 관계는 지원한다.

 

(2) 외래 키 양방향

스크린샷 2022-03-09 오후 3 11 02
  • Locker의 매핑에 연관관계 주인을 잡아서 하면 된다.
  • 내가 내 것만 관리할 수 있다.
  • 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같다.

 

📌 일대일 정리

  • 주 테이블에 외래 키
    • 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
    • 객체지향 개발자 선호
    • JPA 매핑 편리
    • 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
    • 단점 : 값이 없으면 외래 키에 null 허용
  • 대상 테이블에 외래 키
    • 대상 테이블에 외래 키가 존재
    • 전통적인 데이터베이스 개발자 선호
    • 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
    • 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 추가 공부한다.)

 

📚 4. 다대다 [N:M]

실무에서 사용하지 않는다!

스크린샷 2022-03-09 오후 3 27 33
  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없으며 두 테이블을 연결하는 별도의 테이블이 필요하다.
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어야한다.
    • 회원과 상품이 다대다의 관계라고 하면, 회원_상품 테이블을 생성한다.
1. 회원 : 회원_상품 = 1:N
2. 회원_상품 : 상품 = M:1

 

  • 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능하다.
스크린샷 2022-03-09 오후 3 29 47
  • @ManToMany 사용한다. (회원 엔티티에)
  • @JoinTable로 연결 테이블을 지정한다.
  • 다대다 매핑도 단방향, 양방향 모두 가능하다.

 

다대다 단방향
(1) 회원 엔티티

@Entity 
public class Member { 
	@Id @GeneratedValue 
	@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<>(); 
	... 
}

 

(2) 상품 엔티티

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

 

다대다 양방향
(1) 회원 엔티티

@Entity 
public class Member { 
	@Id @GeneratedValue 
	@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<>(); 
	... 
}

 

(2) 상품 엔티티

@Entity 
public class Product { 
	@Id @Column(name = "PRODUCT_ID") 
	private String id; 
	private String name; 
	
	// 상품 엔티티에 역방향 참조 추가
	@ManyToMany(mappedBy = "products")
	private List<Member> members;
	
	... 
}
  • 다른 양방향과 마찬가지로 역방향도 @ManyToMany를 지정하고, mappedBy로 주인 필드를 지정한다.

 

✔️ 다대다 매핑의 한계
스크린샷 2022-03-09 오후 3 29 54

  • 편리해 보이지만 실무에서 사용하지 않는다!
  • 연결 테이블이 단순히 연결만 하고 끝나는 경우는 없다.
    • 주문 시간, 수량 같은 데이터가 들어올 수 있다.
    • 즉, 다대다 매핑으로 만들어진 중간 테이블에 추가 정보를 넣을 수 없다!
  • 중간 테이블이 숨겨져 있기 때문에 예상치 못한 쿼리가 나간다.

 

✔️ 다대다 한계 극복
스크린샷 2022-03-09 오후 3 30 00

  • 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
  • @ManyToMany@OneToMany, @ManyToOne
  • 다대다를 써야할 때는 일대다, 다대일로 변경해서 사용하자!

 

📚 5. 실전 예제 - 3. 다양한 연관관계 매핑

📖 A. 배송, 카테고리 추가

✔️ 엔티티

  • 상품을 주문할 때 배송정보를 입력할 수 있다. (@OneToOne)
  • 상품을 카테고리로 구분할 수 있다. (@ManyToMany)
스크린샷 2022-03-09 오후 5 15 53

 

✔️ ERD
스크린샷 2022-03-09 오후 5 17 24

 

✔️ 테이블이 추가된 ERD 분석
주문과 배송 : 주문(ORDERS)와 배송(DELIVERY)은 일대일 관계다. 객체 관계를 고려할 때 주문에서 배송으로 자주 접근할 예정이므로 외래 키를 주문 테이블에 두었다. 참고로 일대일 관계이므로 ORDERS 테이블에 있는 DELIVERY_ID 외래 키에는 유니크 제약조건을 주는 것이 좋다.

상품과 카테고리 : 한 상품은 여러 카테고리(CATEGORY)에 속할 수 있고, 한 카테고리도 여러 상품을 가질 수 있으므로 둘은 다대다 관계다. 테이블로 이런 다대다 관계를 표현하기는 어려우므로 CATEGORY_ITEM 연결 테이블을 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.

추가된 요구사항을 객체에 반영해서 아래 그림의 상세한 엔티티를 완성했다!

✔️ 엔티티 상세
스크린샷 2022-03-09 오후 5 17 32

 

📖 B. 일대일 매핑

Order

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "ORDERS")
public class Order {

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

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;      //주문 회원

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    @OneToOne
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;  //배송정보

    private Date orderDate;     //주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status;//주문상태


    //==연관관계 메서드==//
    public void setMember(Member member) {
        //기존 관계 제거
        if (this.member != null) {
            this.member.getOrders().remove(this);
        }
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
        delivery.setOrder(this);
    }

    //Getter, Setter
    ...
}

 

Delivery

import javax.persistence.*;

@Entity
public class Delivery {

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

    @OneToOne(mappedBy = "delivery")
    private Order order;

    private String city;
    private String street;
    private String zipcode;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]

    //Getter, Setter
    ...
}

 

DeliveryStatus

public enum DeliveryStatus {
    READY, //준비
    COMP   //배송
}
  • OrderDelivery는 일대일 관계고 그 반대도 일대일 관계다.
  • 여기서는 Order가 매핑된 ORDERS를 주 테이블로 보고 주 테이블에 외래 키를 두었다.
  • 따라서 외래 키가 있는 Order.delivery가 연관관계의 주인이다.
  • 주인이 아닌 Delivery.order 필드에는 mappedBy 속성을 사용해서 주인이 아님을 표시했다.

 

📖 C. 다대다 매핑

Category

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Category {

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

    private String name;

    @ManyToMany
    @JoinTable(name = "CATEGORY_ITEM",
            joinColumns = @JoinColumn(name = "CATEGORY_ID"),
            inverseJoinColumns = @JoinColumn(name = "ITEM_ID"))
    private List<Item> items = new ArrayList<Item>();

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Category parent;

    @OneToMany(mappedBy = "parent")
    private List<Category> child = new ArrayList<Category>();

    //==연관관계 메서드==//
    public void addChildCategory(Category child) {
        this.child.add(child);
        child.setParent(this);
    }

    public void addItem(Item item) {
        items.add(item);
    }


    //Getter, Setter
    ...
}

 

Item

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Item {

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

    private String name;        //이름
    private int price;          //가격
    private int stockQuantity;  //재고수량

    @ManyToMany(mappedBy = "items")                        
    private List<Category> categories = new ArrayList<Category>(); 

    //Getter, Setter
    ...
}
  • CategoryItem은 다대다 관계고 그 반대도 다대다 관계다.
  • Category.items 필드를 보면 @ManyToMany@JoinTable을 사용해서 CATEGORY_ITEM 연결 테이블을 바로 매핑했다.
  • 그리고 여기서는 Category를 연관관계의 주인으로 정했다.
  • 따라서 주인이 아닌 Item.categories 필드에는 mappedBy 속성을 사용해서 주인이 아님을 표시했다.

 

다대다 관계는 연결 테이블을 JPA가 알아서 처리해주므로 편리하지만 연결 테이블에 필드가 추가되면 더는 사용할 수 없으므로 실무에서 활용하기에는 무리가 있다. 따라서 CategoryItem이라는 연결 엔티티를 만들어 일대다, 다대일 관계로 매핑하는 것을 권장한다!

 

📖 D. 애노테이션

✔️ @JoinColumn

외래 키를 매핑할 때 사용

속성설명기본값
name매핑할 외래 키 이름필드명 + _ + 참조하는 테 이블의 기본 키 컬럼명
referencedColumnName외래 키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL)외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.
unique nullable insertable updatable columnDefinition table@Column의 속성과 같다.

 

✔️ @ManyToOne

다대일 관계 매핑

속성설명기본값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야 한다.TRUE
fetch글로벌 페치 전략을 설정한다.- @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY
cascade영속성 전이 기능을 사용한다.
targetEntity연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입정보를 알 수 있다.

 

✔️ @OneToMany

다대일 관계 매핑

속성설명기본값
mappedBy연관관계의 주인 필드를 선택한다.
fetch글로벌 페치 전략을 설정한다.- @ManyToOne=FetchType.EAGER - @OneToMany=FetchType.LAZY
cascade영속성 전이 기능을 사용한다.
targetEntity연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입정보를 알 수 있다.

 


참고

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글