제육's 휘발성 코딩
반응형

목표

  • 객체와 테이블 연관관계의 차이를 이해
  • 객체의 참조와 테이블의 외래 키를 매핑
  • 용어 이해
    • 방향 : 단방향, 양방향
    • 다중성 : 다대일, 일대다, 일대일, 다대다
    • 연관관계의 주인 : 객체 양방향 연관관계는 관리 주인이 필요

연관관계가 필요한 이유

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

예제 시나리오

  • 회원과 팀 존재할 때, 회원은 하나의 팀에만 소속 = 하나의 팀에는 여러 회원 소속
  • 회원과 팀은 다대일 관계를 갖는다.

테이블 중심 설계시 문제점

다음과 같이 설계했을 때 발생하는 문제점에 대해 알아보자.

회원 테이블

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "TEAM_ID")
    private Long teamId;

팀 테이블

@Entity
public class Team {

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

  private String name;
}

JpaMain

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

            Member findMember = em.find(Member.class, member.getId());
            Long findTeamId = findMember.getTeamId();
            Team findTeam = em.find(Team.class, findTeamId);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 테이블에 맞추어 설계할 경우 객체 지향스럽지가 않다.
  • 조회하려면 Member의 Id를 찾고 그 값으로 TeamId를 찾고 등..

단방향 연관관계

sec4  사진5

  • 현재는 Team에서 Member로 갈 수 없는 상황으로 보자. (단방향)

Member 엔티티 수정

package hellojpa;

import javax.persistence.*;

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

    @ManyToOne // 해당 클래스의 관점에서 지정
    @JoinColumn(name = "TEAM_ID") 
    private Team team;
}
  • @ManyToOne - 멤버 입장에선 여러 멤버가 하나의 팀에 가입 가능 = 다대일
  • @JoinColume(name = "TEAM\_ID") - 조인이 필요한 대상의 컬럼 네임 지정
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.setTeam(team);
            em.persist(member);

            Member findMember = em.find(Member.class, member.getId());
            Team findTeam = findMember.getTeam();
            System.out.println("findTeam = " + findTeam.getName());

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

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

양방향 연관관계 , 연관관계 주인

sec4  사진6

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

양방향 관계를 위한 Team 수정

@Entity
public class Team {

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

    @OneToMany(mappedBy = "team") //Member의 Team team 부분 매핑 설정 (team이 연관관계 주인이다.)
    private List<Member> members = new ArrayList<>();
}
  • Team 입장에선 OneToMany - 일대다
  • mappedBy로 Member의 Team team 설정한 부분의 이름을 매핑 - 하인 입장 (읽기만 가능)
  • 객체는 사실 양방향 설정을 하려고 하면, 단방향을 2개 설정해야 하는 것이다. (테이블 연관관계는 양방향1개로 끝)

JpaMain

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

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.setTeam(team);
            em.persist(member);

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member.getId());
            Team findTeam = findMember.getTeam();
            List<Member> members = findTeam.getMembers();
            for (Member m : members) {
                System.out.println("m = " + m.getName());
            }
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 양방향 연관관계 설정으로 이제 Team에서도 멤버 객체를 가져올 수 있다.

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

 

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

  • 객체 연관관계 : 단방향 2개 (회원 -> 팀, 팀 -> 회원)
  • 테이블 연관관계 : 양방향 1개 (조인 연산)

sec4  사진7

  • 테이블에 있는 FK를 변경해야 되는데 양방향이다 보니 Member에있는 Team을 변경해야할지, Team에 있는 List members를 변경해야 할지 딜레마가 온다. 연관 관계 주인으로 해결하자.

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

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

sec4  사진8

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

잘못된 매핑 예시

image

  • TEAM_ID 가 null 이된다. 주인에 값을 안넣고 주인이 아닌 곳에 값을 넣으려고 함 - 주의
  • 주인에만 값을 넣어도 되지만, 양쪽에다 값을 모두 넣어주는게 맞다. (객체지향적 관점)

올바른 매핑 예시

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

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");
//            team.getMembers().add(member);
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);

            team.getMembers().add(member);

            em.flush();
            em.clear();

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • 연관관계 주인에만 값을 넣어줘도 되지만, 객체지향 관점에서 읽기 전용인 반대편에도 넣어주는 것이 좋다. 즉, 양 쪽에 모두 값을 넣어주는 것이 바람직하다. 그 이유는 주인 쪽에만 값을 넣어줘도 지연로딩을 통해 커밋 시점에서 동작하지만, 트랙잭션 안에서 값을 꺼내는 경우에 하인 쪽의 값은 조회가 되지 않기 때문에 별도의 flush가 필요하기 때문이다.

연관관계 편의 메서드

// 주인 엔티티에서 양방향 엔티티를 설정하는 방식
public void changeTeam(Team team) { //setTeam을 비즈니스로직이 들어간 다른 메서드로 변경
  this.team = team;
  team.getMembers().add(this); //this 나 자신 (Member) 
}
  • 주인이나 하인 쪽 엔티티에서 양방향으로 값을 넣어주어서 단방향 매핑만으로 연관관계 매핑을 할 수 있다.
  • 무한루프 주의(toString, lombok, JSON 생성 라이브러리 - 컨트롤러에서 엔티티 반환 X, DTO 사용 (스펙이 바껴버림))
    • Member에서도 Team정보를 참조, Team에서도 Member정보를 참조를 무한 반복하는 현상

양방향 매핑 정리

  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다.
  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다. 즉, 단방향으로 설계를 마치자.
  • 양방향 매핑은 객체 그래프 탐색 기능(반대 방향으로 조회)이 추가된 것이다.
  • JPQL에서 역방향 탐색할일이 많으므로 단방향 매핑을 잘 해두고, 추후 필요시 양방향 매핑을 하자.

 

예제

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

객체 구조

image

  • 참조를 사용하도록 변경한다.

주문 테이블

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

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^