개발자로 후회없는 삶 살기

JPA PART.다양한 연관관계 매핑 본문

[백엔드]/[JPA | 학습기록]

JPA PART.다양한 연관관계 매핑

몽이장쥰 2023. 8. 17. 21:52

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/149

 

김영한의 스프링 부트와 JPA 실무 완전 정복 로드맵 - 인프런 | 로드맵

Java, JPA 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

다양한 연관관계 매핑에 대해 알아봅니다. 대부분의 매핑을 다 알아보는 시간입니다.

 

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

연관관계 매핑시에 고려해야할 사항이 3가지 있습니다.

 

1. 다중성

다중성은 다대일, 일대다, 일대일, 다대다입니다. 그냥 DB 설계에 맞춰서 하면 됩니다. DB 개념이 잘 잡혀있으면 다대일인지, 일대일인지 알 수 있습니다. 대부분 다대일, 일대다를 많이 쓰고 일대일은 가끔 나오고 다대다는 절대로 쓰면 안 됩니다.

 

2. 단, 양 방향

테이블은 방향이 없고 객체는 방향이 있습니다. 객체의 양방향도 단방향 2개입니다.

 

3. 주인

테이블의 외래키가 있는 다측이 주인입니다.

 

=> 다대일

1. 다대일 단방향

JPA에서 가장 많이 사용하는 다대일 단방향입니다. DB는 다측에 외래키가 가는 데 객체에서도 다측에 ManyToOne 매핑을 걸면 됩니다.

 

2. 다대일 양방향

이게 앞서 본 양방향으로 1측에 List 참조를 넣으면 되고 이때도 테이블에 전혀 영향을 주지 않습니다. DB는 방향이 없기 때문에 객체에만 추가하면 됩니다. 설계할 때는 전혀 고려하지 않아도 되고 어플 개발 단계에서 조회할 때 필요하거나 jpql 작성할 때 필요하면 추가한다고 했습니다.

 

=> 1대다

이제 새로운 내용이 등장합니다. 여기서는 1이 주인으로 1 방향에서 외래키를 관리하는 것입니다.

 

※ 이 방법을 권장하지 않고 강사님께서는 이 모델을 실무에서 가져가지 않습니다.

 

1. 일대다 단방향

1인 팀에서 DB 멤버를 관리합니다. 이렇게 하는 이유는 관리를 한다는 것은 db의 fk를 바라본 다는 것인데 db는 다측에 fk가 있어야 해서 1에서 다측을 보는 것입니다.

 

객체 설계상 팀에는 멤버가 들어가는데 멤버에는 팀이 안 들어갈 확률이 높습니다. 객체 설계를 먼저하면 이렇게 나올 것입니다. 근데 DB 입장에서 보면 무조건 다측에 외래키가 있어야 해서 멤버에 외래키가 있어야 합니다. 그러면 팀의 리스트에 members 값을 바꿀 때 멤버 테이블인 다른 테이블의(team 테이블이 아닌) 값을 바꾸게 됩니다.

 

-> 코드

이걸 코드로 만들면 1측에 OneToMany하고 JoinColumn을 다측인 member 테이블의 team_id로 합니다. db를 관리한다는 것은 db의 fk를 바라본다는 것이고 그러면 무조건 joincolumn으로 db 테이블의 컬럼을 매칭해야 합니다.

 

이걸 사용해보면 member는 pk외에 name 넣었으니 당연히 insert되고 팀도 이름 넣고 외래키로 member 넣었으니 되는데 팀 테이블에 외래키가 없어서 외래키를 넣을 수가 없습니다. 

 

 

이 외래키가 멤버에 있는데 그러면 멤버가 업데이트 됩니다. insert를 회원과 팀을 하는데 member에 update set이랍니다. 팀에 getMembers().add(member) 하고 insert를 했는데 외래키가 없으니 어쩔 수 없이 멤버 테이블에 update가 나갑니다.

 

-> 이걸 쓰지 않는 이유

팀에 손을 댔는데 멤버가 업데이트 되네? 실무에서 테이블이 한 두개가 아닌데 이런 현상이 나오면 알 수가 없습니다. 따라서 기존처럼 다대일 단방향에서 필요하면 양방향 추가하는 방식으로 갑니다. 사용하지 않는 것이 좋습니다.

 

※ 일대다 양방향은 공식적으로 존재하지 않는 매핑입니다.

 

=> 일대일

일대일 관계는 주 테이블이나 대상 테이블 중에 외래키를 다 넣어도 상관없습니다. 두 테이블이 있을 때 멤버가 주 테이블일 때 주 테이블에 외래키를 넣을 수도 있고 대상 테이블인 팀 테이블에 넣을 수도 있습니다. 추가로 DB 외래키에 유니크 제약 조건을 추가해야 합니다.

 

