[JPA] 연관관계 매핑 정리

3Beom's 개발 블로그·2022년 10월 25일
0

SpringJPA

목록 보기
6/21
post-custom-banner

출처

본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편 을 수강하며 기록한 필기 내용을 정리한 글입니다.

-> 인프런
-> 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의


0. 예제

  • USER, ORDERS, ORDER_COOKIE, COOKIE, DELIVERY 5가지 테이블이 있다.
  • 각 테이블마다 상단 양쪽에 이름이 두개 적혀있는데, 각각은 아래와 같다.
    -> 좌측 파스칼 표기 : JPA 상에서 구현되는 엔티티의 이름
    -> 우측 대문자 표기 : DB에 생성되는 테이블 이름
  • 각 필드마다 두가지 이름이 있는데, 각각은 아래와 같다.
    -> 좌측 카멜 케이스 표기 : JPA 상에서 구현되는 엔티티의 필드 이름
    -> 우측 대문자 표기 : DB 테이블 내 컬럼 이름
  • 노란색 열쇠가 표기된 컬럼은 PK를, 분홍색 열쇠가 표기된 컬럼은 FK이다.
  • 테이블 간에 연결된 분홍색 점선은 비식별 연관관계를 의미하며, 각 테이블 간의 연관관계는 다음과 같다.
  1. USER : ORDERS = 1 : N
    • 한 명의 유저가 여러 주문을 할 수 있다. (이것도 사고 저것도 사고..)
    • 하나의 주문을 여러 유저가 할 수는 없다. (여러명이 모여 하나의 주문을 할 수 없다.)
  2. ORDERS : COOKIE = N : M 관계이며, 중간에 'ORDER_COOKIE'를 두어 1 : N, N : 1 관계로 구현한다.
    • 하나의 주문에 여러 쿠키들이 주문될 수 있다.
    • 하나의 쿠키가 여러 주문에 의해 팔릴 수 있다.
  3. ORDERS : DELIVERY = 1 : 1
    • 하나의 주문이 여러번의 배달로 처리될 수 없다.
    • 하나의 배달은 하나의 주문만 처리한다.



<용어를 다음과 같이 설정한다.>

  • 테이블 : DB의 테이블
  • 엔티티 : JPA를 활용하여 DB 테이블과 매핑되는 클래스
  • 컬럼 : DB 테이블의 컬럼
  • 필드 : JPA를 활용하여 DB의 컬럼과 매핑되는 멤버변수

(해당 용어들은 각각 다른 의미도 갖지만, 편의상 이렇게 설정한다.)

1. 연관관계 매핑

  • DB 테이블 간의 연관관계를 JPA의 엔티티와 연결짓기 위해 연관관계 매핑을 해주어야 한다.
  • 연관관계 매핑 종류는 다음과 같다.
  • 다대일 매핑
  • 일대다 매핑
  • 일대일 매핑
  • 다대다 매핑
  • 앞에 붙은 글자의 테이블과 매핑된 엔티티가 연관관계 주인 필드를 갖는다.
    (다대일 매핑은 '다' 쪽의 테이블과 매핑된 엔티티가 연관관계 주인 필드를 갖는다.)

연관관계 주인 필드?
-> DB의 FK 컬럼과 매핑된 필드

2. 다대일 매핑 (@ManyToOne)

<예제의 'User - Order' 관계를 토대로 살펴본다.>

@ManyToOne
@JoinColumn(name = "USER_ID")
private User user; // 연관관계 주인 필드
  • 바로 직전 글에서 다루었던 내용과 동일하다.
    [JPA] 1:N 단•양방향 연관관계 매핑 (@ManyToOne, @OneToMany)
  • 가장 많이 사용되는 연관관계이다.
  • @ManyToOne 어노테이션으로 매핑한다.
  • @JoinColumn 어노테이션에 테이블의 FK 컬럼 명을 전달한다.
    -> SQL에서 Join문을 활용할 때, 기준이 되는 컬럼 명을 전달한다.
  • 1:N(=N:1) 관계에서 N 쪽이 연관관계 주인 필드를 갖게 된다.
    -> N 쪽의 테이블과 매핑된 엔티티 안에 DB의 FK 컬럼과 매핑된 필드가 있다.
    -> 예제에서 'User : Order = 1 : N' 이므로, Order 엔티티의 user 필드가 연관관계 주인 필드이다.

2-1. 상대방 객체에 참조 추가

