Peony의 기록 창고 🌼
article thumbnail
반응형

단방향 연관관계

목표

  • 객체와 테이블 연관관계의 차이를 이해해야 한다.
  • 객체의 참조와 테이블의 외래 키를 어떻게 매핑하는지에 대해 이해한다.
  • 용어 이해
    • 방향(Direction): 단방향, 양방향
    • 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
    • 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리가 필요

 

연관관계가 필요한 이유

: 객체지향 설계의 목표는 자율적인 객체들의 협력 공통체를 만드는 것이다.

 

예제 시나리오

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원(N)과 팀(1)은 다대일 관계이다.

 

객체를 테이블에 맞추어 모델링

 

객체를 테이블에 맞추어 모델링 코드

 @Entity
 public class Member {
   
   @Id @GeneratedValue
   @Column(name = "MEMBER_ID")
   private Long id;
 ​
   @Column(name = "USERNAME") 
   private String name;
 ​
   @Column(name = "TEAM_ID")
   private Long teamId; 
   ...
 }
 ​
 @Entity
 public class Team {
   
   @Id @GeneratedValue
   @Column(name = "TEAM_ID")
   private Long id;
   private String name; 
   ...
 }
  • 참조 대신에 외래 키를 그대로 사용한다.
  • 객체를 위와 같이 테이블에 맞춰 모델링을 할 경우 생기는 문제는 무엇일까?

 

객체를 테이블에 맞춰 모델링 했을 경우 DB에 저장&조회하는 로직

 package hellojpa;
  
 import javax.persistence.EntityManager;
 import javax.persistence.EntityManagerFactory;
 import javax.persistence.EntityTransaction;
 import javax.persistence.Persistence;
  
 public class JpaMain {
     public static void main(String[] args) {
         EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
         EntityManager em = emf.createEntityManager();
         EntityTransaction tx = em.getTransaction();
         tx.begin();
         try {
             //팀 저장
             Team team = new Team();
             team.setName("TeamA");
             em.persist(team);
  
             //회원 저장
             Member member = new Member();
             member.setName("member1");
             member.setTeamId(team.getId()); 
             em.persist(member);
  
             tx.commit();
         } catch (Exception e) {
             tx.rollback();
         } finally {
             em.close();
         }
         emf.close();
     }
 }
  • 외래 키 식별자를 직접 다루고 있다.
  • 이럴 경우 문제는? → 조회할 때 해당 외래키를 가지고 조인 쿼리를 직접 짜야한다.

 

Q. member1 이 소속된 팀 정보를 조회하려면?

 //조회
 Member findMember = em.find(Member.class, member.getId());
 //연관관계가 없음
 Team findTeam = em.find(Team.class, team.getId());
  • 매번 member를 우선 조회한 뒤 외래키를 뽑아 그것으로 팀의 정보를 조회해야한다.
    → 협력관계를 만들 수 없다.

 

정리 : 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
1. 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
2. 객체는 참조를 사용해서 연관된 객체를 찾는다.
3. 테이블과 객체 사이에는 이런 큰 간격이 있다.

 

객체 지향 모델링 (객체 연관관계 사용)

 

객체 지향적으로 엔티티 설계(객체 연관관계 사용)

 package hellojpa;
 ​
 import javax.persistence.*;
 ​
 /* 회원(Member) 엔티티*/
 @Entity
 public class Member {
 ​
     @Id @GeneratedValue
     private Long id;
 ​
     @Column(name = "USERNAME")
     private String name;
     
     @ManyToOne
     @JoinColumn(name = "team_id")
     private Team team;
     
     ... getter, setter
 }
  • @ManyToOne - 멤버 입장에선 여러 멤버가 하나의 팀에 가입 가능 = 다대일
  • @JoinColume(name = "TEAM\_ID") - 조인이 필요한 대상의 컬럼 네임 지정

 

 

 try{
     Team team = new Team();
     team.setName("teamA");
     em.persist(team);
 ​
     Member member = new Member();
     member.setName("mamber1");
     member.setTeam(team);
     em.persist(member);
     
     em.flush();
     em.clear();
 ​
     Member findMember = em.find(Member.class, member.getId());
     Team findTeam = member.getTeam();
     
     tx.commit();
 }catch(Exception e ){
     tx.rollback();
 }finally {
     em.close();
 }
  • 단방향 연관관계를 매핑해서 객체지향적으로 테스트 해볼 수 있다.
  • 현재는 em.find에서 1차 캐시의 값을 바로 가져오기 때문에 별도의 쿼리문이 출력되지 않지만, 해당 부분을 보고 싶다면 em.flush();, em.clear();를 적용시켜줘서 영속성 컨텍스트를 초기화 시켜주면된다.

 

