본문 바로가기
JPA

다양한 연관관계 매핑

by 성건희 2020. 4. 9.
반응형
실습 해보기

프로젝트 실습을 하면서 연관관계 매핑을 자주 하기는 했지만,

보통 1대다 다대1 관계를 많이 썼고 나머지는 잘 쓰지 않았다.

따라서 해당 개념에 대해 제대로 익히기 위해서 실습을 통해 학습해 봤다.

 

실습 해보기

연관관계

간단한 쇼핑 페이지의 연관 관계이다.

모든 연관관계를 학습할 수 있어 정말 좋은 예제라고 생각한다.

주문상품다대다 관계로 풀어내기 위해 주문 상품이라는 중간 테이블을 두어서
일대다, 다대일 관계로 풀어냈다.

 

참고 !! 연관관계 매핑 시 고려해야 될 사항

  1. 다중성 (1대 다..)
  2. 단방향, 양방향
  3. 연관관계의 주인

 

연관관계를 매핑할 때는 먼저 두 엔티티가 일대다 관계인지, 일대일 관계인지 다중성을 알아야 한다.

그 후 한 쪽만 참조하는 단방향 관계인지, 양 쪽 모두 참조하는 양방향 관계인지를 확인한다.

만약 양방향 관계라면, 두 엔티티 중 연관관계의 주인을 정해서 주인만 외래 키를 관리하도록 해야한다.

 

회원과 주문

한 명의 회원이 여러 주문을 할 수 있으므로 1 : N 관계이다.

단방향으로 하는게 좋을까, 양방향으로 하는게 좋을까?

보통 판매자 입장에서 해당 상품을 주문 한 회원을 찾는 경우는 있어도,

회원이 어떤 주문을 했는지를 조회하는 경우는 드물다.

따라서 주문이 회원을 참조하는 다대일 단방향 매핑을 하면 될 것 같다.

 

아니, 회원이 어떤 주문을 했는지 조회 할 경우도 있잔아요

물론 그럴 수 있다.

그런데 테이블 관계에서 생각해보면 외래 키 하나로 조인해서 양방향 쿼리가 가능하다.

즉 회원이 어떤 주문을 했는지 조회하려면 다음의 쿼리를 날리면 된다.

SELECT * FROM member m INNER JOIN orders o ON m.orders_id = o.orders_id;

 

양방향 매핑은 복잡하다. 연관관계의 주인도 정해야 하며,

두 개의 단방향 연관관계를 양방향으로 만들기 위해서 로직도 잘 짜야한다.

단방향 매핑은 언제나 연관관계의 주인이 된다.

양방향 매핑은 단방향과 비교해 반대방향으로 객체 그래프 탐색 기능이 추가된 것이 전부이다.

따라서 먼저 단방향을 고려하고 꼭 필요할 경우에만 양방향으로 관리하자.

 

단방향 매핑이므로 주문 엔티티에만 회원을 가지면 다음과 같다.

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

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
}
@Entity
public class Member {

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

관계는 다음과 같이 @ManyToOne을 통해 매핑한다.

orders가 Many가 되고, member가 One이라는 뜻이다.

@ManyToOne같은 키워드를 다중성이라고 하는데 다음과 같이 4가지가 있다.

 

다중성

다대일 : @ManyToOne

일대다 : @OneToMany

일대일 : @OneToOne

다대다 : @ManyToMany

 

실무에서는 다대일, 일대다를 가장 많이 쓰고

다대다는 실무에서 쓰면 안된다고 한다.

왜 그런지는 뒤에서 알아보자.

 

기본서를 정독하면서 알게 된 사실이 하나 있는데, 다중성은 왼쪽을 연관관계의 주인으로 정한다고 한다고 한다.

예를 들면 일대다 관계에서는 일(1)이 연관관계의 주인이 된다.

 

주문과 상품

주문과 상품은 다대다관계 이지만, 사실 다대다는 실무에서 사용하면 안된다. (이유는 뒤에서 설명)

따라서 중간 테이블인 주문상품 테이블을 두고 일대다, 다대일 양방향 매핑으로 구현해보자.

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