@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
  • 1 쪽의 엔티티가 N 쪽의 엔티티에 참조를 통해 접근하기 위한 매핑이다.
    -> User 엔티티 내에 Order 객체 필드를 둬서, 참조를 통해 Order 객체에 접근할 수 있게 된다.
  • 다대일 양방향 매핑이라고도 한다.
    (그냥 다대일 단방향 매핑에서, 상대방 엔티티도 접근할 수 있도록 설정해 주는 것이다.)
  • @OneToMany 어노테이션을 활용하며, mappedBy 속성에 연관관계 주인 필드명을 설정해주어야 한다.
    -> mappedBy = "user"
  • 이렇게 설정하면 아래와 같이 1 쪽의 엔티티 객체에서도 N 쪽의 엔티티 객체 필드를 갖게 되므로, 참조를 통해 접근할 수 있다.
User user = entityManager.find(User.class, 1L);
List<Order> orders= user.getOrders();
for (Order o : orders){
	System.out.println("Order id : " + o.getId());
}

3. 일대다 매핑 (@OneToMany)

<예제의 'User - Order' 관계를 토대로 살펴본다.>

@OneToMany
@JoinColumn(name = "USER_ID")
private List<Order> orders = new ArrayList<>();
  • 다대일 매핑과 달리, 일대다는 1:N(=N:1) 연관관계에서 1 쪽이 연관관계 주인 필드를 갖는다.
    -> 1 쪽의 테이블과 매핑된 엔티티에서 DB의 FK 컬럼을 관리한다.
  • 권장되는 매핑 방식은 아니며, 그 이유는 다음과 같다.

<일대다 매핑이 권장되지 않는 이유>

  • 일대다 매핑은 연관관계 주인 필드를 '일' 쪽의 엔티티가 갖게 된다.
  • 하지만 DB 테이블 구조 상, '일' 쪽이 아닌 '다' 쪽이 FK 컬럼을 갖는다.
  • 따라서 '일' 쪽에서 persist() 하면, '다' 쪽의 테이블로 쿼리가 나간다.
    - 예제에서 User 객체를 DB에 저장하려고 persist() 했는데, ORDER 테이블로 쿼리가 나간다.
  • 즉, 개발 과정에서 예상치 못한 쿼리가 다른 테이블로 전달 될 수 있다.
  • Main에서는 다음과 같이 활용될 수 있다.
Order order = new Order();
em.persist(order)

User user = new User();
user.setName("user1");
user.getOrders().add(order);
em.persist(user);
  • 이렇게 구현하고 실행해 보면 다음과 같이 쿼리가 나간다.
    • ORDER 테이블로 INSERT 쿼리
    • USER 테이블로 INSERT 쿼리
    • ORDER 테이블로 UPDATE 쿼리
  • 뭔가 이상하게 동작하는 것을 확인할 수 있다.

⇒ ⭐아무리 다대일 매핑이 안맞는 상황이라도, 일대다 매핑보다는 차라리 다대일 양방향 매핑을 사용하자.⭐

4. 일대일 매핑 (@OneToOne)

<예제의 'Order - Delivery' 관계를 토대로 살펴본다.>

// Order class
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
  • 일대일 연관관계는 다대일, 일대다 연관관계의 FK에 유니크 제약조건을 추가한 것이다.
  • FK 컬럼은 어느 테이블에 둬도 문제 없이 동작한다.
    -> 예제에서는 ORDER 테이블에 FK 컬럼으로 'DELIVERY_ID'를 설정하였다.
  • @OneToOne 어노테이션이 활용된다.
  • @JoinColumn 어노테이션은 꼭 써주는게 좋다.

4-1. 상대방 객체에 참조 추가

// Delivery class
@OneToOne(mappedBy = "delivery")
private Order order;
  • 동일하게 일대일 관계이므로 @OneToOne 어노테이션이 활용된다.
  • 다대일 매핑과 유사하게 mappedBy 속성으로 연관관계 주인 필드를 가리켜야 한다.

