Java - SpringJPA

Spring JPA (5) - JPA 개념 잡기 : 단방향 연관관계, 양방향 연관관계, 연관관계의 주인

TerianP 2022. 10. 3.
728x90

1. 연관관계 매핑의 기초

- 객체와 테이블 연관관계 차이를 이해

- 객체의 참조와 테이블 외래키를 매핑 => 객체 지향적 모델링은 단순히 FK 에 해당하는 변수를 사용하는 것이 아닌 객체의 참조를 FK 로 사용하는것!!

Member 객체에 teamId 대신 Team 객체가 참조값으로 들어간다

 

- 간단 용어 정리!!

용어 설명
방향 Direction 단방향, 양방향
다중성 Multiplicity 다대일 N:1, 일대다 1:N, 일대일 1:1, 다대다 N:M
연관관계의 주인 Owner 객체 양방향 연관관계 관리

 

테이블과 객체 사이의 간격!!

- 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다

- 객체는 참조를 사용해서 연관된 객체를 찾는다.

 

예제 시나리오
- 회원과 팀이 존재하며, 회원은 하나의 팀에만 소속되고, 하나의 팀에는 여러 회원이 소속될 수 있다
- 회원과 팀은 다대일의 관계를 갖는다

 

1) 기억하자!! @JoinColumn

@JoinColumn 에 해당하는 컬럼은 FK 가 있는 컬럼을 의미한다.
따라서 name = "FK컬럼명" 을 매핑해주면 된다

 

2. 단방향 연관관계

- 단방향은 말 그대로 한쪽에서 다른 한쪽으로 흐르는 일종의 시냇물 이라고 생각하면 된다 => 위에서 아래로 한쪽으로만 흐는 관계

- 이렇게 단방향 관계를 지정해두지 않으면 member_id = 1 인 teamName 을 가져오는 아주 간단한 것이라도 아래처럼 복잡한 과정을 거쳐야 한다

// 관계 설정이 없는 경우
// Member 1 이 소속된 팀 이름을 가져오기 위해서는 아래의 과정을 거쳐야 한다
// member 를 가져와서 다시 member 의 teamId 를 가져와서 다시 팀 이름을 가져오는 아주아주 복잡한 과정
Member member = em.find(Member.class, 1L);
Team team = em.find(Team.class, member.getTeamId());

String teamName = team.getTeamName();

 

1) 단방향 연관관계 규칙 매핑 2가지

- 이것을 이제 단방향 연관관계를 매핑을 통해 보다 쉽게 바꿀 수 있다

       =>  단방향 연관관계 매핑을 아래의 2가지 규칙을 기억해서 해야한다

첫째로 중요한 것은 '어떤' 관계를 갖는지 알려주어야 한다
    => 1:1, 1:N, N:M 이고, 이때 사용하는 어노테이션이 ManyToOne, ManyToMany 등등이 있다
- 가장 중요하게 기억해야 할 부분은 어떤 관계를 갖는지 지정할때는 "현재 클래스" 를 기준으로 해야한다.
- Member(현재클래스) : Team 는 N:1 의 관계를 갖기 때문에 ManyToOne 어노테이션을 붙여주어야 한다

둘째로 중요한 것은 해당 PK 로 어떤 컬럼을 조인하는지 알려주어야 한다.
   => 이때 사용하는 어노테이션이 JoinColumn 어노테이션이고, 파라미터로 join 하는 컬럼명을 갖는다

 

2) 코드 만져보기

- Member

public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    /*
        아래처럼 단순히 PK 에 해당하는 teamId 를 사용하는 것이 아니라
        Team 객체 전체가 일종의 FK 로서 사용됨 => 객체끼리 관계를 형성
        private Long teamId;
    */


    /*
        team == PK 로 생각한다!!
        이때 주의해야 할 것들이 있다.
        첫째로 중요한 것은 '어떤' 관계를 갖는지 알려주어야 한다 => 1:1, 1:N, N:M 이고, 이때 사용하는 어노테이션이 ManyToOne, ManyToMany  등등이 있다
          - 가장 중요하게 기억해야 할 부분은 어떤 관계를 갖는지 지정할때는 "현재 클래스" 를 기준으로 해야한다.
          - Member : Team 는 N:1 의 관계를 갖기 때문에 ManyToOne 어노테이션을 붙여주어야 한다
        둘째로 중요한 것은 해당 PK 로 어떤 컬럼을 조인하는지 알려주어야 한다.
          - 이때 사용하는 어노테이션이 joinColumn 어노테이션이고, 파라미터로 join 하는 컬럼명을 갖는다
    */

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    private String userName;

}

 