    ...

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

주문을 한번할 때 여러 상품을 고를 수 있으므로 OneToMany관계이다.

양방향 매핑에서는 연관관계의 주인이 항상 쪽인 외래 키가 있는 쪽이므로 OrderItem이 주인이 된다.

 

참고 ) 연관관계의 주인

객체의 양방향은 참조가 2군데 이므로, 둘 중 테이블의 외래 키를 관리할 곳을 정해야 함.

연관관계의 주인은 외래 키를 관리하는 참조다. 주인의 반대편은 조회만 가능.

 

양방향 매핑을 위해 주문상품 테이블을 다음과 같이 지정했다.

@Entity
public class OrderItem {

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

    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "item_id")
    private Item item;
}

보통 주문한 상품이 뭔지가 궁금하지, 해당 상품이 어떤 주문을 했는지는 중요하지 않으므로

OrderItemItem을 참조하는 단방향 매핑으로 구현했다.

따라서 Item은 OrderItem을 모른다.

@Entity
public class Item {

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

 

주문과 배송

하나의 주문건에 대해 한번의 배송이 이루어 지므로 1:1관계다.

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

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

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

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

테이블 관계에서는 항상 쪽이 외래 키를 가진다.

하지만 1:1관계에서는 둘 중 어느곳에서나 외래 키를 가질 수 있다.

이것을 크게 주 테이블에 외래 키, 대상 테이블에 외래 키를 가지는 2가지 방법으로 나뉜다.

실무에서는 이것 때문에 DBA와 싸움이 발생하는데

이유는 주 테이블에 외래 키를 지정하는 방식은 주 테이블에 외래 키가 있어서

외래 키를 객체 참조 하듯이 사용할 수 있어 객체지향 개발자들이 선호하는 방식이고,

대상 테이블에 외래 키를 지정하는 방식은 추후에 테이블 관계를 1:1에서 1:N으로 변경할 때

테이블 구조를 그대로 유지할 수 있어 데이터베이스 개발자들이 선호하는 방식이기 때문이다.

 

일대일 정리

주 테이블에 외래 키

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

 

대상 테이블에 외래 키

  • 대상 테이블에 외래 키가 존재

  • 전통적인 데이터베이스 개발자가 선호하는 방식

  • 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지

  • 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨

    (이 부분은 잘 이해가 되지 않아서 프록시를 학습한 후 다시 봐야겠다..)

 

실무에서는 위와 같은 트레이드 오프가 있어서 DBA와 싸울 수도 있다.

김영한 님 曰 : 주 테이블에 외래 키 방식을 선호하지만 DBA를 잘 설득해야 한다.

 

상품과 카테고리

상품에는 여러 카테고리를 중복시킬 수 있고, 카테고리도 여러 상품이 있을 수 있으므로 N:N관계다.

아까 실무에서 다대다 관계는 실무에서 사용하면 안된다고 했는데, 그 이유는 다음과 같다.

 

다대다 매핑의 한계

편리해 보이지만 실무에서 사용 X

연결 테이블이 단순히 연결만 하고 끝나지 않음

주문시간, 수량 같은 데이터가 들어올 수 있음

중간 테이블이 있기 때문에 쿼리도 내가 예상하지 못한 쿼리가 나감.

 

중간 테이블을 왜 넣어요? 그냥 바로 다대다로 표현하면 안됨??

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

따라서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.

객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.

 

해당 예제는 그냥 다대다 관계를 실습하기 위해서 만든 것이고, 사실은 일대다, 다대일 관계로 풀어내야한다.

@Entity
public class Category {

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

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

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

관계형 데이터베이스는 테이블 2개로 다대다를 표현할 수 없으므로, 중간 테이블인 category_item을 추가했다.

@JoinTablejoinColumns은 Category (현재방향)와 매핑할 조인 컬럼 정보를 지정하는 것이고,

inverseJoinColumns는 Item (반대방향)과 매핑할 조인 컬럼 정보를 지정하는 것이다.

다 쪽이 양쪽에 있으므로 원하는 곳을 연관관계의 주인으로 정하면 되는데,

카테고리에서 상품을 관리하는 경우가 더 많다고 생각되서 카테고리를 연관관계의 주인으로 정했다.

 

일대다 관계

위 예제에는 자세히 설명하지 않았지만 중요한 내용이므로 별도 설명을 이어가겠다.

일대다 관계는 처음 접했을 때 상당히 햇갈렸다.

바로 예제로 들어가보자.

 

일대다 단방향 관계

Team과 Member가 1:N 단방향 매핑일 경우 객체 연관관계는 다음과 같다.

객체 연관관계

(그림이 개판이긴 하지만 이해좀...)

 

하지만 문제는 테이블 연관관계에서다.

테이블 연관관계

객체 관계에서는 Team이 Member를 가지고 있는데

테이블 관계에서는 Member가 Team 외래 키를 가지고 있는 아이러니한 상황..

이러한 이유는 테이블 연관관계에서는 항상 쪽에 외래 키가 있기 때문이다.

코드로 표현해보자.

@Entity
public class Team {

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