4-2. FK는 어느 테이블에?

  • 일대일 관계는 어느 곳에 FK를 두어도 기능은 할 수 있다.
  • 하지만 상황에 맞게 잘 설정하면 미래에 도움이 될 수 있다.
  • 다음과 같이 정리해 보았다.
  1. 지금은 일대일이지만, 나중에 한쪽이 다수가 될 가능성이 있을 경우, 다수가 될 가능성이 높은 쪽에 FK를 두는게 더 이득이다.
    • 만약 나중에 하나의 배달로 여러 건의 주문을 해결할 수 있도록 로직이 변경될 경우에 대해 생각해 본다.
      - 즉, Order : Delivery = N : 1 로 로직이 변경된다.
      - 처음부터 ORDER 테이블에 FK 컬럼을 뒀다면, 그냥 해당 FK에 유니크 제약조건만 없애면 된다.
      - 하지만 DELIVERY 테이블에 FK 컬럼을 뒀다면, 해당 FK 컬럼을 지우고 ORDER 테이블에 FK 컬럼을 추가해야된다.

  2. 만약 그런 기획이 보이지 않으면, JPA 개발 과정에서 여러번 쓰이는 쪽에 FK를 두는게 좋을 수 있다.
    • 예제를 두고 보면, 사실 배달 정보보다는 주문 정보가 더 많이 쓰일 것이다.
    • 만약 주문 데이터들 중, 배달 상태에 따라 뭔가 작업을 해야하는 경우,
      • ORDER 테이블에 FK가 있으면, JPA에서 Order 데이터 find() 할 때 어차피 Delivery 데이터도 딸려오게 된다.
        -> 연관관계 주인 필드가 있으므로
      • 하지만 DELIVERY 테이블에 FK가 있으면 많진 않지만 추가해야 할게 생긴다.
    • 큰 차이는 아닐 수 있지만, 쪼끔 더 나아질 수 있다.
    • 또한, 덜 쓰이는 쪽에 FK를 두면, 지연 로딩 기능이 제한될 수 있다고 한다.

⭐️ 상황에 따라 DB 개발자와 논의 후 적절한 방식을 택하면 된다.

5. 다대다 매핑 (@ManyToMany)

  • 다대다 매핑은 안쓰는게 좋다. 쓰지 말자.
  • 원래 RDB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
  • 즉, 중간에 연결 테이블을 추가해서 일대다 - 다대일 관계로 만들어야 한다.
  • 하지만 JPA 에서는 다대다가 가능하다.
    (그냥 서로를 참조할 수 있는 컬렉션을 갖고 있으면 된다.)
  • 객체에서는 구현이 가능하지만, 관계형 데이터베이스에서는 불가능하기 때문에 두 테이블 사이에서 서로 연결시킬 수 있는 Join 테이블을 추가하는 방식으로 구현된다.
// Order class
@ManyToMany
@JoinTable(name = "ORDER_COOKIE",
		joinColumns = @JoinColumn(name = "ORDER_ID"),
        inverseJoinColumns = @JoinColumn(name = "COOKIE_ID"))
private List<Cookie> cookies = new ArrayList<>();
@ManyToMany(mappedBy = "cookies")
private List<Order> orders = new ArrayList<>();
  • @ManyToMany 어노테이션을 활용한다.
  • @JoinTable 어노테이션으로 연결 테이블을 지정한다.
  • @JoinTable 어노테이션의 joinColumns 속성에 본인의 PK를, inverseJoinColumns 속성에 상대방의 PK를 넣는다.
    -> 그럼 본인의 PK와 상대방의 PK를 가리키는 FK 컬럼 두개로 이루어진 연결 테이블이 중앙에 생성된다.
  • 하지만 해당 매핑 방법은 쓰지 않는 것이 좋다.

<다대다 매핑 쓰면 안되는 이유>

  • 보통은 연결 테이블이 단순히 연결만 시키는 용도로 쓰이지 않는다.
    • 무조건 주문 시간, 수량 등 추가되는 데이터가 있다.
  • 하지만 다대다 매핑 방식을 쓸 경우, 연결 테이블을 JPA에서 관리하지 않게 된다.
    • JPA에서 그냥 자동으로 연결 테이블을 만들어 버리기 때문이다.
    • 연결 테이블과 매핑된 클래스가 없다.

<다대다 대체 방법>

  • 연결 테이블을 JPA에서 하나의 엔티티로 만든다.
    • 예제에서는 'OrderCookie' 클래스를 만들고, DB의 ORDER_COOKIE 테이블과 매핑시킨다.
  • @OneToMany - @ManyToOne 관계를 양쪽으로 두개 만든다.
    • Order : OrderCookie = 1 : N
    • Cookie : OrderCookie = 1 : N
  • 이 때, 연결 테이블이어도 본인만의 PK를 갖고 있는게 좋다.
    • ORDER_ID, COOKIE_ID 가 FK이자 PK 인 것으로 설정하는 것 보다, ORDER_COOKIE_ID 컬럼을 둬서 PK로 만들고, FK는 FK로만 두는게 좋다.
    • FK이자 PK로 설정하게 되면, 미래에 예상치 못한 제약이 생길 수 있다. PK는 최대한 제약 없이, 다른 데이터들과 아무 상관 없는 값이어야 한다.
profile
경험과 기록으로 성장하기
post-custom-banner

0개의 댓글