- TeamName 가져오기

 // 단방향 관계 설정 후에는 아래와 같이 쉽게 가져올 수 있다
 // 34 번 유저가 속한 팀의 이름을 구하기 위해서는
 Member member = em.find(Member.class, 34L);

 // Team 과 관계설정이 되어있음으로 바로 가져올 수 있다
String teamName = member.getTeam().getTeamName();

left 조인문으로 join 해서 teamID 에 맞는 team 엔티티를 가져온다


- TeamName 변경하기

// 단방향 관계 설정 후에는 아래와 같이 쉽게 변경 가능하다

Member member = em.find(Member.class, 34L);

// teamName 을 바꿀때도 기존처럼 set 을 사용하면 된다
Team team = member.getTeam();

// 팀이름을 변경
team.setTeamName("TeammmmB");

// 변경된 team 객체를 member 에 set
member.setTeam(team);

join 후에 update 쿼리문도 실행된다


현재의 단방향 관계 모양

 

3. 양방향 연관관계와 연관관계의 주인

- 단방향 연관관계를 통해서 Member 를 통해서 Team 을 조회할 수 있게 되었다.

- 그러나 아직까지도 반대인 Team 에서 Member 를 조회할 수는 없다. Why? 아직 Team 과 Member 의 연관관계가 세워지지 않았기 때문에!!

- 사실 모든 DB 의 기본은 양방향 연관관계를 기본으로 한다. 이는 DB 에는 select * from member 나 select * from team 이나 모두 가능하기 때문이다 => 이제는 이렇게 양방향으로 조회가 가능하도록 하는 양방향 연관관계 매핑을 해볼 시간이다!

- 즉 Team 에 속해있는 Member 를 찾아오기 위한 연관관계 매핑을 하는 것!!

 

1) Team 코드 수정

public class Team {

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

    private String teamName;

    /*
        Member : Team 일 때는 Member 클래스를 기준으로 N:1 의 관계가 성립했다
        규칙 1 ) 그렇다면 자연스럽게 Team : Member 의 관계는 현재 클래스인 Team 을 기준으로
            - 1 : N 의 관계가 성립하며 이에 따라서 OneToMany 어노테이션을 사용한다.
        규칙 2 ) 동시에 mappedBy 파라미터를 사용하는데 이 파라미터의 값에는
            - 연관관계 주인쪽에서 현재 클래스를 지정하는 변수를 넣어준다 => Member 에서 현재 클래스 Team 을 지정하는 변수 team
            - 따라서 Team 쪽에서는 Member 와 매핑되는 이름이 team 임으로 파라미터의 값을 team 으로 적는것이다

        !!!! 사실 mappedBy 는 아주 어렵고, 중요한 개념으로 뒤에 다시 설명하도록 하겠다 !!!!
    */
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

}

2) 코드 확인

System.out.println("---------- 양뱡향 연관관계 --------------");
List<Member> list = member.getTeam().getMembers();
list.forEach(m ->{
    System.out.println(m.toString());
});

 

4. MappedBy

- 객체와 테이블이 관계를 맺는 차이를 이해해야 비로소 MappedBy 를 이해할 수 있다

사실 양방향 연관관계라고 해도 실제로는 단방향이 2개가 있는 모양이다.
객체 연관관계 = 2개 => 팀-> 회원(단방향), 회원->팀(단방향)


그러나 테이블에서의 연관관계는 1개 뿐이다
회원 <-> 팀 (양방향)
why? teamId 라는 PK 하나! 연결되어있기 때문