    @OneToMany
    @JoinColumn(name = "team_id")
    private List<Member> members = new ArrayList<>();
}

일대다 단방향 관계에서 주의할 점@JoinColumn을 꼭!!! 명시해주어야 한다.

그렇지 않으면 조인 테이블 전략을 사용해 중간 테이블을 만들어버리기 때문이다.

조인 테이블 전략은 장점도 있지만 테이블이 하나 더 생기게 되므로 성능상의 문제, 운영하기가 쉽지않다.

 

일대다 단방향의 문제점

  1. 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
  2. 연관관계 관리를 위해 추가로 UPDATE 쿼리가 발생한다.

 

Member memberA = new Member();
Member memberB = new Member();

Team team = new Team();
team.getMembers().add(memberA);
team.getMembers().add(memberB);

em.persist(memberA); // INSERT memberA
em.persist(memberB); // INSERT memberB
em.persist(team); // INSERT team, UPDATE memberA FK, UPDATE memberB FK

실무에서는 테이블이 엄청 많을텐데,

이런식으로 내가 예상하지 못한 곳에서 update 쿼리가 발생한다면 문제가 심각할 것 같다.

 

이러한 문제점 때문에 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것을 권장한다.

 

일대다 양방향 관계

@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;

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

    @ManyToOne
    @JoinColumn(name = "team_id", insertable = false, updatable = false) // 읽기 전용으로 만듬
    private Team team;
}

일대다 관계에서는 일(1)이 연관관계의 주인이 된다.

참고 ) 다중성은 왼쪽을 연관관계의 주인으로 정한다.

따라서 member가 아닌 team이 연관관계의 주인이 되는 상황이 벌어진다.

하지만 ToOne연관관계는 mappedBy를 사용하지 못한다.

꼼수로 @JoinColumn(name = "team_id", insertable = false, updatable = false)처럼 읽기 전용처럼 만드는 수법을 사용한다.

사실 이 방식은 일대다 양방향이라기 보다는 마치 일대다 양방향처럼 보이게 하는 방법이다.

따라서 일대다 단방향이 가지는 단점을 그대로 가지고 있다.

그러니 될 수 있으면 다대일 양방향 사용하세요..

 

회고

객체 관계와 테이블 관계간의 차이가 있어서 이 부분을 이해하는데 매우 햇갈렸고 시간도 오래걸렸다..

연관관계를 제대로 이해하지 않고 사용한다면 심각한 문제가 발생할 수 있겠구나라는 생각도 들었다.

그래서 아직도 많은 기업들이 마이바티스를 사용하는걸까?

하지만 JPA를 제대로 이해한다면 비즈니스 로직에 좀 더 집중할 수 있고

객체 중점으로 개발할 수 있을 것이라 생각된다.

더 열심히 공부해야지~

 

참고

- 자바 ORM 표준 JPA 프로그래밍 - 기본편

반응형

'JPA' 카테고리의 다른 글

JPA 를 공부하면서 알게 된 내용 정리 2  (0) 2022.01.29
JPA 를 공부하면서 알게 된 내용 정리 1  (0) 2022.01.26
연관관계 매핑 기초  (0) 2020.04.06
엔티티 매핑  (0) 2020.04.06
영속성 관리 - 내부 동작 방식  (0) 2020.04.03

댓글