1대1에서는 주인, 노예가 없는데 주인을 임의로 회원으로 정하면 회원에도 외래키를 넣어도 되고 락커에도 외래키를 넣어도 되는 것입니다. 보통 먼저 접근되는 테이블이 주입니다.

 

1. 일대일 주 테이블에 외래 키 단방향

멤버를 주로 보면 회원이 딱 하나의 락커를 가질 수 있다는 룰이 있습니다. 이러면 멤버에 외래키 + UK를 넣어도 되고, 락커에도 외래키 + UK를 넣어도 됩니다. 둘 다 1대1 연관관계가 됩니다. 둘 중에 하나에 어디든 넣어도 됩니다.

ex) 회원에 외래키 + UK를 넣는다고 하면 DB는 회원 테이블에 락커 id를 넣고 회원 엔터티에도 락커 참조를 넣으면 됩니다. 이건 마치 다대일 단방향과 유사합니다.

 

-> 코드

@Entity
public class Locker {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
}

락커를 추가합니다. 

 

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

멤버가 락커를 가지고 OneToOne으로 잡고 JoinColumn(locker_id)로 하면 됩니다. 멤버가 주인으로 테이블의 fk를 관리하기 위해 joincolumn합니다. 이러면 1대1 단뱡향이 끝나고 다대일 단방향과 유사합니다.

 

2. 일대일 주 테이블에 외래 키 양방향

양방향을 만들고 싶으면 락커에도 member를 만들고 onetoone을 하는데 mappedBy를 해야합니다.

 

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

다대일 양방향과 똑같이 적용되고 여기서도 가짜 매핑은 읽기 전용이 적용됩니다.

 

3. 일대일 대상 테이블에 외래 키 단방향

1대1에는 주 테이블에 외래키를 둬도 된다고 했지만 대상 테이블에 외래키를 둬도 된다고 했습니다.

 

하지만 이건 지원도 안 되고 처리할 방법도 없습니다.

 

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

락커를 영관관계의 주인으로 잡고 양방향을 잡은 것입니다. 사실 대상 테이블을 잡는 게 아니고 기존 (2. 일대일 주 테이블에 외래 키 양방향)에서 대상 테이블을 주 테이블로 바꿔서 하는 것입니다. 2번을 뒤짚은 것입니다.

 

- 일대일 양방향 정리

단방향은 정리할 게 없습니다. 일대일은 총 3가지로 

1. 주 테이블에 외래 키 단방향, 
2. 일대일 주 테이블에 외래 키 양방향, 
4. 일대일 대상 테이블에 외래 키 양방향

으로 단방향은 그냥 주 테이블에 외래키 넣으면 됩니다. 근데 양방향은 2가지 입니다.

멤버와 락커가 있을 때 외래키를 멤버에 락커 id를 주는게 좋을까요? 아니면 락커에 멤버 id를 두는게 좋을까요? 멤버에 외래키를 두는 방법은 일대일 주 테이블에 외래키 단방향, 양방향 2가지이고 락커에 두는 법은 일대일 대상 테이블에 양방향 1가지입니다.

즉, 멤버를 주인으로 해서 멤버에 외래키를 두는 게 좋을까요? 아니면 락커를 주인으로 두는 게 좋을까요?

 

-> 트레이드 오프

1) DBA 입장

둘 다 개발에서 가능해서 정답은 없습니다. 하지만 DBA의 관점에서 시간이 흘러서 하나의 회원이 여러개의 락커를 가질 수 있게 비즈니스 룰이 바뀌게 된다면 기존에 락커에 외래키를 두고 onotoone에 joincolumn하고 멤버에 멥드바이를 한 상태였다면 락커가 다측이 되어서 manytoone하고 UK 제약 조건만 없애면 됩니다. 따라서 나중에 다측이 될 거 같은 곳에 외래키를 두고 양방향으로 하는 게 좋습니다.

 

2) 개발자 입장

멤버에 락커 외래키가 있는 게 성능도 그렇고 장점이 많습니다. 객체 지향적으로 봤을 때 멤버에 락커 ID가 있는게 맞습니다.

강사님은 따라서 1대1 관계에서는 개발자 입장에서 그냥 양방향도 생각하지 말고 객체지향적으로 상대방을 가지고 있어야할 곳(여기서는 멤버)에서 상대방 id를 가지고 있게 합니다. 더 select를 많이 하는 곳에 외래키를 둡니다. 

결론적으로 명확하게 일대일 관계라면 단방향으로 만들고 더 많이 select 되는 테이블에 외래키를 둡니다. 역시 일대일에서도 단뱡향이 개발에 좋습니다. 그리고 양방향을 해야할 때는 둘 중 나중에 다측이 될 거 같은 곳에 외래키로 참조를 둬서 joinColumn을 두는 게 좋습니다. 하지만 때로는 대상 테이블에 외래키 두고 양방향으로 해야 하면 그냥 해야합니다.

 