따라서! 우리도 team 과 members 둘 중 하나를 PK 로 설정해야한다
즉, 연관관계의 주인을 지정해줘야 한다

 

1) 연관관계의 주인 Owner

  • 양방향 매핑 규칙
    • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
    • 연관관계의 주인만이 외래 키를 관리 ( 등록, 수정)
    • 주인이 아닌 쪽은 읽기만 가능!!
    • 주인은 mappedBy 속성 사용 X => 주인이 아니면 mappedBy 로 주인을 지정해주어야 함!!!

 

2) 그래서 누구를 주인으로?

DB 테이블에서 외래 키 FK 가 있는 곳을 주인으로 정하자

- 아래 그림으로 봤을 때 TEAM_ID 가 PK 로 있는 쪽은 TEAM 테이블이고, TEAM_ID 가 FK 로 있는 쪽은 MEMBER 이다.

따라서 FK 있는 쪽이 주인임으로 연관관계의 주인은 MEMBER 클래스가 된다

=> 보통 연관관계 1:N 에서 N 인쪽이 주인으로 지정된다


3) 연관관계의 주인 주의점

- 연관관계의 주인쪽에서 FK 를 업데이트 하는 것이 아닌 가짜 매핑 즉 주인의 반대편에서 FK 를 업데이트 하면 제대로 DB 에 반영되지 않는다!!!

/* 옳지 못한 양방향 연관관계 매핑 */
/* 연관관계 주인이 아닌 쪽을 통해서 FK 를 수정하려고 할 때 */
// Member 생성
Member member = Member.builder()
        .userName("memberBB")
        .build();
em.persist(member);

// Team 생성
Team team = new Team();
team.setTeamName("TeamBB");

// Team 에 있는 members 에 member 객체 넣기
team.getMembers().add(member);

em.persist(team);

연관관계 주인이 아닌 쪽에서 FK 를 수정하려고 했기 때문에teamMember 에 team_id 에는 null 이 들어가게 된다


- 따라서 FK 를 업데이트 - 등록, 수정 - 하려고 한다면, 연관관계의 주인쪽에서 FK 를 다루고 활용해야 한다!

/* 옮게 된 양방향 연관관계 매핑 */
/* 연관관계 주인쪽에서 FK 를 등록 */
// Team 생성
Team team = new Team();
team.setTeamName("TeamCC");

// Member 생성
Member member = Member.builder()
        .userName("memberCC")
        // 연관관계의 주인이 되는 Member 쪽에서 team 을 넣어서 등록한다다
       .team(team)
        .build();
        
em.persist(member);

em.persist(team);

연관관계 주인 쪽에서 FK 를 등록했기 때문에 하려고 했기 때문에teamMember 에 team_id 에는 제대로 값이 들어가게 된다


4) 양방향 연관관계 매핑 시 양방향으로 세팅해야 한다!

예를 들어 em.persist 만 하고 아직 commit 이전 상태라면, 혹은 EntityManager 가 clear 된 상태라면 이렇게만 사용해서는 문제가 생긴다. 이는 Member 쪽에는 team 이 세팅되어있으나 team 의 members 즉 List<member> 에는 아직 member 가 add 되지 않았기 때문이다

// 만약 이 상태에서 바로 teamDD 의 members 를 가져온다면 어떻게 될까?
Team teamDD = em.find(Team.class, team.getId());
System.out.println("---- 출력 -----");
teamDD.getMembers().forEach(m -> {
    System.out.println(m.toString());
});
System.out.println("=========================");

출력 안된다!!

따라서 아래처럼 2중으로, 양방향으로 세팅해줘야 한다

/* 완전 옮게 된 양방향 연관관계 매핑 */
/* 연관관계 주인쪽에서 FK 를 등록 && 주인이 아닌쪽에서 FK 관련해서 세팅 */
// Team 생성
Team team = new Team();
team.setTeamName("TeamDD");
em.persist(team);

