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

1. 목표

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

1.1. 연관관계가 필요한 이유

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

1.1.1. 예제 시나리오

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

1.1.2. 테이블 중심 설계시 문제점

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

1.1.2.1. 회원 테이블

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

1.1.2.2. 팀 테이블

<code />
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; }

1.1.2.3. JpaMain

<code />
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를 찾고 등..

1.1.3. 단방향 연관관계

sec4  사진5

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

1.1.3.1. Member 엔티티 수정

<code />
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") - 조인이 필요한 대상의 컬럼 네임 지정
<code />
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로는 갈 수 없는 상황이다. 이어서 양방향 연관관계에 대해 알아보자.

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

sec4  사진6

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

1.1.4.1. 양방향 관계를 위한 Team 수정

<code />
@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개로 끝)

1.1.4.2. JpaMain

<code />
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를 사용해야할까? 이 부분을 정확하게 이해해야한다.

1.1.4.3.  

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

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

sec4  사진7

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

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

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

sec4  사진8

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

1.1.5.1. 잘못된 매핑 예시

image

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

1.1.5.2. 올바른 매핑 예시

<code />
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가 필요하기 때문이다.

1.1.5.3. 연관관계 편의 메서드

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

1.1.6. 양방향 매핑 정리

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

 

1.2. 예제

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

1.2.1. 객체 구조

image

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

1.2.2. 주문 테이블

<code />
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; }

1.2.3. 주문 상품 테이블

<code />
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

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