본문 바로가기
BackEnd/JPA

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

by 개발 Blog 2024. 7. 14.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

다대일 [N:1]

연관관계 매핑 시 고려사항 3가지 

1. 다중성

- 다대일 : @ManyToOne

- 일대다 : @OneToMany

- 일대일 : @OneToOne

- 다대다 : @ManyToMany (실무에서 사용하면 안 된다.)

 

2. 단방향, 양방향

- 테이블

  • 외래 키 하나로 양쪽 조인 가능하다.
  • 사실 방향이라는 개념이 없다.

- 객체

  • 참조용 필드가 있는 쪽으로만 참조 가능하다.
  • 한쪽만 참조하면 단방향이다.
  • 양쪽이 서로 참조하면 양방향이다.

3. 연관관계의 주인 

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

 

다대일 단방향

- 가장 많이 사용하는 연관관계이다.

- 다대일의 반대는 일대다.

 

다대일 양방향

- 외래 키가 있는 쪽이 연관관계의 주인이다.

- 양쪽을 서로 참조하도록 개발한다.

 

일대다 [1:N] 단방향

- 1:N 중 1이 연관관계 주인이다.

- 객체 입장에선 논리적으로 1(Team)에 FK가 들어갈 수 있지만 실제 DB에선 무조건 N(Member)에 FK가 들어가야 한다. 왜냐하면 TEAM_ID는 유일해야 하는데 FK로 잡아버리면 중복이 되기 때문이다.

- 어쨌든 객체 연관관계에서는 Team에 FK를 주는 게 가능하다. 하지만 그렇게 되면 List members 값을 변경했을 때 다른 테이블에 있는 FK(TEAM_ID)를 관리해야 한다.

@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<>();
}
public class JpaMain {

    public static void main(String[] args) {

        //생략

        try {

            Member member = new Member();
            member.setUsername("member1");

            em.persist(member);

            Team team = new Team();
            team.setName("teamA");
            team.getMembers().add(member); //애매한 부분

            em.persist(team);


            tx.commit();
            
    }
}

- insert 쿼리 두 번에 update 쿼리가 추가로 수행되었다. 

- 양쪽 객체를 저장하는 insert 쿼리 후에 team.getMembers(). add(member); 여기서 team 엔티티를 저장하려면 Member 테이블을 update 할 수밖에 없는 구조이다.

- 따라서 어쩔 수 없이 쿼리가 한 번 더 수행된다.

 

또한 1:N 방식은 여러 테이블이 엮여 있는 실무에서 문제를 일으킬 수 있다.

Team에 데이터를 insert 하면 연관된 다른 테이블에도 update가 발생할 수 있어 복잡해진다. 

따라서 객체지향적으로 조금 손해(ex) Member에서 Team으로 갈 일이 없는데 만드는 것)를 보더라도 이 방식은 권장하지 않는다.

 

정리

- 일대다 단방향은 1:N 관계에서 1이 연관관계의 주인이다.
- 테이블의 일대다 관계에서는 항상 N 쪽에 외래 키가 있다.
- 객체와 테이블의 차이로 인해 반대편 테이블의 외래 키를 관리하는 특이한 구조이다.
- @JoinColumn을 반드시 사용해야 하며, 그렇지 않으면 중간에 테이블이 추가되는 조인 테이블 방식을 사용하게 된다.

- 1:N 단방향 매핑보다는 N:1 양방향 매핑을 사용하자.

 

일대다 [1:N] 양방향

- Member 입장에서 Team의 정보가 필요할 때 사용한다.

- 공식적인 스펙상 가능하지 않지만, 편법으로는 가능하다.

@Entity
public class Member {
	//생략
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

- 이렇게 insertable = false, updatable = false을 넣으면 매핑도 되어있고 값도 다 쓰는데, 읽기 전용이 된다.

 

정리

- 이런 매핑은 공식적으로 존재하지 않는다.

- @JoinColumn(insertable=false, updatable=false)를 사용한다.

- 읽기 전용 필드를 통해 양방향처럼 사용하는 방법이다.

- 권장 방식은 다대일 양방향 매핑을 사용하는 것이다.

 

일대일 [1:1]

 

일대일 단방향

- 일대일 관계는 양쪽 테이블 모두 일대일 관계를 가질 수 있다.
- 외래 키를 주 테이블 또는 대상 테이블에 선택하여 저장할 수 있다.
  ㄴ 주 테이블에 외래 키를 두는 방식
  ㄴ 대상 테이블에 외래 키를 두는 방식
- 외래 키에 데이터베이스 유니크(UNI) 제약 조건을 추가하여 유일성을 보장한다.

- 다대일 단방향 매핑과 유사하다.

@Entity
public class Locker {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
}
@Entity
public class Member {
	//생략

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

 

일대일 양방향

- 다대일 단방향 매핑처럼 외래 키가 있는 곳이 연관관계 주인이다.

- 반대편은 mappedBy를 적용한다.

@Entity
public class Locker {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")  // 읽기전용
    private Member member;
}

 

일대일 : 대상 테이블에 외래 키 단방향(불가능)

- 이 방식은 JPA에서 지원하지 않는다.

- 대상 테이블에 외래키를 주고 싶으면 양방향으로 설계해야 한다.

 

일대일 : 대상 테이블에 외래 키 양방향

- 사실 이 방식은 일대일 방식을 대칭으로 뒤집은 것과 같다.

일대일 정리

주 테이블에 외래 키
  - 주 객체가 대상 객체를 참조하듯이, 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
  - 객체지향 개발자가 선호한다.
  - JPA 매핑이 편리하다.
  - 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
  - 단점: 값이 없으면 외래 키에 null을 허용해야 한다.

대상 테이블에 외래 키
  - 대상 테이블에 외래 키가 존재한다.
  - 전통적인 데이터베이스 개발자가 선호한다.
  - 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지할 수 있다.
  - 단점: 프록시 기능의 한계로 인해 지연 로딩으로 설정해도 항상 즉시 로딩된다.

요약
- 주 테이블에 외래 키를 두면 객체지향적으로 매핑이 편리하지만, 외래 키에 null을 허용해야 한다.
- 대상 테이블에 외래 키를 두면 전통적인 데이터베이스 개발에 유리하지만, 항상 즉시 로딩된다.

 

다대다 [N:M]

결론부터 말하면 실무에서는 쓰면 안 되는 방식이다.

- 관계형 데이터베이스는 정규화된 테이블 2개로 N:M 관계를 표현할 수 없다.

- 연결 테이블을 추가해서 1:N, N:1 관계로 풀어내야 한다.

 

하지만 객체는 가능하다.

- Member도 Product List를 가질 수 있고 Product도 Member List를 가질 수 있다.

 

Product 엔티티

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Member 엔티티

@Entity
public class Member {