/* team 과 member 모두 2중으로 세팅한다 */
// Member 생성
Member member = Member.builder()
        .userName("memberCC")
        // member -> team : 연관관계의 주인이 되는 Member 쪽에서 team 을 넣어서 등록한다
       .team(team)
        .build();
em.persist(member);

// team -> member : Team 에 있는 members 에 member 객체 넣기
team.getMembers().add(member);

// 만약 이 상태에서 바로 teamDD 의 members 를 가져온다면 어떻게 될까?
Team teamDD = em.find(Team.class, team.getId());
System.out.println("---- 출력 -----");
teamDD.getMembers().forEach(m -> {
    System.out.println(m.toString());
});
System.out.println("=========================");


5) 양방향 연관관계 최종 정리

1. 순수 객체 상태를 고려해서 양쪽에 값을 설정하자
2. 연관관계 편의 메서드를 생성하자
3. 양방향 매핑 시 무한 루프에 주의하자 : Ex) ToString(), lombok, JSON 생성 라이브러리
   => 컨트롤러에서는 Entity 를 절대!! 반환하지 말 것, 가능한 DTO 로 변환해서 반환할 것
4. 단방향 매핑 만으로도 이미 연관관계 매핑 설계를 완료해야 함
     => 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
5. JPQL 에서 역방향으로 탐색할 일이 많음
6. 단방향 매핑을 잘하고 양방향 매핑은 필요할 때 추가해도 됨 => 테이블 영향을 주지 않음
7. 연관관계 주인의 기준은 비즈니스 로직을 기준으로 하는 것이 아닌 외래 키 FK 의 위치를 기준으로 정하자

 

연관관계 편의 메서드

일반적인 setter 메서드를 살짝 변형해서 1번에 해당하는 연관관계 주인과 아닌 쪽 모두의 값을 설정 할 때 보다 쉽게 하기위한 메서드

 // 연관관계 매핑을 위한 메서드
 // 이것을 통해서 team 을 넣어주면 해당 team 의 members 에 자기자신 객체를 넣는다
 // 이렇게 하면 뒤에서 team.getMembers().add(member) 따로 setter(Team team) 해서 2번 할 필요는 없어진다
public void addMembers(Team team){
     // this 는 현재 자기 자신 객체 의미한다
     team.getMembers().add(this);
 }
 

 ---------------------------------------
         
// 메서드가 동작하면서 Member 안에 team 에 매개변수로 던지는 team 을 세팅하고
 // 다시 team 의 members 에는 현재 member 자기 자기 객체를 추가한다
member.addMembers(team);

 

5. 실전 예제 코드로 만들기

- Member, Order ,OrderItem, Item 클래스가 있을 때 아래처럼 구성하려고 한다.

- 각 클래스 연관관계에 맞춰 매핑을 시작한다

1) Memeber

@Entity(name = "ShopMember")
@Table
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue // 전략 생략하면 AUTO
    @Column(name="MEMBER_ID")
    private Long id;
    private String name;
    private String city;
    private String street;
    private String zipcode;

    // 양방향 연관관계 매핑 => 현재클래스 : member
    // mappedBy 에는 연관관계의 주인쪽에서 현재 클래스를 부를 변수명
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

}

 

2) Order

package com.use.jpabasic.practice.domain;

import lombok.*;

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

@Entity
@Table(name = "ORDERS")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Order {

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

    // 현재 클래스 : MEMBER => 하나의 MEMBER 에 여러 ORDER 이 올 수 있음 -> 1:N
    @ManyToOne
    @JoinColumn(name="MEMBER_ID")
    private Member member;

    // 양방향 연관관계 => 현재클래스 : OrderItem = 1: N
    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();

    private LocalDateTime orderDate;

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

    // 양방향 연관관계 편의 메서드
    public void addOrderItems(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

}

 

3) Item

@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class Item {

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

    private String name;

    private int price;

    private int stockQuantity;

}

 

4) OrderItem

@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class OrderItem {

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

    // 현재 클래스 : ORDER => 하나의 ORDERITEM 에 여러 ORDER 이 올 수 있음 -> 1:N
    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

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

    private int orderPrice;

    private int count;
}

댓글