=> 다대다

이건 절대 안 씁니다. DB는 다대다면 매핑 테이블이 나옵니다. 근데 객체는 다대다 관계가 @ManyToMany 어노테이션으로 가능합니다. 멤버와 제품이 양쪽에서 List를 가질 수 있습니다.

 

편리해 보이지만, 실무에서는 매핑 테이블에 추가 컬럼이 엄청 많이 들어갑니다. 절대로 단순히 연결만하고 끝나는 일이 없습니다. (수량, 변경시간 등)이 다 들어갑니다. 하지만 manytomany에는 중간 테이블에 추가 컬럼을 넣을 수 없습니다.

 

-> 한계 극복 방법

그러면 ManyToMany는 어떻게 해야할까요? OneToMany와 ManyToOne으로 바꾸고 중간 테이블을 엔터티로 승격하면 됩니다.

 

중간에 엔터티를 하나 만드는 것입니다. 위 예제에서 매핑 테이블로 쓰인 Member_Product를 실제 MemberProduct 엔터티로 만드는 것입니다.

 

그리고 그 안에 컬럼으로 id, member와의 1대다 참조, product와의 1대다 참조를 넣습니다. 매핑 테이블 입장에서 fk 2개를 가지고 있는데 그걸 MemberProduct 엔터티에 넣는 것은 당연해 보입니다. joincolumn도 넣어서 테이블의 컬럼 명과 매칭해줍니다.

 

그러면 회원 입장에서는 1측에서 다측을 보니 OneToMany(mappedBy)가 될 것이고 List를 가집니다.  \Product도 OneToMany(mappedBy = product)로 잡으면 됩니다.

 

-> 결과

이렇게 하면 결과적으로 이 그림이 나옵니다. 중간 테이블을 승격하고 관계를 RDB처럼 풀어냅니다.

 

이렇게 하면 중간 테이블인 MemberPro에 원하는 수량, 변경날짜, 주문 날짜 등을 다 넣을 수 있습니다. 이렇게 하는 이유는 비즈니스는 복잡해서 ManyToMany로 될 수 있는 것은 하나도 없기 때문입니다. 

 

🚨 주의사항

이렇게 매핑 테이블을 만들면 대부분 pk 2개를 묶어서 pk로 사용한다고 배웠습니다. 하지만 실무에서는 웬만하면 pk는 의미없는 값으로 하는게 좋습니다.

 

따라서 id를 가지고 member_product_id를 넣는 것입니다. 이렇게 하면 나중에 유연성이 생깁니다. 새로운 pk를 따고 다른 걸 fk로 쓰는 것입니다.

 

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

다양한 연관관계를 예제에 녹여봅니다. 주문과 배송은 1대1, 상품과 카테고리가 다대다입니다.

 

-> ERD

주문과 배송 중 누구에 fk를 넣을까요? 주테이블에 넣으면 좋다고 했는데 select를 먼저 하는 orders(db 예약어 order)에 넣습니다.

 

카테고리와 상품은 다대다로 하였고 중간 테이블이 있습니다.

 

-> 엔터티 설계

상품과 배송은 1대1 양방향으로 걸었습니다. 단뱡향이 좋다고 했는데 현재는 이렇습니다.

카테고리와 상품은 List를 가지고 후에 OneToMany를 할 것입니다.

 

- 코드

1. delivery 클래스

주문과 배송을 1대1로 합니다.

 

-> Orders

Orders가 주로 하기로 했기 때문에 oneToone하고 joincolumn합니다.

 

-> delivery

@Entity
public class Delivery {
    @Id
    @GeneratedValue
    private Long id;
    
    @OneToOne(mappedBy = "delivery")
    private Order order;
}

양방향으로 하기로 했기에 onetoone하고 mappedBy하면 됩니다.

 

2. 카테고리 클래스 다대다

@Entity
public class CategoryItem {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;
    
    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;
}

CategoryItem 엔터티를 만듭니다. 

 

// 카테고리
@Entity
public class Category {
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "category")
    private List<CategoryItem> categoryItems = new ArrayList<>();
}

// 상품
@Entity
public class Item {
    @Id
    private Long id;
    
    @OneToMany(mappedBy = "item")
    private List<CategoryItem> categoryItems = new ArrayList<>();
}

한 쪽인 카테고리는 OneToMany를 가지고 상품도 동일하게 만들면 됩니다.

'[백엔드] > [JPA | 학습기록]' 카테고리의 다른 글

JPA PART.프록시와 연관관계 관리  (0) 2023.08.22
JPA PART.고급 매핑  (0) 2023.08.20
JPA PART.연관관계 매핑 기초  (0) 2023.08.12
JPA PART.엔터티 매핑  (0) 2023.08.01
JPA PART.영속성 관리  (0) 2023.06.11
Comments