    //생략
    
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
}

- @ManyToMany를 사용한다.

- @JoinTable에 연결 테이블명을 지정한다.

- 연결 테이블을 생성하고 외래키 제약조건도 생성한다.

 

양방향으로 매핑하고 싶으면 Product에 mappedBy를 써주면 된다.

@Entity
public class Product {

    @Id @GeneratedValue
    private Long id;

    private String name;

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

 

다대다 매핑의 한계

- 편리해 보이지만 실무에서 사용할 수 없다.
- 연결 테이블이 단순히 연결만 하고 끝나지 않는다.
- 중간 테이블에 주문시간, 수량 같은 추가 데이터를 넣을 수 없다.
- 중간 테이블이 숨겨져 있어 쿼리가 복잡하게 작성된다.

다대다 한계 극복

- 연결 엔티티를 하나 추가한다.

-  @ManyToMany -> @OneToMany, @ManyToOne

@Entity
public class Member {
    //생략

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();
    
}
@Entity
public class MemberProduct {

    @Id @GeneratedValue
    private Long id;

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

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
}
@Entity
public class Product {
	
    // 생략

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

 

 

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

 

배송, 카테고리 추가 - 엔티티

- 주문과 배송은 1:1 관계이다. (@OneToOne)

- 상품과 카테고리는 N:M 관계이다. (@ManyToMany)

 

배송, 카테고리 추가 - ERD

- 주문과 배송은 1:1 관계라서 DELIVERY_ID (FK)를 어디에 넣을지 고민해야 하는데, 주 테이블에 넣기로 결정했다.

- 카테고리와 아이템은 N:M 관계이다.

 

배송, 카테고리 추가 - 엔티티 상세

 

Order와 Delivery를 1대1 양방향으로 설정한다.

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

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

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

    @OneToOne
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;
}
@Entity
public class Delivery {

    @Id @GeneratedValue
    private Long id;

    private String city;
    private String street;
    private String zipcode;
    private DeliveryStatus status;

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

 

다음으로 카테고리와 아이템을 설계한다.

 

하나의 카테고리에 여러 아이템이 소속될 수 있고,

하나의 아이템에 여러 카테고리가 소속될 수 있다고 가정한다.

@Entity
public class Category {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

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

    @ManyToMany
    @JoinTable(name = "CATEGORY_ITEM",
            joinColumns = @JoinColumn(name = "CATEGORY_ID"), //조인 테이블에서 현재 엔티티에 대한 외래키를 지정
            inverseJoinColumns = @JoinColumn(name = "ITEM_ID") //조인 테이블의 반대쪽 엔티티에 대한 외래키 설정
    )
    private List<Item> items = new ArrayList<>();
}
@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> categoryList = new ArrayList<>();
    
    //생략
}

 

이제 실행해 보면

의도한 대로 테이블이 생성된 것을 확인할 수 있다.

 

N:M 관계는 1:N, N:1로 

- 위의 코드는 예시이기 때문에 N:M을 쓴 것이고, 실무에서는 사용하면 안 된다.

- 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1 관계로 표현된다.
- 실전에서는 중간 테이블이 단순하지 않다.
- @ManyToMany는 제약이 많다: 필드 추가 불가, 엔티티와 테이블 간 불일치 문제 발생.
- 실전에서는 @ManyToMany 사용을 권장하지 않는다.

 

@JoinColum

- 외래 키를 매핑할 때 사용한다.

 

@ManyToOne 

- 다대일 관계 매핑할 때 사용한다.

- mappedBy가 없다.

 

@OneToMany

- 일대다 관계 매핑할 때 사용한다.

- mappedBy가 있다.

 

즉 다대일은 연관관계 주인이 되어야 한다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 프록시와 연관관계 관리  (0) 2024.07.16
[JPA] 고급 매핑  (0) 2024.07.15
[JPA] 연관관계 매핑 기초  (2) 2024.07.14
[JPA] 엔티티 매핑  (0) 2024.07.12
[JPA] 영속성 관리 - 내부 동작 방식  (1) 2024.07.12