현재는 Member에서 Team으로는 갈 수 있지만, Team에서 Member로는 갈 수 없는 상황이다. 이어서 양방향 연관관계에 대해 알아보자.

 

양방향 연관관계 : 연관관계의 주인

  • 단방향에서 양방향이 된다는 것의 의미는 양측에서 서로를 참조할 수 있다는 것이다.
  • 테이블 관계는 FK 하나로 조인을 하기 때문에 FK가 들어간 순간 자동으로 양방향 관계다.
  • 객체 관계는 추가로 Team에다가 List members를 넣어줘야만 양방향 관계를 만들 수 있다.

Team객체에 members라는 List를 추가해서 양방향 연관관계를 만들어주자.

 @Entity
 public class Team{
     ...
     @OneToMany(mappedBy = "team")
     private List<Member> members = new ArrayList<>();
     ...
 }
  • Team 입장에선 OneToMany - 일대다 : mappedBy로 Member의 Team team 설정한 부분의 이름을 매핑 : 하인 입장 (읽기만 가능)
  • 추가한 members를 확인하는 코드를 작성해보자.
 Member findMember = em.find(Member.class, member.getId());
 List<Member> members = findMember.getTeam().getMembers();
 for (Member member1 : members) {
     System.out.println("member1.getName() = " + member1.getName()); // member1.getName() = mamber1
 }
  • 이제 반대방향으로도 객체 그래프 탐색이 가능해졌다

 

연관관계의 주인과 mappedBy

  • mappedBy = 연관관계의 개념에 대해 이해를 어렵게 만드는 주범!
  • 객체와 테이블간 연관관계를 맺는 차이를 이해해야 한다.

 

그렇다면 언제 mappedBy를 사용해야 할까? 이 부분을 정확하게 이해해야 한다.

 

객체와 테이블이 관계를 맺는 차이란?

  • 객체 연관관계 : 단방향 2개
    • 회원 → 팀 연관관계 1개(단방향)
    • 팀 → 회원 연관관계 1개(단방향)
  • 테이블 연관관계 : 1개 (조인 연산)
    • 회원 ↔️ 팀의 연관관계 1개(양방향)

 

결국 양방향 관계란?

  • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
    • A → B(a.getB())
    • B → A(b.getA())
  • 테이블에서 양방향으로 참조할려면 외래 키 하나로 연관관계를 가진다.(양쪽으로 조인할 수 있다.)
 SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
 ​
 SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

  • 두 객체에서 서로가 서로를 참조하는 값을 만들어 놓은 상황이다.
  • 여기서 어떤 것으로 DB에서 외래키를 만들어 관리해야 할까? : 테이블에 있는 FK를 변경해야 되는데 양방향이다 보니 Member에있는 Team을 변경해야할지, Team에 있는 List members를 변경해야 할지 딜레마가 온다. → 연관관계의 주인(Owner)를 정해야 한다.

 

연관관계의 주인 - 양방향 매핑 규칙

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만이 외래키를 관리 (등록, 수정)한다.
  • 주인이 아닌 쪽은 읽기만 가능하다.
  • 주인은 mappedBy 속성 사용 X
  • 주인이 아니면 mappedBy 속성으로 주인 지정한다.

 

그럼 누구를 주인으로 할까?

  • 외래키가 있는 곳을 주인으로 정하자.
  • 여기서는 Member.team이 연관관계의 주인. [ 多 쪽이 주인 (외래키가 있는 곳) ]
  • 반대의 경우라면, Team에서 members를 수정하면 Member에 업데이트 쿼리가 나간다. (성능면에서도 부적절)

 

결론: 외래키가 있는곳을 주인(Owner)로 결정하라.

 

주의점

양방향 매핑시 가장 많이 하는 실수

: 연관관계 주인에 값을 입력하지 않음

 Member member = new Member();
 member.setName("mamber1");
 em.persist(member);
 ​
 Team team = new Team();
 team.setName("teamA");
 ​
 team.getMembers().add(member);
 ​
 em.persist(team);

 

< 실행 결과 >

 

결론: 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
순수한 객체 관계를 고려하면 양쪽 다 값을 입력하는 것이 좋다.

 

❓만약, 여기서 team.getMembers().add(member);을 넣지 않는다면 어떤 문제가 있을까?

→ DB에 반영하는 부분에는 문제가 없다. 하지만, 영속화 컨텍스트의 1차 캐시에 저장된 team의 members에는 해당 Member가 추가되지 않은 상태이다. 이런 상황에서 team.members를 사용하게 된다면 DB에서 조회하는게 아닌 1차 캐시에서 꺼내 사용하기 때문에 해당 member가 추가되지 않은 결과가 반환 될 것이고, 문제가 생기게 된다.

⇒ 양쪽 모두 값을 세팅해주자.

 

💡TIP: 연관관계 편의 메서드를 생성하자.

 public void changeTeam(Team team) {
     this.team = team;
     team.getMembers().add(this);
 }
  • 이러면 Team을 세팅하는 시점에서 team에 Members도 같이 추가된다.
  • 연관관계 편의 메서드같은 옵셔널 메서드는 관례적으로 쓰이는 Getter, Setter가아닌 사용자 정의 메서드명(임의)으로 정의해주는게 좋다.

 

⚠️ 양방향 매핑시 무한 루프를 조심하자. (순환 참조)

→ ex) toString(), lombk , JSON 생성 라이브러리

 

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료 ! : 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
  • JPQL에서 역방향으로 탐색할 일이 많다.
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 X)
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.
    • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.

 

실전 예제 2 - 연관관계 매핑 시작

테이블 구조는 이전 포스팅과 동일하다. 게터와 세터는 모두 들어가있지만 코드에는 생략했다. 단방향 연관관계 매핑으로 리팩토링해보자.

 

객체 구조

  • 서로간에 참조가 가능하도록 변경되었다.
  • 연관관계의 주인은 테이블구조의 외래키가 있는 위치를 기준으로 한다.

 

주문 테이블

 package jpabook.jpashop.domain;
  
 import javax.persistence.*;
 import java.time.LocalDateTime;
  
 @Entity
 @Table(name = "ORDERS")
 public class Order {
  
     @Id @GeneratedValue
     @Column(name = "ORDER_ID")
     private Long id;
  
 //    @Column(name = "MEMBER_ID")
 //    private Long memberId;
  
     @ManyToOne
     @JoinColumn(name = "MEMBER_ID")
     private Member member;
  
     private LocalDateTime orderDate;
  
     @Enumerated(EnumType.STRING)
     private OrderStatus status;
 }

 

주문 상품 테이블

 package jpabook.jpashop.domain;
  
 import javax.persistence.*;
  
 @Entity
 public class OrderItem {
  
     @Id @GeneratedValue
     @Column(name = "ORDER_ITEM_ID")
     private Long id;
  
 //    @Column(name = "ORDER_ID")
 //    private Long orderId;
  
     @ManyToOne
     @JoinColumn(name = "ORDER_ID")
     private Order order;
  
 //    @Column(name = "ITEM_ID")
 //    private Long itemId;
  
     @ManyToOne
     @JoinColumn(name = "ITEM_ID")
     private Item item;
  
     private int orderPrice;
  
     private int count;
 }

 

이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.

 

 

반응형
profile

Peony의 기록 창고 🌼

@myeongju